Summary
When a server calls MarketplaceService:PromptPurchase() for an asset (including a free Limited UGC given as a prize), the server is never told by Roblox whether the purchase completed. The only signal is PromptPurchaseFinished, which is reported by the client, fires when the prompt UI closes rather than when the transaction completes, and never fires at all if the player leaves while the prompt is open.
In our experience this let one player turn a single legitimate prize win into 100 copies of a Limited UGC, 99 of them unearned, with a normal client and no exploit software.
Where and when
- Experience: Grab A Plush (group-owned)
- Item: Rosie Sea bunny, a free Limited UGC (1,000 copies) sold only in this experience and given to daily draw winners
- When: the loop was used on 2026-09-26, starting shortly after a legitimate win at 04:46 UTC. The underlying behaviour is not new; that is when we found it.
- Platforms: all. This is server/API behaviour, not client-specific.
- Frequency: 100%. Our logs show it working 99 times in a row.
Actual behaviour
- No server notification exists.
ProcessReceiptcovers developer products only. PromptPurchaseFinishedis client-reported. It can be spoofed or suppressed (existing report).- It fires when the prompt UI closes, not when the transaction completes. The purchase is final when the player presses Buy. If they leave, crash or disconnect while the “purchase complete” screen is still open, the event never fires, even though the copy was delivered.
- Nothing else can confirm a specific purchase.
PlayerOwnsAssetis a boolean. It can confirm a player’s first copy but can’t tell a second copy from the first.- Open Cloud
ListInventoryItemscan list copies, but only for players whose inventory is visible to Everyone, and only with a user-owned API key. Group-owned keys get401 "Authentication type provided was invalid!"(existing report).
So the server cannot tell “bought, then left” from “cancelled” or “transaction failed”.
Reproduction steps
- Publish a free (0 Robux) Limited UGC with a per-user copy limit above 1.
- In a server Script, call
MarketplaceService:PromptPurchase(player, assetId)and connectMarketplaceService.PromptPurchaseFinishedwith aprintin the handler. - Join a live server (not Studio) and press Buy on the prompt.
- When the “purchase complete” screen appears, do not dismiss it. Leave the experience.
- Observe: the player owns the new copy, and the server’s
PromptPurchaseFinishedhandler never ran.
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local ASSET_ID = 0 -- a free Limited UGC
MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
print("PromptPurchaseFinished", player.Name, assetId, isPurchased)
end)
Players.PlayerAdded:Connect(function(player)
task.wait(5)
MarketplaceService:PromptPurchase(player, ASSET_ID)
end)
How this became 100 copies
Our daily draw recorded a prize as owed, and the Claim button made the server call PromptPurchase. We cleared the prize only after PromptPurchaseFinished arrived and PlayerOwnsAsset confirmed ownership while the player was still connected.
We deliberately did not clear the prize before prompting. Transactions do fail, and a winner whose transaction failed would have lost the prize with no way to retry.
The winner pressed Buy, left before closing the prompt, and rejoined. The server never heard about the purchase, so the prize was still owed and was offered again. Every loop was a real copy out of the item’s limited stock. They stopped at 100 only because that was the item’s per-user limit, which can be raised but never lowered after creation.
Our workaround, and what it costs
We have patched our experience. Delivery is now proven by the server and never by the client:
- Each win is one atomic DataStore record that is settled, never deleted.
- A win is settled only when the server sees the delivered copy:
PlayerOwnsAssetturning true for a first copy, or a new instance ID from Open CloudListInventoryItemsfor a repeat win.isPurchasedis ignored. - Delivery is re-checked when the prompt closes, when the player leaves, and when they rejoin.
- Unconfirmed prompts per win are capped (5 for a first copy, 3 for a repeat win) with growing waits, after which the win is held for staff review rather than deleted.
It works, but every row here exists only because the server can’t learn a purchase outcome from Roblox:
| Limitation | Effect | What we had to build |
|---|---|---|
| No server-side completion event for asset purchases | “Bought then left” looks the same as “cancelled” or “failed” | Keep the win open until the server sees evidence; re-check on close, leave and join |
PromptPurchaseFinished is client-reported |
Spoofable, suppressible, never fires if the player leaves | Treated only as a hint to re-check |
PlayerOwnsAsset is a boolean |
Can’t confirm a second copy | Compare Open Cloud inventory instance IDs against the copies owned at the first prompt |
| Inventory API can’t read private inventories | Repeat winners with a private inventory can’t be verified | Ask the player to make their inventory public; the prize waits |
| Group-owned keys get 401 on the Inventory API | A group-owned experience depends on one person’s account key | User-owned key with inventory read only, in Secrets |
| No trusted outcome at all | Some residual risk remains | Prompt cap, then staff review |
| Client can still prompt Limited UGC itself (reported May 2026) | Server-side gates can be bypassed entirely | Detection only: a server prompt ledger compared against new copies |
Any experience that gives away Limited UGC through PromptPurchase and waits for PromptPurchaseFinished has the same hole.
Additional information
I have provided the affected user ID that exploited this bug in the private content section of this post in hopes that the transactions will be reversed or at least deleted so as to not inflate the market for this limited UGC item once they are able to sell it.
Related reports:
- MarketplaceService.PromptPurchaseFinished is not protected, being abused by exploiters
- Client-Side PromptPurchase Still Works for Limited UGC Items Despite Server-Side Restriction Announcement
- Group-owned API key doesn’t seem to work with Open Cloud Inventory API
Expected behavior
The server receives an authoritative, Roblox-originated notification for every completed asset purchase that the experience prompted, including when the player has since left. Developer products already have this in ProcessReceipt.
Proposed solution
Any one of these, in order of preference, would remove the need for the whole table above.
- Preferred: an asset receipt callback. A
ProcessReceipt-style server callback for asset and collectible purchases made through an experience’sPromptPurchase, carryingPlayerId,AssetId, a uniquePurchaseIdand the newCollectibleItemInstanceId. Retried until acknowledged, and delivered to a server even after the player has left. - Alternative: a prompt outcome lookup.
PromptPurchasereturns a prompt ID, and a server API such asMarketplaceService:GetPurchaseStatusAsync(id)returns its final outcome. - Minimum: a server-only copy count. For example
MarketplaceService:GetOwnedInstanceCountAsync(player, assetId)for assets the experience sells, not subject to inventory privacy.
Also helpful:
- Fire
PromptPurchaseFinishedwhen the transaction completes rather than when the UI closes, and document that it is client-reported. - Allow group-owned API keys on the Inventory API.
- Close the client-side
PromptPurchasepath for Limited UGC that is restricted to server prompts. - Allow a Limited’s per-user copy limit to be lowered.
A private message is associated with this bug report



