In donation games players can purchase GamePasses to support the creator, but there is a bug called “Fake Donation”, basically the players can spoof the PromptGamePassPurchaseFinished event and tell the server they just paid 50,000 ROBUX when they actually spent 5…
GamepassProtector MODULE
GamepassProtector closes that hole with a single RemoteEvent: the client asks the server to “lock” the pass before the prompt, the server caches the real price, then compares it to the live price after purchase. GET THE MODULE HERE
My solution
Client → server RemoteEvent (reliable or not) with the game-pass id.
Server uses MarketplaceService to snapshot the real price and stores it in a cache.
When PromptGamePassPurchaseFinished fires, server fetches the live price again; if the Robux value changed → kick for “fake donation”, otherwise return the info and award perks.
local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GamepassProtectorEvent = ReplicatedStorage:WaitForChild("GamepassProtectorEvent")
local LocalPlayer = Players.LocalPlayer
local GAMEPASS_ID = 1564484512
task.wait(5)
GamepassProtectorEvent:FireServer(GAMEPASS_ID) -- lock the gamepass
MarketplaceService:PromptGamePassPurchase(LocalPlayer, GAMEPASS_ID)
PromptGamePassPurchaseFinished is a client sent event that is fired when the user closes the purchase prompt. It does not handle if the item purchase was approved on their backend.
Because of this, Roblox recommends you use UserOwnsGamePassAsync on the documentation. An issue with this though is it has internal caching behavior so the result may return incorrectly if it was already called for the player.
What I mean is that in donation games (PLS DONATE, etc.) you can use a dummy account to buy a 5-Robux GamePass, and—without clicking “OK”—change the same GamePass’s price to a much higher amount (say 10,000 Robux). When you finally click “OK,” it processes the GamePass as if you paid the new 10,000-Robux price, because the MarketplaceService.PromptGamePassPurchaseFinished event (server-side) doesn’t tell you how many Robux were actually spent.
Hate to break it to you, but a response filled with incorrect grammar and punctuation is not “clear as day”; it’s just a sloppy, incoherent mess. I understood the general point he was trying to make, but having to re-read it several times because of poor grammar and punctuation isn’t exactly an enjoyable experience.
By comparison, you can understand people who are drunk, but their words may not always be coherent.
everything is wrong here, I’ve seen this multiple times in discussions in devforum, let me break the news to you:
PromptGamePassPurchaseFinished is SAFE: Roblox does internal checking to see if the user actually bought the gamepass before triggering the event, yes exploiters can try triggering this event but it won’t actually go through.
PromptProductPurchaseFinished is NOT SAFE: It can be triggered by exploiters.
ProcessReceipt is SAFE: Can’t be triggered by exploiters
Yes, but that’s not what this post is about.
It’s safe to process a GamePass purchase inside PromptGamePassPurchaseFinished the event hands you the three values you actually need (player, gamePassId, wasPurchased).
What it doesn’t give you is the amount of Robux that just changed hands. The usual workaround is to call
and read ProductInfo.PriceInRobux. That works perfectly in static catalogues, but it breaks the moment you allow players to create, re-price and sell their own donation GamePasses, the classic “donation board” model.
Here is why: PromptGamePassPurchaseFinished fires after the native purchase dialog is dismissed, either because the player cancelled or because they pressed the final “OK”. Between the moment the purchase prompt appeared and the moment the player closed it, the creator can change the GamePass price.
Exploit chain in practice:
Victim opens a 5 robux donation prompt.
Attacker quickly edits the same GamePass to 50,000 robux.
Victim presses “OK”.
PromptGamePassPurchaseFinished fires, wasPurchased == true, and your script still reads the new price from GetProductInfo, crediting the attacker with a 50,000 robux “donation” even though the victim only paid 5 robux.
You just logged a fake donation and probably auto-ranked the attacker to “top donor”.
This behaviour is documented (see PromptGamePassPurchaseFinished docs). Roblox has never added the actual sale price to the event, so the only reliable way to block the exploit is to cache the price that was shown when the prompt was triggered and refuse to credit anything if the cached value ≠ the live value when the event fires.
I see what u mean! my own system gets the price of passes on player join and caches all of it, which also becomes problematic since now the “attacker” can just change the price to 5 at any moment.
If im not mistaken, i believe if they time their new price perfectly they could still get away with it, cause when the RemoteEvent arrives on the server, the client may have already gotten the prompt
This would probably make it hard for a non exploiting user to do it, but i wouldnt be surprised if an exploiter did this:
Send the Event while the price is at 50k
Set the price to 5
Pull up the prompt on their client and buy it
Set the price back to 50k
Close the client prompt
I believe the steps above would cause this method to fail. But otherwise great resource! ill make sure to implement it later
Exploiters can join the game with the pass priced at 20k, set the price to 1, buy it on the roblox site, click the buy button, and since userownsgamepassasync is cached, it will return them as not owning it. then the exploiter signals to the server that they bought it, and the price was the same as when they clicked the buy button ingame, so it checks out.
Also, the roblox server does check whether you OWN the gamepass before allowing the signal to sent
Also also, the only way to REALLY make it secure is to use a cache bypass, which only a few games have access to., and requires an http proxy
Also also also, use product updated checks to make sure the pass was not updated AT LEAST 5 seconds before the donation time.
so sad, just keep a hold of the gamepass information when the booth is set and use that as the sole truth, it’s up to you to program stuff like this properly, the attack is also probably sufficiently annoying to carry out that it’s not that much worth it to get an advantage on a dumb leaderboard, there’s more important things to worry about than a donations leaderboard being polluted by badly written code that doesn’t account for quite bizarre race condition.