How to handle paid random items and PolicyService

Crates, eggs and spins bought with Robux or Robux-bought currency must respect ArePaidRandomItemsRestricted. How to check it on the server and show odds.

warningLuaudit check: paid-random-items

What it looks like

Players can spend Robux, or a currency that can be bought with Robux, on a random outcome: a crate, an egg, a spin or a luck boost. The server never checks PolicyService to see whether that player is allowed to buy random items.

Luau
OpenEgg.OnServerEvent:Connect(function(player)
	profile.Gems -= eggPrice
	hatch(player)
end)

How to detect it

  • Find every random reward and what it costs. Roblox counts it as a paid random item if it costs Robux directly or costs a currency the game sells for Robux. Follow the currency back to ProcessReceipt to see whether any developer product grants it.
  • Items that change the odds of a paid random item, such as luck boosts, pity systems or rate-up scrolls, count as paid random items too.
  • Look for a server-side call to PolicyService:GetPolicyInfoForPlayerAsync and a check of ArePaidRandomItemsRestricted before the server takes payment and rolls. When it's true, that player must not be able to open the paid random item.
  • A check that only hides the button on the client doesn't count. The server handler has to refuse.
  • GetPolicyInfoForPlayerAsync is a web call and can error, so it needs pcall. Check what happens when it fails: treating an unknown result as restricted is the safe choice.
  • Roblox also requires the odds of every outcome to be shown as percentages before purchase. That usually lives in menu code. If the menu isn't part of what you're auditing, say the odds display needs checking rather than reporting it.

What an exploiter could do

No exploit is needed. This is a Roblox policy requirement driven by laws in some countries. Players whose policy says paid random items are restricted can still buy them, which breaks Roblox's rules for the experience.

How to fix it

  • When a player joins, call GetPolicyInfoForPlayerAsync on the server inside pcall, retry on failure, and keep the result in a server table.
  • In every handler that sells a paid random item, refuse unless the stored result says paid random items aren't restricted for that player.
  • For restricted players, use one of the alternatives Roblox lists, such as selling the item directly at a fixed price, earning it through play, or showing that it's unavailable in their region.
  • Keep the odds in data the menu can read, and show every outcome's chance before purchase.

Vulnerable example

Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)

local eggPrice = 300
local pets = { "Cat", "Dog", "Dragon" }
local weights = { 70, 25, 5 }
local rng = Random.new()

local function hatch(): string
	local pick = rng:NextInteger(1, 100)
	for index, weight in weights do
		pick -= weight
		if pick <= 0 then
			return pets[index]
		end
	end
	return pets[#pets]
end

ReplicatedStorage.Remotes.OpenEgg.OnServerEvent:Connect(function(player: Player)
	local profile = PlayerData.get(player)
	if not profile or profile.Coins < eggPrice then
		return
	end
	profile.Coins -= eggPrice
	table.insert(profile.Inventory, hatch())
end)

Fixed example

Fixed
local Players = game:GetService("Players")
local PolicyService = game:GetService("PolicyService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)

local eggPrice = 300
local pets = { "Cat", "Dog", "Dragon" }
local weights = { 70, 25, 5 }
local rng = Random.new()
local allowed: { [Player]: boolean } = {}

local function hatch(): string
	local pick = rng:NextInteger(1, 100)
	for index, weight in weights do
		pick -= weight
		if pick <= 0 then
			return pets[index]
		end
	end
	return pets[#pets]
end

Players.PlayerAdded:Connect(function(player)
	for attempt = 1, 3 do
		local ok, policy = pcall(PolicyService.GetPolicyInfoForPlayerAsync, PolicyService, player)
		if ok then
			if player.Parent == Players then
				allowed[player] = policy.ArePaidRandomItemsRestricted == false
			end
			return
		end
		task.wait(attempt * 2)
	end
end)

Players.PlayerRemoving:Connect(function(player)
	allowed[player] = nil
end)

ReplicatedStorage.Remotes.OpenEgg.OnServerEvent:Connect(function(player: Player)
	local profile = PlayerData.get(player)
	if not profile or not allowed[player] or profile.Coins < eggPrice then
		return
	end
	profile.Coins -= eggPrice
	table.insert(profile.Inventory, hatch())
end)

Common false positives

  • Random rewards that can't be paid for, directly or through a currency sold for Robux, aren't paid random items. A chest opened with a key found in the world doesn't need the check.
  • A currency that can only be earned in play, never bought, doesn't make what it buys a paid random item. Check that no product, pass or bundle grants it.
  • A fixed, disclosed sequence of rewards, where the player knows each outcome before paying, isn't random.