How to grant rewarded video ad prizes safely
A remote that says an ad finished lets anyone skip the ad. Why ad prizes belong in ProcessReceipt, and how to use ShowRewardedVideoAdAsync correctly.
What it looks like
The game offers a prize for watching a rewarded video ad, but hands it out somewhere other than ProcessReceipt: when the client says the ad finished, or straight after ShowRewardedVideoAdAsync returns on the server.
Luau
remotes.AdWatched.OnServerEvent:Connect(function(player) profile.Coins += 200 end)
How to detect it
- Find every use of
AdService:ShowRewardedVideoAdAsync,CreateAdRewardFromDevProductIdandGetAdAvailabilityNowAsync. - Roblox's rewarded video ads guide makes every ad prize a developer product. When the player watches the whole ad, Roblox grants that product through
ProcessReceipt, withProductPurchaseChannelset toEnum.ProductPurchaseChannel.AdReward. Give the prize only in the game's receipt handling. If the game usesBindReceiptHandlerfor developer products, check which handler gets the ad product. ShowRewardedVideoAdAsynccan only be called from the server. A remote the client fires to say an ad finished, or to claim the prize afterwards, comes from a client that never has to show an ad. Any remote that grants an ad prize is a finding.- Look for code that grants the prize when
ShowRewardedVideoAdAsyncreturnsEnum.ShowAdResult.ShowCompleted. That bypasses Roblox's redelivery, which hands an unfinished receipt back toProcessReceiptwhen the player buys again or rejoins, and ifProcessReceiptalso grants the product, the player gets it twice. - Check the
pcallaround the call. The guide's own example passes thepcallsuccess flag to the client, but success only means the call didn't error.ShowInterrupted,AdNotReadyand the other results also return without an error, so a prize given on success alone is given for ads nobody watched. - Check the ad product has a branch in
ProcessReceipt. If it doesn't, the receipt goes unanswered and the player gets nothing.
What an exploiter could do
They fire the claim remote whenever they like and collect every ad prize without watching anything, as often as the handler allows. Where the server grants on the call's return as well as the receipt, every watched ad pays twice.
How to fix it
- Create the reward with
CreateAdRewardFromDevProductIdand grant it only inProcessReceipt, recording thePurchaseIdlike any other product. - Use the result of
ShowRewardedVideoAdAsynconly to tell the client what happened, never to grant anything. - The client may ask the server to show an ad, but no remote should grant the prize.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local remotes = ReplicatedStorage.Remotes local adPrize = 150 remotes.ClaimAdPrize.OnServerEvent:Connect(function(player: Player) local profile = PlayerData.get(player) if profile then profile.Coins += adPrize end end)
Fixed example
Fixed
local AdService = game:GetService("AdService") local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local remotes = ReplicatedStorage.Remotes local adProduct = 1100020 local adPrize = 150 local showing: { [Player]: boolean } = {} remotes.ClaimAdPrize.OnServerEvent:Connect(function(player: Player) if showing[player] then return end showing[player] = true local ok, result = pcall(function() return AdService:ShowRewardedVideoAdAsync(player, AdService:CreateAdRewardFromDevProductId(adProduct)) end) showing[player] = nil if player.Parent then remotes.ClaimAdPrize:FireClient(player, ok and result == Enum.ShowAdResult.ShowCompleted) end end) Players.PlayerRemoving:Connect(function(player) showing[player] = nil end) MarketplaceService.ProcessReceipt = function(receipt) if receipt.ProductId ~= adProduct then return Enum.ProductPurchaseDecision.NotProcessedYet end local player = Players:GetPlayerByUserId(receipt.PlayerId) local profile = player and PlayerData.get(player) if not profile then return Enum.ProductPurchaseDecision.NotProcessedYet end local key = "Purchase:" .. receipt.PurchaseId if not profile.Claimed[key] then profile.Claimed[key] = true profile.Coins += adPrize end return Enum.ProductPurchaseDecision.PurchaseGranted end
Common false positives
- A remote that only asks the server to show an ad, and grants nothing itself, is the pattern the guide recommends.
- Telling the client the result of
ShowRewardedVideoAdAsyncso it can update its interface is fine, as long as nothing is granted from it. - If the game has several
ProcessReceiptcallbacks, report that as ProcessReceipt set in more than one place, not here.