Roblox Studio Advanced Monetization Guide
When a player says they bought something and got nothing, a short, consistent log tells you what happened and keeps you from granting the same thing twice.
This is the failure log, not a monetization tour. It does not explain how to choose between passes and developer products, how to design a store, or how to write a full receipt handler. It assumes you already sell something and already have a server-side handler. Here you add the notes and logging that make a failed purchase traceable.
No prices, rates, or payout figures appear here. None of them help you debug.
Why a log beats memory
A purchase touches several places at once. The client shows a prompt. Roblox handles the payment. Your server receives a receipt. Your code grants the item and saves it.
When a player reports a problem, you usually hear about it hours later, with few details. If you did not record each step, you are guessing. Guessing leads to the worst outcome: granting the item again “to be safe,” then finding out the first grant worked after all.
The fields to record
For every purchase attempt, aim to capture these. Write them the same way every time.
- Time. Record it in UTC so logs from different servers line up. Note your local time separately if a player reports it that way.
- Player ID. The numeric
UserId, not the display name. Names can change. - Product ID. The developer product or pass ID involved.
- Purchase ID. The receipt’s
PurchaseIdfor developer products. The current docs use this as the key for granting a purchase exactly once. - Server ID.
game.JobId, so you know which server handled it. - Place version.
game.PlaceVersion, so you know which version of your code was live. - Did the prompt show? Yes, no, or unknown.
- Did the receipt callback run? Yes or no, and how many times for this purchase ID.
- Was the player in the server? At the moment the callback ran.
- Did the grant succeed? And did the save succeed?
- What did you return? Granted, or not processed yet.
Not every report will fill every field. That is fine. The gaps tell you where to look.
Step 1: Log the prompt on the client
When your shop button asks Roblox to show the purchase prompt, log it on the client. Wrap the call so you know whether it errored:
local ok, err = pcall(function()
MarketplaceService:PromptProductPurchase(player, PRODUCT_ID)
end)
print("[purchase] prompt", PRODUCT_ID, ok, err)
This only shows up in the client’s own log, so it helps most during your own tests. It answers the first question: did the prompt even try to open?
Step 2: Log the prompt result on the server, for notes only
MarketplaceService has a PromptProductPurchaseFinished event. The current reference lists its arguments as the user ID, the product ID, and whether it was purchased.
You can log it on the server:
MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId, productId, isPurchased)
print("[purchase] prompt_finished", userId, productId, isPurchased, game.JobId)
end)
Treat this as a note, never as a reason to grant anything. The developer product docs say plainly not to use this event to process purchases. A “true” here does not mean your grant ran.
Step 3: Log inside the receipt handler
Your existing receipt handler is where the real story lives. Add a log line at each decision point. Here is a shape you can fold into your own handler:
local function logReceipt(stage, receiptInfo, extra)
print("[purchase]", stage,
"time=" .. DateTime.now():ToIsoDate(),
"user=" .. tostring(receiptInfo.PlayerId),
"product=" .. tostring(receiptInfo.ProductId),
"purchase=" .. tostring(receiptInfo.PurchaseId),
"job=" .. game.JobId,
"ver=" .. tostring(game.PlaceVersion),
extra or "")
end
Then call it at these points:
- On entry.
logReceipt("receipt_start", receiptInfo). If this line never appears, the callback never ran on this server. - Player missing. If the player is not in the server, log
"player_absent"before you return not processed yet. Roblox can deliver the receipt again later. - Already granted. If your saved record shows this purchase ID was handled, log
"duplicate_skipped". - Grant failed. If your grant or save errors, log
"grant_failed"with the error message. - Granted. Log
"granted"right before you return the granted value.
Keep the log lines short and consistent. You want to search for one purchase ID and see its whole story in order.
Step 4: Know where to read server logs
The Developer Console shows log output in Studio tests and in live games. The current docs say you can open it with F9, by typing /console in chat, or from the in-game settings menu. Its Log view has a client and server toggle. Only the game owner or group members with edit permission can see server output.
Server output disappears when the server shuts down. If you need a record that outlives one server, write a small summary to a data store keyed by purchase ID. Check the current data store docs for limits before you log at volume.
What NOT to grant twice
This is the part that costs you the most if you get it wrong.
- Never grant from the prompt event. Only your receipt handler grants developer products.
- Never grant from a client request. A remote event from the client saying “I bought it” is not proof.
- Always check the purchase ID first. The current reference examples key the grant on
PurchaseIdand skip it if it was already granted. Your handler may be called more than once for the same purchase. - Never return granted before the save succeeds. If you return granted and the save fails, the player loses what they paid for.
- Do not hand-grant from a support message without checking your log. Search the purchase ID first. If the log shows
granted, the item exists somewhere, so look for a save or load bug instead.
Passes behave differently because a pass is owned once. Applying a pass perk should set a state, such as “has speed boost,” never add to a counter. That way, running your ownership check twice is harmless.
Reading the log when a player reports a problem
Match the pattern you see:
- No prompt line. The button or client code failed. The player likely was not charged.
- Prompt finished true, no receipt start. The callback did not run on that server. Check that your handler is set in a server script and that only one script sets it.
- Player absent, then nothing. The receipt is waiting. Check whether a later server logged it when the player rejoined.
- Grant failed, repeated. Your handler has a bug. Fix it, and let the retry deliver it.
- Duplicate skipped. Your safety check worked. Do not grant again.
Check the current return values
The reference pages checked for this article show ProcessReceipt returning an Enum.ProductPurchaseDecision, and they also describe a newer receipt-binding method that uses its own decision enum. Read the current MarketplaceService reference for the exact names your handler should return. Do not copy them from an old forum post.
Your next step
Add the log lines to your handler today, before the next report arrives. Then read the current Creator docs on testing purchases, including whether a given test spends real Robux, run one test, and confirm you can trace it from prompt to grant.