GamePass Protector: Anti fake donations

Problem

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

  1. Client → server RemoteEvent (reliable or not) with the game-pass id.

  2. Server uses MarketplaceService to snapshot the real price and stores it in a cache.

  3. 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.


Usage:


TESTING PLACE HERE


ServerScriptService / Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local MarketplaceService = game:GetService("MarketplaceService")

local GamepassProtectorModule = require(ServerStorage:WaitForChild("GamepassProtector"))
local GamepassProtectorEvent = ReplicatedStorage:WaitForChild("GamepassProtectorEvent")

GamepassProtectorEvent.OnServerEvent:Connect(function(player:Player, gamepassId:number)
	GamepassProtectorModule:LockGamepass(player, gamepassId)
end)

MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player:Player, gamepassId:number, wasPurchased:boolean)
	
	local GamepassInfo = GamepassProtectorModule:CheckGamepass(player, gamepassId, wasPurchased)
	warn(GamepassInfo)
	if not GamepassInfo then return end 
	
	warn("GAMEPASS INFO:", GamepassInfo)
	warn("GAMEPASS PRICE:", GamepassInfo.PriceInRobux)
	warn("HANDLE GAMEPASSES HERE...")
	
end)

ServerStorage / GamepassProtector (ModuleScript): GET THE MODULE


StarterGui (or StarterPlayer → StarterPlayerScripts) / LocalScript:

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)
6 Likes

ngl no offense or anything but this is kind of pointless if we have ProcessReceipt

11 Likes

ProcessReceipt is only for developer products. This module dedicated for gamepasses, they don’t have this kind of callback in marketplaceservice

2 Likes

ur telling me people actually can spoof the wasPurchased boolean? how?

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.

2 Likes

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.

This would make more sense if you didn’t use AI to generate your responses.

2 Likes

The response was as clear as day. Stop being so whinny about it.

2 Likes

ohh, not gonna lie there was no need for an ENTIRE module for it though?
it just needed a PSA and that’s about it right?

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.

1 Like

You’re right, but, tbh the module is an extra thing, I just wanted to give my possible solution for this

1 Like

Idc, but, thx for your opinion

Is AI illegal in your country? I don’t know about you, but in my country you can use AI however you want (in fact, this comment was made with AI). :slight_smile:

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

2 Likes

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

MarketplaceService:GetProductInfo(gamePassId, Enum.MarketplaceProductType.GamePass)

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:

  1. Victim opens a 5 robux donation prompt.

  2. Attacker quickly edits the same GamePass to 50,000 robux.

  3. Victim presses “OK”.

  4. 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.

  5. 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.

1 Like

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:

  1. Send the Event while the price is at 50k
  2. Set the price to 5
  3. Pull up the prompt on their client and buy it
  4. Set the price back to 50k
  5. 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.

(sorry if this made no sense)

SignalPromptGamepassPurchaseFinished , its a corescript onky function, but exploiters can call it

Or exploiters could just use replicatesignal function that their executors have

1 Like

basically, a race condition.

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.

you could also just use your brain & think for yourself, are you not capable of that?

2 Likes