How to check IsPaidItemTradingAllowed before players trade
Trading items bought with Robux, or with currency bought with Robux, needs a PolicyService check for each player. How to add it on the server.
What it looks like
Players can trade, or sell to each other, items that were bought with Robux or with a currency the game sells for Robux. The server never checks PolicyService to see whether each player is allowed to trade paid items.
Luau
remotes.SendTrade.OnServerEvent:Connect(function(player, target, items) startTrade(player, target, items) end)
How to detect it
- Find every way an item moves from one player to another: trades, player markets, auction houses, stalls and listings for coins.
- Work out whether any of those items are paid items. An item is paid if it was bought with Robux, or with a currency any developer product grants, or came from a paid random item such as a crate bought with that currency. Follow the currency back to
ProcessReceipt. - Look for a server-side
PolicyService:GetPolicyInfoForPlayerAsynccall and a check ofIsPaidItemTradingAllowedbefore a player can start, join or complete a trade that includes paid items. Each player in the trade needs their own check. GetPolicyInfoForPlayerAsyncis a web call and can error, so it needspcall. Treating an unknown result as not allowed is the safe choice.- A check that only hides the trade button on the client doesn't count. The server handler has to refuse.
- Gifts between players move items the same way. If paid items can be gifted, say the gift path needs the same check rather than treating it as certain.
- Robux prices can differ by region. Roblox's regional pricing guide warns that trades and gifts of paid items between players with different price levels allow price arbitrage, and suggests
MarketplaceService:GetUsersPriceLevelsAsyncto limit those transfers. It says to call it when players join rather than caching the result between sessions. Mention it where paid items move between players, as advice rather than a finding.
What an exploiter could do
No exploit is needed. Roblox's paid random items page says that when IsPaidItemTradingAllowed is false, creators must not let that player trade paid items, including what came from a paid random item. Without the check, players in places where this is restricted can still trade them, which breaks Roblox's rules for the experience.
How to fix it
- When a player joins, call
GetPolicyInfoForPlayerAsyncon the server insidepcall, retry on failure, and keepIsPaidItemTradingAllowedin a server table. - In every handler that starts, accepts or completes a trade or listing, refuse when the stored value isn't
truefor either player and paid items are involved. - Let those players keep and use what they bought. Only the transfer to others is restricted.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = ReplicatedStorage.Remotes local requests: { [Player]: Player } = {} remotes.RequestTrade.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end requests[player] = target remotes.TradeRequested:FireClient(target, player) end) Players.PlayerRemoving:Connect(function(player) requests[player] = nil for from, to in requests do if to == player then requests[from] = nil end end end)
Fixed example
Fixed
local Players = game:GetService("Players") local PolicyService = game:GetService("PolicyService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = ReplicatedStorage.Remotes local requests: { [Player]: Player } = {} local canTrade: { [Player]: boolean } = {} local function checkPolicy(player: Player) for attempt = 1, 3 do local ok, policy = pcall(PolicyService.GetPolicyInfoForPlayerAsync, PolicyService, player) if ok then if player.Parent == Players then canTrade[player] = policy.IsPaidItemTradingAllowed == true end return end task.wait(attempt * 2) end end remotes.RequestTrade.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end if not canTrade[player] or not canTrade[target] then return end requests[player] = target remotes.TradeRequested:FireClient(target, player) end) Players.PlayerAdded:Connect(checkPolicy) for _, player in Players:GetPlayers() do task.spawn(checkPolicy, player) end Players.PlayerRemoving:Connect(function(player) canTrade[player] = nil requests[player] = nil for from, to in requests do if to == player then requests[from] = nil end end end)
Common false positives
- Trading items that can only be earned in play, never bought with Robux or a bought currency, isn't paid item trading. Check that no product, pass or bundle leads to them.
- Games where every item in the trade comes from free play can trade freely even if other items in the game are paid, as long as the server keeps paid items out of trades.
- A trade system that's checked once on join and refuses everyone whose result is unknown is the fix, not a finding.