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.

criticalLuaudit check: transfer-rewards-outside-receipt

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 * 10

How to detect it

  • Find every call to PromptRobuxTransferAsync and every reward tied to sending or receiving Robux.
  • Roblox's reference says PromptRobuxTransferAsync returns a transferRequestId for 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:BindReceiptHandler with Enum.ReceiptType.RobuxTransferSender (and RobuxTransferReceiver if 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 PurchaseId in saved data and skips ones it has seen. It should return Enum.ReceiptDecision.NotProcessedYet when the player isn't in the server or the grant fails, so the receipt is delivered again, and Processed only 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 BindReceiptHandler handler for RobuxTransferSender, sized from CurrencySpent and recorded by PurchaseId.
  • Return NotProcessedYet until 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 transferRequestId or 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 ProcessReceipt or BindReceiptHandler belong to the other monetisation checks.