How to reward Robux transfers only when they complete
PromptRobuxTransferAsync only opens the prompt. How to grant thank-you rewards from the transfer receipt with BindReceiptHandler.
What it looks like
The game thanks players for sending Robux with MarketplaceService:PromptRobuxTransferAsync, but gives the thank-you reward when the prompt opens, when the client says the transfer went through, or for the amount the client asked to send. It doesn't wait for the transfer receipt.
Luau
local requestId = MarketplaceService:PromptRobuxTransferAsync(player, receiverId, amount)
profile.Coins += amount * 10How to detect it
- Find every call to
PromptRobuxTransferAsyncand every reward tied to sending or receiving Robux. - Roblox's reference says
PromptRobuxTransferAsyncreturns atransferRequestIdfor the request. Its Robux transfers guide says to process the receipt before granting a reward, so it's only given for completed transfers. A reward given when the call returns is given for transfers the player cancelled or that never settled. - Look for
MarketplaceService:BindReceiptHandlerwithEnum.ReceiptType.RobuxTransferSender(andRobuxTransferReceiverif receivers are rewarded). Rewards belong in those handlers. - Check the reward is sized from the receipt's
CurrencySpent, not from an amount the client sent or the amount passed to the prompt. - Check each handler records the receipt's
PurchaseIdin saved data and skips ones it has seen. It should returnEnum.ReceiptDecision.NotProcessedYetwhen the player isn't in the server or the grant fails, so the receipt is delivered again, andProcessedonly once the reward is given. - A remote the client fires to say a transfer finished is a finding whatever it checks, since the client never has to send anything.
What an exploiter could do
They open the transfer prompt and close it, or fire the "transfer done" remote, and collect the reward for Robux they never sent. With the amount taken from the client, they claim the largest reward every time.
How to fix it
- Open the prompt from the server, but grant nothing there.
- Grant the reward in a
BindReceiptHandlerhandler forRobuxTransferSender, sized fromCurrencySpentand recorded byPurchaseId. - Return
NotProcessedYetuntil the reward is safely granted.
Vulnerable example
Vulnerable
local MarketplaceService = game:GetService("MarketplaceService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local coinsPerRobux = 10 ReplicatedStorage.Remotes.TipCreator.OnServerEvent:Connect(function(player: Player, receiverId: unknown, amount: unknown) if typeof(receiverId) ~= "number" or typeof(amount) ~= "number" or amount < 10 or amount > 500 or amount % 1 ~= 0 then return end local ok = pcall(MarketplaceService.PromptRobuxTransferAsync, MarketplaceService, player, receiverId, amount) local profile = PlayerData.get(player) if ok and profile then profile.Coins += amount * coinsPerRobux end end)
Fixed example
Fixed
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local coinsPerRobux = 10 ReplicatedStorage.Remotes.TipCreator.OnServerEvent:Connect(function(player: Player, receiverId: unknown, amount: unknown) if typeof(receiverId) ~= "number" or typeof(amount) ~= "number" or amount < 10 or amount > 500 or amount % 1 ~= 0 then return end pcall(MarketplaceService.PromptRobuxTransferAsync, MarketplaceService, player, receiverId, amount) end) MarketplaceService:BindReceiptHandler(Enum.ReceiptType.RobuxTransferSender, function(receipt: { [string]: any }) local player = Players:GetPlayerByUserId(receipt.PlayerId) local profile = player and PlayerData.get(player) if not profile then return Enum.ReceiptDecision.NotProcessedYet end local key = "Transfer:" .. receipt.PurchaseId if not profile.Claimed[key] then profile.Claimed[key] = true profile.Coins += receipt.CurrencySpent * coinsPerRobux end return Enum.ReceiptDecision.Processed end)
Common false positives
- Logging the
transferRequestIdor showing a "thanks for opening the prompt" message gives nothing away. - Games that only offer transfers between players, with no reward, have nothing to grant.
- Developer product receipts handled with
ProcessReceiptorBindReceiptHandlerbelong to the other monetisation checks.