roblox-monetization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- Source: brockmartin/roblox-game-skill (MIT) -->

Roblox Monetization Systems Reference

Roblox变现系统参考文档

1. Overview

1. 概述

Load this reference when:
  • Adding in-game purchases (GamePasses, Developer Products)
  • Designing or revising a monetization strategy
  • Optimizing revenue (pricing, placement, conversion funnels)
  • Implementing Premium Payouts or Rewarded Video Ads
  • Calculating DevEx projections
  • Reviewing monetization ethics and Roblox policy compliance
Roblox provides four primary monetization channels: GamePasses (one-time permanent unlocks), Developer Products (consumable/repeatable purchases), Premium Payouts (revenue from Premium subscribers playing your game), and Rewarded Video Ads (ad-based revenue). Each channel serves a different purpose and should be combined strategically.
Key principle: All purchase granting MUST happen on the server. Never trust the client to determine what a player owns or has purchased.

在以下场景中查阅本参考文档:
  • 添加游戏内购买功能(GamePasses、Developer Products)
  • 设计或修订变现策略
  • 优化收益(定价、放置位置、转化漏斗)
  • 实现Premium收益分成或激励视频广告
  • 计算DevEx收益预估
  • 审核变现伦理与Roblox政策合规性
Roblox提供四种主要变现渠道:GamePasses(一次性永久解锁)、Developer Products(可消耗/重复购买)、Premium收益分成(来自Premium玩家游玩你的游戏的收益)和激励视频广告(基于广告的收益)。每个渠道用途不同,需结合使用以达成策略目标。
核心原则:所有购买奖励必须在服务器端发放。绝不要信任客户端来判断玩家拥有或已购买的内容。

Quick Reference

快速参考

Load Full Reference below only when you need specific API implementations or pricing formulas.
Key rules:
  • GamePasses: one-time purchase, check with UserOwnsGamePassAsync on join + cache.
  • Developer Products: consumable, ProcessReceipt is the ONLY place to grant items.
  • ProcessReceipt contract: grant item THEN return PurchaseGranted. If grant fails, return NotProcessedYet. Never return PurchaseGranted before granting.
  • All purchase logic is SERVER-SIDE. Client only prompts.
  • PromptGamePassPurchase / PromptProductPurchase from client, handle on server.
  • TOS: odds disclosure MANDATORY for random items. Games get removed without it.
  • TOS: no real-world trading, no misleading purchase UI, no pay-to-win that ruins gameplay.
  • DevEx: dual-rate system. New Rate $0.0038/R$ (earned after Sept 5, 2025). Old Rate $0.0035/R$ (earned before). Must clear Old Rate balance first before New Rate kicks in.
  • Premium Payouts: engagement-based, detect with player.MembershipType.
  • Subscriptions: recurring monthly revenue via PromptSubscriptionPurchase. Tiered benefits.
  • Private Servers: monetizable via PromptCreatePrivateServer / PromptPurchasePrivateServer.
  • Paid Access: one-time Robux or local currency fee via PromptPurchaseExperience. Common for closed betas.
  • Immersive Ads: AdService image/portal/video ad units. Earn via ad views, separate from Rewarded Video Ads.
  • PolicyService: must-check for compliance (age/region restrictions on subscriptions, random items, ads).
  • Commerce Products: sell physical merchandise through Roblox.
  • Creator Store: sell plugins ($4.99+) and models ($2.99+) for USD. 30-day escrow hold.
  • Never store purchase state only in DataStore without session locking (use ProfileStore).

仅当你需要具体API实现或定价公式时,才查看下方完整参考内容。
关键规则:
  • GamePasses:一次性购买,在玩家加入时通过UserOwnsGamePassAsync检查并缓存结果。
  • Developer Products:可消耗物品,ProcessReceipt是唯一可以发放物品的位置。
  • ProcessReceipt约定:先发放物品,再返回PurchaseGranted。如果发放失败,返回NotProcessedYet。绝不要在发放前返回PurchaseGranted。
  • 所有购买逻辑均为服务器端实现。客户端仅负责触发购买提示。
  • 在客户端调用PromptGamePassPurchase / PromptProductPurchase,在服务器端处理结果。
  • 服务条款:随机物品必须披露概率。未遵守此规则的游戏会被下架。
  • 服务条款:禁止现实交易、误导性购买UI、破坏游戏体验的付费获胜机制。
  • DevEx:双汇率系统。新汇率$0.0038/R$(2025年9月5日及之后赚取的Robux)。旧汇率$0.0035/R$(2025年9月5日之前赚取的Robux)。必须先清空旧汇率余额,新汇率才会生效。
  • Premium收益分成:基于玩家参与度,通过player.MembershipType检测。
  • 订阅服务:通过PromptSubscriptionPurchase实现月度 recurring 收入,提供分层福利。
  • 私人服务器:通过PromptCreatePrivateServer / PromptPurchasePrivateServer实现变现。
  • 付费准入:通过PromptPurchaseExperience收取一次性Robux或本地货币费用,常用于封闭测试。
  • 沉浸式广告:通过AdService提供图片/传送门/视频广告单元,通过广告浏览赚取收益,与激励视频广告分离。
  • PolicyService:必须检查合规性(订阅、随机物品、广告的年龄/地区限制)。
  • 商业产品:通过Roblox销售实体商品。
  • Creator Store:以美元销售插件(最低$4.99)和模型(最低$2.99),有30天托管期。
  • 绝不要仅在DataStore中存储购买状态而不使用会话锁(使用ProfileStore)。

Full Reference

完整参考

2. GamePasses

2. GamePasses

GamePasses are one-time permanent purchases tied to the player's account. Once bought, the player owns it forever across all sessions. Ideal for VIP perks, permanent stat boosts, cosmetic bundles, and feature unlocks.
GamePasses是与玩家账号绑定的一次性永久购买项。一旦购买,玩家将永久拥有该权益,跨所有游戏会话均有效。适用于VIP特权、永久属性提升、外观捆绑包和功能解锁。

Core API

核心API

Method / EventPurpose
MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId)
Check if player owns a GamePass
MarketplaceService:PromptGamePassPurchase(player, gamePassId)
Show the purchase prompt to a player
MarketplaceService.PromptGamePassPurchaseFinished
Fires when the prompt closes (purchased or cancelled)
方法/事件用途
MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId)
检查玩家是否拥有某个GamePass
MarketplaceService:PromptGamePassPurchase(player, gamePassId)
向玩家显示购买提示
MarketplaceService.PromptGamePassPurchaseFinished
当购买提示关闭时触发(无论购买完成或取消)

Complete GamePass System (Server Script)

完整GamePass系统(服务器脚本)

Place this in
ServerScriptService
:
luau
-- ServerScriptService/GamePassService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

-- ===== CONFIGURATION =====
-- Map each GamePass ID to a function that grants its perks.
-- Add new passes here; the rest of the system handles them automatically.
local GAME_PASSES = {
	[123456789] = {
		name = "VIP",
		grant = function(player: Player)
			-- Example: tag the player so other scripts can check
			player:SetAttribute("IsVIP", true)

			-- Example: give a permanent speed boost
			local character = player.Character or player.CharacterAdded:Wait()
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if humanoid then
				humanoid.WalkSpeed = 24
			end
		end,
	},
	[987654321] = {
		name = "2x Coins",
		grant = function(player: Player)
			player:SetAttribute("CoinMultiplier", 2)
		end,
	},
}

-- ===== GRANT PERKS ON JOIN =====
-- Check every configured GamePass when the player joins.
local function onPlayerAdded(player: Player)
	for gamePassId, passInfo in GAME_PASSES do
		local success, ownsPass = pcall(function()
			return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
		end)

		if success and ownsPass then
			local grantSuccess, grantErr = pcall(passInfo.grant, player)
			if not grantSuccess then
				warn(`[GamePass] Failed to grant "{passInfo.name}" to {player.Name}: {grantErr}`)
			end
		elseif not success then
			warn(`[GamePass] Failed to check ownership of {passInfo.name} for {player.Name}: {ownsPass}`)
		end
	end

	-- Re-grant perks on every respawn (speed, accessories, etc.)
	player.CharacterAdded:Connect(function()
		for gamePassId, passInfo in GAME_PASSES do
			if player:GetAttribute("IsVIP") or player:GetAttribute("CoinMultiplier") then
				-- Only re-grant if we already confirmed ownership
				local success, ownsPass = pcall(function()
					return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
				end)
				if success and ownsPass then
					pcall(passInfo.grant, player)
				end
			end
		end
	end)
end

-- ===== GRANT PERKS ON PURCHASE (mid-session) =====
-- If the player buys a GamePass while already in-game, grant immediately.
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, gamePassId: number, wasPurchased: boolean)
	if not wasPurchased then
		return
	end

	local passInfo = GAME_PASSES[gamePassId]
	if not passInfo then
		return
	end

	local success, err = pcall(passInfo.grant, player)
	if success then
		print(`[GamePass] Granted "{passInfo.name}" to {player.Name} (mid-session purchase)`)
	else
		warn(`[GamePass] Failed to grant "{passInfo.name}" to {player.Name}: {err}`)
	end
end)

-- ===== INITIALIZE =====
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
将此脚本放置在
ServerScriptService
中:
luau
-- ServerScriptService/GamePassService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

-- ===== 配置项 =====
-- 将每个GamePass ID映射到一个发放对应特权的函数。
-- 在此处添加新的GamePass,系统其余部分会自动处理。
local GAME_PASSES = {
	[123456789] = {
		name = "VIP",
		grant = function(player: Player)
			-- 示例:标记玩家,以便其他脚本检查
			player:SetAttribute("IsVIP", true)

			-- 示例:给予永久速度加成
			local character = player.Character or player.CharacterAdded:Wait()
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if humanoid then
				humanoid.WalkSpeed = 24
			end
		end,
	},
	[987654321] = {
		name = "2倍金币",
		grant = function(player: Player)
			player:SetAttribute("CoinMultiplier", 2)
		end,
	},
}

-- ===== 玩家加入时发放特权 =====
-- 玩家加入时检查所有已配置的GamePass。
local function onPlayerAdded(player: Player)
	for gamePassId, passInfo in GAME_PASSES do
		local success, ownsPass = pcall(function()
			return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
		end)

		if success and ownsPass then
			local grantSuccess, grantErr = pcall(passInfo.grant, player)
			if not grantSuccess then
				warn(`[GamePass] 向{player.Name}发放"{passInfo.name}"失败:{grantErr}`)
			end
		elseif not success then
			warn(`[GamePass] 检查{player.Name}是否拥有{passInfo.name}失败:{ownsPass}`)
		end
	end

	-- 玩家每次重生时重新发放特权(速度、配饰等)
	player.CharacterAdded:Connect(function()
		for gamePassId, passInfo in GAME_PASSES do
			if player:GetAttribute("IsVIP") or player:GetAttribute("CoinMultiplier") then
				-- 仅在已确认拥有权的情况下重新发放
				local success, ownsPass = pcall(function()
					return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
				end)
				if success and ownsPass then
					pcall(passInfo.grant, player)
				end
			end
		end
	end)
end

-- ===== 游戏中购买时发放特权 =====
-- 如果玩家在游戏中购买GamePass,立即发放特权。
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, gamePassId: number, wasPurchased: boolean)
	if not wasPurchased then
		return
	end

	local passInfo = GAME_PASSES[gamePassId]
	if not passInfo then
		return
	end

	local success, err = pcall(passInfo.grant, player)
	if success then
		print(`[GamePass] 向{player.Name}发放"{passInfo.name}"(游戏内购买)`)
	else
		warn(`[GamePass] 向{player.Name}发放"{passInfo.name}"失败:{err}`)
	end
end)

-- ===== 初始化 =====
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Prompting Purchases (Server or Client)

触发购买(服务器或客户端)

luau
-- Client-side: prompt a GamePass purchase from a button, shop GUI, etc.
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local VIP_PASS_ID = 123456789

local function promptVIPPurchase()
	MarketplaceService:PromptGamePassPurchase(Players.LocalPlayer, VIP_PASS_ID)
end

-- Connect to a shop button
script.Parent.MouseButton1Click:Connect(promptVIPPurchase)

luau
-- 客户端:从按钮、商店GUI等触发GamePass购买
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local VIP_PASS_ID = 123456789

local function promptVIPPurchase()
	MarketplaceService:PromptGamePassPurchase(Players.LocalPlayer, VIP_PASS_ID)
end

-- 绑定到商店按钮
script.Parent.MouseButton1Click:Connect(promptVIPPurchase)

3. Developer Products

3. Developer Products

Developer Products are consumable/repeatable purchases. Players can buy them multiple times. Ideal for currency packs, temporary boosts, extra lives, loot crates, and skip-timers.
Developer Products是可消耗/重复购买项。玩家可以多次购买。适用于货币包、临时增益、额外生命、战利品箱和跳过计时器。

Core API

核心API

Method / EventPurpose
MarketplaceService:PromptProductPurchase(player, productId)
Show the purchase prompt
MarketplaceService.ProcessReceipt
CRITICAL callback Roblox invokes to confirm granting
方法/事件用途
MarketplaceService:PromptProductPurchase(player, productId)
显示购买提示
MarketplaceService.ProcessReceipt
关键回调函数,Roblox调用它来确认物品发放

The ProcessReceipt Contract

ProcessReceipt约定

ProcessReceipt
is the single most important callback in Roblox monetization. Roblox calls it and expects one of two return values:
Return ValueMeaning
Enum.ProductPurchaseDecision.PurchaseGranted
Item was successfully granted. Roblox finalizes the purchase. Returning this without actually granting is a policy violation and causes player complaints.
Enum.ProductPurchaseDecision.NotProcessedYet
Granting failed or could not be confirmed. Roblox will retry calling ProcessReceipt later (including on rejoin).
Rules:
  • Only ONE script can set
    MarketplaceService.ProcessReceipt
    . If two scripts both assign it, only the last one takes effect and the other is silently overwritten.
  • Return
    PurchaseGranted
    ONLY after successfully persisting the granted item (DataStore save confirmed).
  • If DataStore save fails, return
    NotProcessedYet
    so Roblox retries.
  • Always handle the case where the player has left the game before ProcessReceipt fires.
ProcessReceipt
是Roblox变现中最重要的回调函数。Roblox调用它并期望以下两种返回值之一:
返回值含义
Enum.ProductPurchaseDecision.PurchaseGranted
物品已成功发放。Roblox完成购买流程。未实际发放物品就返回此值属于政策违规,会引发玩家投诉。
Enum.ProductPurchaseDecision.NotProcessedYet
发放失败或无法确认。Roblox会重试调用ProcessReceipt(包括玩家重新加入游戏时)。
规则:
  • 只有一个脚本可以设置
    MarketplaceService.ProcessReceipt
    。如果两个脚本都赋值,只有最后一个生效,另一个会被静默覆盖。
  • 仅在成功持久化已发放物品(确认DataStore保存成功)后,才返回
    PurchaseGranted
  • 如果DataStore保存失败,返回
    NotProcessedYet
    以便Roblox重试。
  • 务必处理ProcessReceipt触发前玩家已离开游戏的情况。

Complete Developer Product System (Server Script)

完整Developer Product系统(服务器脚本)

Place this in
ServerScriptService
:
luau
-- ServerScriptService/DeveloperProductService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local purchaseHistoryStore = DataStoreService:GetDataStore("PurchaseHistory")

-- ===== CONFIGURATION =====
-- Map each product ID to a handler that grants the item.
-- The handler receives the player and must return true on success.
local PRODUCTS = {
	[111111111] = {
		name = "100 Coins",
		grant = function(player: Player): boolean
			local leaderstats = player:FindFirstChild("leaderstats")
			if not leaderstats then
				return false
			end

			local coins = leaderstats:FindFirstChild("Coins")
			if not coins then
				return false
			end

			coins.Value += 100
			return true
		end,
	},
	[222222222] = {
		name = "500 Coins",
		grant = function(player: Player): boolean
			local leaderstats = player:FindFirstChild("leaderstats")
			if not leaderstats then
				return false
			end

			local coins = leaderstats:FindFirstChild("Coins")
			if not coins then
				return false
			end

			coins.Value += 500
			return true
		end,
	},
	[333333333] = {
		name = "Speed Boost (60s)",
		grant = function(player: Player): boolean
			local character = player.Character
			if not character then
				return false
			end

			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if not humanoid then
				return false
			end

			humanoid.WalkSpeed = 32
			task.delay(60, function()
				if humanoid and humanoid.Parent then
					humanoid.WalkSpeed = 16
				end
			end)
			return true
		end,
	},
}

-- ===== PROCESS RECEIPT CALLBACK =====
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
	-- 1. Check if this purchase was already granted (idempotency guard)
	local purchaseKey = `{receiptInfo.PlayerId}_{receiptInfo.PurchaseId}`

	local alreadyGranted = false
	local lookupSuccess, lookupErr = pcall(function()
		alreadyGranted = purchaseHistoryStore:GetAsync(purchaseKey)
	end)

	if not lookupSuccess then
		-- Cannot verify history; retry later to avoid duplicates
		warn(`[Product] DataStore lookup failed for {purchaseKey}: {lookupErr}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if alreadyGranted then
		-- Already granted in a previous attempt; finalize
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end

	-- 2. Find the product handler
	local productInfo = PRODUCTS[receiptInfo.ProductId]
	if not productInfo then
		warn(`[Product] No handler for product ID {receiptInfo.ProductId}`)
		-- Unknown product: still return NotProcessedYet so it can be handled
		-- after a code update adds the missing handler
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 3. Find the player (they may have left before ProcessReceipt fires)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if not player then
		-- Player left; retry on their next join
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 4. Grant the item
	local grantSuccess = false
	local grantOk, grantErr = pcall(function()
		grantSuccess = productInfo.grant(player)
	end)

	if not grantOk then
		warn(`[Product] Grant error for "{productInfo.name}" to {player.Name}: {grantErr}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if not grantSuccess then
		warn(`[Product] Grant returned false for "{productInfo.name}" to {player.Name}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 5. Record the purchase BEFORE returning PurchaseGranted
	local saveSuccess, saveErr = pcall(function()
		purchaseHistoryStore:SetAsync(purchaseKey, true)
	end)

	if not saveSuccess then
		-- Grant succeeded but save failed. This is the hardest edge case.
		-- Returning PurchaseGranted risks no record if we crash before saving.
		-- Returning NotProcessedYet risks a duplicate grant on retry.
		-- Best practice: return PurchaseGranted since the player already received
		-- the item, and log the failure for manual reconciliation.
		warn(`[Product] CRITICAL: Grant succeeded but history save failed for {purchaseKey}: {saveErr}`)
	end

	print(`[Product] Granted "{productInfo.name}" to {player.Name} (PurchaseId: {receiptInfo.PurchaseId})`)
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

-- ===== ASSIGN CALLBACK (only one script can do this) =====
MarketplaceService.ProcessReceipt = processReceipt
将此脚本放置在
ServerScriptService
中:
luau
-- ServerScriptService/DeveloperProductService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local purchaseHistoryStore = DataStoreService:GetDataStore("PurchaseHistory")

-- ===== 配置项 =====
-- 将每个产品ID映射到一个处理物品发放的函数。
-- 处理函数接收玩家对象,成功时返回true。
local PRODUCTS = {
	[111111111] = {
		name = "100金币",
		grant = function(player: Player): boolean
			local leaderstats = player:FindFirstChild("leaderstats")
			if not leaderstats then
				return false
			end

			local coins = leaderstats:FindFirstChild("Coins")
			if not coins then
				return false
			end

			coins.Value += 100
			return true
		end,
	},
	[222222222] = {
		name = "500金币",
		grant = function(player: Player): boolean
			local leaderstats = player:FindFirstChild("leaderstats")
			if not leaderstats then
				return false
			end

			local coins = leaderstats:FindFirstChild("Coins")
			if not coins then
				return false
			end

			coins.Value += 500
			return true
		end,
	},
	[333333333] = {
		name = "速度增益(60秒)",
		grant = function(player: Player): boolean
			local character = player.Character
			if not character then
				return false
			end

			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if not humanoid then
				return false
			end

			humanoid.WalkSpeed = 32
			task.delay(60, function()
				if humanoid and humanoid.Parent then
					humanoid.WalkSpeed = 16
				end
			end)
			return true
		end,
	},
}

-- ===== PROCESS RECEIPT回调函数 =====
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
	-- 1. 检查此购买是否已发放(幂等性保护)
	local purchaseKey = `{receiptInfo.PlayerId}_{receiptInfo.PurchaseId}`

	local alreadyGranted = false
	local lookupSuccess, lookupErr = pcall(function()
		alreadyGranted = purchaseHistoryStore:GetAsync(purchaseKey)
	end)

	if not lookupSuccess then
		-- 无法验证历史记录;稍后重试以避免重复发放
		warn(`[Product] {purchaseKey}的DataStore查询失败:{lookupErr}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if alreadyGranted then
		-- 之前已发放;完成购买流程
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end

	-- 2. 查找产品处理函数
	local productInfo = PRODUCTS[receiptInfo.ProductId]
	if not productInfo then
		warn(`[Product] 产品ID {receiptInfo.ProductId}无对应处理函数`)
		-- 未知产品:仍返回NotProcessedYet,以便代码更新添加处理函数后再处理
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 3. 查找玩家(ProcessReceipt触发前玩家可能已离开)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if not player then
		-- 玩家已离开;下次加入时重试
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 4. 发放物品
	local grantSuccess = false
	local grantOk, grantErr = pcall(function()
		grantSuccess = productInfo.grant(player)
	end)

	if not grantOk then
		warn(`[Product] 向{player.Name}发放"{productInfo.name}"出错:{grantErr}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if not grantSuccess then
		warn(`[Product] 向{player.Name}发放"{productInfo.name}"返回失败`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 5. 返回PurchaseGranted前记录购买历史
	local saveSuccess, saveErr = pcall(function()
		purchaseHistoryStore:SetAsync(purchaseKey, true)
	end)

	if not saveSuccess then
		-- 发放成功但保存失败。这是最棘手的边缘情况。
		-- 返回PurchaseGranted可能导致崩溃前未记录购买历史。
		-- 返回NotProcessedYet可能导致重试时重复发放。
		-- 最佳实践:返回PurchaseGranted,因为玩家已收到物品,并记录失败以便手动对账。
		warn(`[Product] 严重问题:{purchaseKey}发放成功但历史记录保存失败:{saveErr}`)
	end

	print(`[Product] 向{player.Name}发放"{productInfo.name}"(PurchaseId: {receiptInfo.PurchaseId})`)
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

-- ===== 赋值回调函数(仅一个脚本可执行此操作) =====
MarketplaceService.ProcessReceipt = processReceipt

Prompting Developer Product Purchases (Client)

触发Developer Product购买(客户端)

luau
-- Client-side shop button example
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local COINS_100_PRODUCT_ID = 111111111

script.Parent.MouseButton1Click:Connect(function()
	MarketplaceService:PromptProductPurchase(Players.LocalPlayer, COINS_100_PRODUCT_ID)
end)

luau
-- 客户端商店按钮示例
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local COINS_100_PRODUCT_ID = 111111111

script.Parent.MouseButton1Click:Connect(function()
	MarketplaceService:PromptProductPurchase(Players.LocalPlayer, COINS_100_PRODUCT_ID)
end)

4. Premium Payouts

4. Premium收益分成

Roblox automatically pays developers based on how much time Premium subscribers spend in their game. There is no purchase prompt; you earn passively. The more engagement time from Premium players, the higher the payout.
Roblox会根据Premium玩家在你的游戏中的游玩时长自动向开发者支付收益。无需购买提示,属于被动收益。Premium玩家的参与时长越长,收益越高。

Detecting Premium Players

检测Premium玩家

luau
-- ServerScriptService/PremiumService.lua
local Players = game:GetService("Players")

local function grantPremiumPerks(player: Player)
	player:SetAttribute("IsPremium", true)

	-- Example perks to incentivize Premium play time:
	-- Extra daily reward, exclusive cosmetics, bonus XP, premium-only areas
	local leaderstats = player:FindFirstChild("leaderstats")
	if leaderstats then
		local coins = leaderstats:FindFirstChild("Coins")
		if coins then
			coins.Value += 50 -- daily Premium login bonus
		end
	end
end

local function revokePremiumPerks(player: Player)
	player:SetAttribute("IsPremium", false)
end

local function onPlayerAdded(player: Player)
	-- Check on join
	if player.MembershipType == Enum.MembershipType.Premium then
		grantPremiumPerks(player)
	end

	-- Real-time detection: player may subscribe or unsubscribe mid-session
	player:GetPropertyChangedSignal("MembershipType"):Connect(function()
		if player.MembershipType == Enum.MembershipType.Premium then
			grantPremiumPerks(player)
		else
			revokePremiumPerks(player)
		end
	end)
end

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
luau
-- ServerScriptService/PremiumService.lua
local Players = game:GetService("Players")

local function grantPremiumPerks(player: Player)
	player:SetAttribute("IsPremium", true)

	-- 示例特权,激励Premium玩家游玩:
	-- 额外每日奖励、专属外观、额外经验值、Premium专属区域
	local leaderstats = player:FindFirstChild("leaderstats")
	if leaderstats then
		local coins = leaderstats:FindFirstChild("Coins")
		if coins then
			coins.Value += 50 -- 每日Premium登录奖励
		end
	end
end

local function revokePremiumPerks(player: Player)
	player:SetAttribute("IsPremium", false)
end

local function onPlayerAdded(player: Player)
	-- 加入时检查
	if player.MembershipType == Enum.MembershipType.Premium then
		grantPremiumPerks(player)
	end

	-- 实时检测:玩家可能在游戏中订阅或取消订阅
	player:GetPropertyChangedSignal("MembershipType"):Connect(function()
		if player.MembershipType == Enum.MembershipType.Premium then
			grantPremiumPerks(player)
		else
			revokePremiumPerks(player)
		end
	end)
end

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Premium Upsell

Premium订阅推广

You can prompt non-Premium players to subscribe:
luau
-- Client-side: prompt a Premium subscription upsell
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer

if player.MembershipType ~= Enum.MembershipType.Premium then
	MarketplaceService:PromptPremiumPurchase(player)
end

-- Listen for result
MarketplaceService.PromptPremiumPurchaseFinished:Connect(function()
	-- MembershipType will update automatically on the server if they subscribed
end)

你可以向非Premium玩家发起订阅提示:
luau
-- 客户端:发起Premium订阅推广
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer

if player.MembershipType ~= Enum.MembershipType.Premium then
	MarketplaceService:PromptPremiumPurchase(player)
end

-- 监听结果
MarketplaceService.PromptPremiumPurchaseFinished:Connect(function()
	-- 如果玩家订阅,服务器端会自动更新MembershipType
end)

5. Rewarded Video Ads

5. 激励视频广告

Players opt-in to watching a short video ad in exchange for an in-game reward. Revenue per completed view. API is via
AdService
- use mcp-roblox-docs for current method signatures (API has changed during beta).
玩家选择观看一段短视频广告,以换取游戏内奖励。根据完成的广告观看次数赚取收益。API通过
AdService
提供 - 请查阅mcp-roblox-docs获取当前方法签名(测试版期间API已变更)。

Placement Best Practices

放置最佳实践

  • Between rounds - natural break, player is already waiting
  • In lobby / waiting area - low-stakes moment, nothing else to do
  • After death (optional revive) - high motivation, clear value proposition
  • Daily bonus multiplier - "Watch ad to double your daily reward"
Avoid: mid-gameplay interruptions, mandatory ads, ads that block progression.
  • 回合之间 - 自然休息时间,玩家已处于等待状态
  • 大厅/等待区域 - 低风险时刻,玩家暂无其他操作
  • 死亡后(可选复活) - 动机强烈,价值主张明确
  • 每日奖励翻倍 - "观看广告以翻倍每日奖励"
**避免:**游戏中途打断、强制广告、阻碍进度的广告。

Reward Value

奖励价值

  • Target 3-10 Robux equivalent value per completed view
  • Too low: players won't bother. Too high: undermines paid products.
  • Implement a server-side cooldown (5+ minutes) to prevent spam

  • 目标为每次完成观看提供3-10 Robux等价价值
  • 价值过低:玩家不愿参与。价值过高:削弱付费产品的吸引力。
  • 实现服务器端冷却(5分钟以上)以防止滥用

6. Subscriptions

6. 订阅服务

Subscriptions provide recurring monthly revenue. Players pay a monthly Robux fee and receive ongoing benefits. This creates predictable income and higher lifetime value per player.
订阅服务提供月度 recurring 收入。玩家支付月度Robux费用,持续获得福利。这能创造可预测的收入,并提高玩家的终身价值。

Core API

核心API

Method / EventPurpose
MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)
Show the subscription purchase prompt
MarketplaceService.PromptSubscriptionPurchaseFinished
Fires when the subscription purchase prompt closes (does NOT confirm purchase - use UserHasSubscriptionAsync to verify)
MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId)
Get subscription tier details (price, name, description)
MarketplaceService:UserHasSubscriptionAsync(userId, subscriptionId)
Check if a player has an active subscription
方法/事件用途
MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)
显示订阅购买提示
MarketplaceService.PromptSubscriptionPurchaseFinished
订阅购买提示关闭时触发(不确认购买成功 - 使用UserHasSubscriptionAsync验证)
MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId)
获取订阅层级详情(价格、名称、描述)
MarketplaceService:UserHasSubscriptionAsync(userId, subscriptionId)
检查玩家是否有活跃订阅

Subscription Configuration

订阅配置

Subscriptions are configured in the Creator Dashboard > Monetization > Subscriptions. Each subscription has:
  • Name - Displayed to the player
  • Description - What benefits they receive
  • Price - Monthly Robux cost (25 R$ minimum)
  • Benefits - Defined by your game; granted server-side
订阅服务在创作者后台 > 变现 > 订阅中配置。每个订阅包含:
  • 名称 - 显示给玩家
  • 描述 - 玩家可获得的福利
  • 价格 - 月度Robux费用(最低25 R$)
  • 福利 - 由你的游戏定义,在服务器端发放

Implementation (Server Script)

实现(服务器脚本)

luau
-- ServerScriptService/SubscriptionService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local SUBSCRIPTIONS = {
	["premium_monthly"] = {
		id = 123456789,
		name = "Premium Monthly",
		grant = function(player: Player)
			player:SetAttribute("Subscriber", true)
			player:SetAttribute("MonthlyBonus", 500)
		end,
		revoke = function(player: Player)
			player:SetAttribute("Subscriber", false)
			player:SetAttribute("MonthlyBonus", 0)
		end,
	},
}

-- Grant on join if subscription is active
local function onPlayerAdded(player: Player)
	for key, sub in SUBSCRIPTIONS do
		local success, hasSub = pcall(function()
			return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
		end)
		if success and hasSub then
			sub.grant(player)
		end
	end
end

-- Handle prompt close (NOTE: this does NOT confirm purchase succeeded)
-- Use UserHasSubscriptionAsync to verify actual subscription status
MarketplaceService.PromptSubscriptionPurchaseFinished:Connect(function(player: Player, subscriptionId: string, didTryPurchasing: boolean)
	if not didTryPurchasing then return end
	-- Player attempted purchase - verify it actually went through
	for key, sub in SUBSCRIPTIONS do
		if sub.id == subscriptionId then
			local success, hasSub = pcall(function()
				return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
			end)
			if success and hasSub then
				sub.grant(player)
			end
			break
		end
	end
end)

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
luau
-- ServerScriptService/SubscriptionService.lua
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local SUBSCRIPTIONS = {
	["premium_monthly"] = {
		id = 123456789,
		name = "月度Premium",
		grant = function(player: Player)
			player:SetAttribute("Subscriber", true)
			player:SetAttribute("MonthlyBonus", 500)
		end,
		revoke = function(player: Player)
			player:SetAttribute("Subscriber", false)
			player:SetAttribute("MonthlyBonus", 0)
		end,
	},
}

-- 玩家加入时如果订阅活跃则发放福利
local function onPlayerAdded(player: Player)
	for key, sub in SUBSCRIPTIONS do
		local success, hasSub = pcall(function()
			return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
		end)
		if success and hasSub then
			sub.grant(player)
		end
	end
end

-- 处理提示关闭(注意:这不代表购买成功)
-- 使用UserHasSubscriptionAsync验证实际订阅状态
MarketplaceService.PromptSubscriptionPurchaseFinished:Connect(function(player: Player, subscriptionId: string, didTryPurchasing: boolean)
	if not didTryPurchasing then return end
	-- 玩家尝试购买 - 验证是否成功
	for key, sub in SUBSCRIPTIONS do
		if sub.id == subscriptionId then
			local success, hasSub = pcall(function()
				return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
			end)
			if success and hasSub then
				sub.grant(player)
			end
			break
		end
	end
end)

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Subscription Design Best Practices

订阅设计最佳实践

  • Tiered value: Offer 2-3 tiers (Bronze/Silver/Gold or Basic/Pro/Ultimate) at increasing prices
  • Clear benefits: List exact benefits in the subscription description. "2x coins" is better than "exclusive rewards"
  • Recurring currency: Give a daily or monthly currency stipend that incentivizes logging in
  • Exclusive content: Cosmetics, titles, frames, and emotes that are permanently unlocked for subscribers
  • Non-disruptive: Free players should still enjoy the full game loop. Subscribers get bonuses, not exclusive gameplay
  • Cancellation: Use
    UserHasSubscriptionAsync
    on player join to detect lapsed subscriptions and revoke benefits. The prompt event only fires when the UI closes, not on actual cancellation.

  • **分层价值:**提供2-3个层级(青铜/白银/黄金或基础/专业/终极),价格递增
  • **明确福利:**在订阅描述中列出确切福利。"2倍金币"比"专属奖励"更清晰
  • **定期货币:**每日或每月发放货币津贴,激励玩家登录
  • **专属内容:**订阅者可永久解锁的外观、头衔、边框和表情
  • **无干扰:**免费玩家仍可享受完整游戏流程。订阅者获得额外奖励,而非专属游戏内容
  • **取消订阅处理:**玩家加入时使用UserHasSubscriptionAsync检测过期订阅并收回福利。提示事件仅在UI关闭时触发,不检测实际取消操作。

7. Private Servers

7. 私人服务器

Private servers let players pay a monthly Robux fee for a dedicated server instance they control. Players can invite friends, play in private, host events, or farm resources without interference.
私人服务器允许玩家支付月度Robux费用,获得一个由他们控制的专属服务器实例。玩家可以邀请朋友、私密游玩、举办活动或不受干扰地收集资源。

Core API

核心API

Method / EventPurpose
MarketplaceService:PromptCreatePrivateServer(player, placeId)
Show the create/purchase prompt for a new private server
MarketplaceService:PromptPurchasePrivateServer(player, privateServerId)
Show the renewal prompt for an existing private server
MarketplaceService.PrivateServerPurchaseFinished
Fires when a private server is purchased or renewed
方法/事件用途
MarketplaceService:PromptCreatePrivateServer(player, placeId)
显示创建/购买新私人服务器的提示
MarketplaceService:PromptPurchasePrivateServer(player, privateServerId)
显示现有私人服务器的续费提示
MarketplaceService.PrivateServerPurchaseFinished
私人服务器购买或续费时触发

Setup

设置步骤

  1. Navigate to your experience in Creator Dashboard
  2. Go to Monetization > Private Servers
  3. Set the monthly price in Robux (min 10 R$, can change every 60 days)
  4. Configure any server-specific settings
  1. 在创作者后台中导航到你的游戏
  2. 进入变现 > 私人服务器
  3. 设置月度Robux价格(最低10 R$,每60天可更改一次)
  4. 配置任何服务器特定设置

Notes

注意事项

  • Price changes are limited to once every 60 days. Plan pricing carefully.
  • Revenue: You earn 50% of the subscription fee (Roblox takes the other 50%).
  • Permissions: Private server owners can configure who can join via the server's settings page.
  • VipServer: The legacy
    VipServer
    API is deprecated. Use the new Private Server APIs.
  • 价格变更限制为每60天一次。请谨慎规划定价。
  • **收益:**你获得订阅费用的50%(Roblox收取另外50%)。
  • **权限:**私人服务器所有者可通过服务器设置页面配置可加入的用户。
  • **VipServer:**旧版
    VipServer
    API已弃用。使用新的私人服务器API。

Common Use Cases

常见使用场景

  • Competitive practice: Teams/guilds rent a server to practice strategies
  • Roleplay communities: Persistent worlds for friend groups
  • Resource farming: Dedicated server for grinding without competition
  • Content creators: Record/stream without interference from other players
  • Classes/events: Educators or event hosts run private sessions

  • **竞技练习:**团队/公会租用服务器练习策略
  • **角色扮演社区:**朋友群组的持久世界
  • **资源收集:**专属服务器,无竞争地刷资源
  • **内容创作者:**录制/直播时不受其他玩家干扰
  • **课程/活动:**教育者或活动主办方运行私密会话

8. Paid Access (Entry Fee)

8. 付费准入(入场费)

Paid access charges a one-time fee - in Robux or local currency - for entry to your experience. Commonly used for closed betas, premium experiences, or content packs.
付费准入收取一次性费用 - Robux或本地货币 - 以进入你的游戏。常用于封闭测试、高级游戏或内容包。

Core API

核心API

Method / EventPurpose
MarketplaceService:PromptPurchaseExperience(player)
Prompt the player to purchase access
MarketplaceService.PromptPurchaseExperienceFinished
Fires when the prompt closes
MarketplaceService:UserOwnsGamePassAsync
Check if the player has purchased access (uses a hidden GamePass)
方法/事件用途
MarketplaceService:PromptPurchaseExperience(player)
提示玩家购买准入权限
MarketplaceService.PromptPurchaseExperienceFinished
提示关闭时触发
MarketplaceService:UserOwnsGamePassAsync
检查玩家是否已购买准入权限(使用隐藏的GamePass)

Implementation

实现

luau
-- Server: Check access on join
local MarketplaceService = game:GetService("MarketplaceService")

-- Roblox assigns a hidden GamePass ID when you enable paid access
-- Check it with UserOwnsGamePassAsync on PlayerAdded
local ACCESS_PASS_ID = 123456789  -- Replace with your experience's ID

local function onPlayerAdded(player: Player)
	local success, hasAccess = pcall(function()
		return MarketplaceService:UserOwnsGamePassAsync(player.UserId, ACCESS_PASS_ID)
	end)

	if success and hasAccess then
		-- Player has purchased access, let them in
	else
		-- Player has not purchased access
		-- Teleport them to the purchase experience or show a purchase prompt
	end
end
luau
-- Client: Prompt purchase
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local function promptPurchase()
	MarketplaceService:PromptPurchaseExperience(Players.LocalPlayer)
end

MarketplaceService.PromptPurchaseExperienceFinished:Connect(function(player: Player, wasPurchased: boolean)
	if wasPurchased then
		-- Player purchased access, teleport to the main experience
	end
end)
luau
-- 服务器:玩家加入时检查准入权限
local MarketplaceService = game:GetService("MarketplaceService")

-- 启用付费准入时,Roblox会分配一个隐藏的GamePass ID
-- 在PlayerAdded时使用UserOwnsGamePassAsync检查
local ACCESS_PASS_ID = 123456789  -- 替换为你的游戏ID

local function onPlayerAdded(player: Player)
	local success, hasAccess = pcall(function()
		return MarketplaceService:UserOwnsGamePassAsync(player.UserId, ACCESS_PASS_ID)
	end)

	if success and hasAccess then
		-- 玩家已购买准入权限,允许进入
	else
		-- 玩家未购买准入权限
		-- 将他们传送到购买页面或显示购买提示
	end
end
luau
-- 客户端:触发购买
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local function promptPurchase()
	MarketplaceService:PromptPurchaseExperience(Players.LocalPlayer)
end

MarketplaceService.PromptPurchaseExperienceFinished:Connect(function(player: Player, wasPurchased: boolean)
	if wasPurchased then
		-- 玩家已购买准入权限,传送到主游戏
	end
end)

Types

类型

TypePriced InPayout
RobuxRobux (one-time)Standard Robux payout
Local CurrencyUser's local currency (fallback USD)USD payout
类型计价方式收益结算
RobuxRobux(一次性)标准Robux结算
本地货币用户本地货币( fallback为USD)USD结算

Use Cases

使用场景

  • Closed beta: Let most engaged users test early
  • Standalone experiences: One-time purchase games (premium content packs)
  • Ticket/event access: Temporary access for limited-time events

  • **封闭测试:**让最活跃的用户提前测试
  • **独立游戏:**一次性购买的游戏(高级内容包)
  • **门票/活动准入:**限时活动的临时准入权限

9. Immersive Ads

9. 沉浸式广告

Immersive ads allow Roblox to serve advertiser content inside your experience. You earn revenue from ad views. Separate from Rewarded Video Ads (which are player-initiated opt-in).
沉浸式广告允许Roblox在你的游戏内投放广告主内容。你通过广告浏览次数赚取收益。与激励视频广告分离(激励视频广告由玩家主动选择观看)。

Ad Formats

广告格式

FormatDescriptionPlacement
Image AdStatic image displayed on an AdPanel or AdPortalOn a surface, billboard, or screen in your experience
Portal AdInteractive portal that teleports to another experienceGround-level portal the player can walk through
Video AdVideo player ad unitOn a screen or surface
Branded AdCustom branded content integrated into the experienceSponsored items, branded environments
格式描述放置位置
图片广告在AdPanel或AdPortal上显示的静态图片游戏内的表面、广告牌或屏幕上
传送门广告可交互的传送门,将玩家传送到另一个游戏玩家可以走过的地面传送门
视频广告视频播放器广告单元屏幕或表面上
品牌广告集成到游戏中的自定义品牌内容赞助物品、品牌环境

Core API

核心API

APIPurpose
AdService
Service for managing ad units
AdPortal
Instance class for portal ad units
AdGui
Instance class for image/video ad units placed in 3D space
API用途
AdService
管理广告单元的服务
AdPortal
传送门广告单元的实例类
AdGui
放置在3D空间中的图片/视频广告单元的实例类

Placement Best Practices

放置最佳实践

  • Natural integration: Place ads where real-world billboards or screens would exist (stadium walls, shop windows, city buildings)
  • Non-intrusive: Ads should not block gameplay, navigation, or UI
  • Contextual: An ad for a racing game fits on a billboard in your racing game's loading area
  • No interaction required: Players should not be required to watch or interact with ads to progress
  • Respect PolicyService: Check
    PolicyService:GetPolicyInfoForPlayerAsync()
    to ensure ads are shown only to eligible users (age/region restrictions)

  • **自然集成:**将广告放置在现实世界中会有广告牌或屏幕的位置(体育场墙壁、商店橱窗、城市建筑)
  • **无干扰:**广告不应阻碍游戏玩法、导航或UI
  • **上下文匹配:**赛车游戏的广告牌适合投放赛车游戏的广告
  • **无需强制交互:**玩家无需观看或交互广告即可推进游戏
  • **遵守PolicyService:**使用
    PolicyService:GetPolicyInfoForPlayerAsync()
    确保仅向符合条件的用户显示广告(年龄/地区限制)

10. Commerce Products and Creator Store

10. 商业产品与Creator Store

Commerce Products

商业产品

Commerce Products allow you to sell physical goods (merchandise) through Roblox. Configured in the Creator Dashboard under Monetization > Commerce Products.
  • Requires seller onboarding and eligibility verification
  • Products are synced to Roblox for purchase
  • Supports fulfillment tracking
商业产品允许你通过Roblox销售实体商品(周边)。在创作者后台的变现 > 商业产品中配置。
  • 需要卖家入驻和资格验证
  • 产品同步到Roblox供购买
  • 支持履约跟踪

Creator Store (Plugins and Models)

Creator Store(插件与模型)

Sell development assets to other creators:
Asset TypeMinimum PriceRevenue Share
Plugin$4.99 USDTaxes and payment processing fees only
Model$2.99 USDTaxes and payment processing fees only
Escrow hold: Roblox holds your share of each sale for 30 days from the date of purchase.
向其他创作者销售开发资产:
资产类型最低价格收益分成
插件$4.99 USD仅扣除税费和支付处理费
模型$2.99 USD仅扣除税费和支付处理费
托管期:Roblox会将你每笔销售的收益保留30天(从购买日期起)。

Marketplace (Catalog) Commissions

市场(目录)佣金

When users purchase your catalog items (accessories, clothes) within your experience via the avatar inspect menu or avatar editor service, you earn a commission on each sale.

当用户在你的游戏内通过头像检查菜单或头像编辑器服务购买你的目录物品(配饰、服装)时,你将获得每笔销售的佣金。

11. PolicyService Compliance

11. PolicyService合规性

Roblox requires you to use
PolicyService
to restrict certain monetization features based on the player's age, location, and platform.
Roblox要求你使用
PolicyService
根据玩家的年龄、位置和平台限制某些变现功能。

When to Check PolicyService

何时检查PolicyService

  • Subscriptions / Commerce Products: Only show purchase options to eligible users
  • Paid random items (loot boxes, gacha): Must block for users in restricted regions
  • Immersive ads: Only show ad units to eligible users
  • Paid item trading: Must check eligibility
  • **订阅/商业产品:**仅向符合条件的用户显示购买选项
  • **付费随机物品(战利品箱、扭蛋):**必须对限制地区的用户屏蔽
  • **沉浸式广告:**仅向符合条件的用户显示广告单元
  • **付费物品交易:**必须检查资格

Implementation

实现

luau
-- ServerScriptService/PolicyServiceCheck.lua
local PolicyService = game:GetService("PolicyService")
local Players = game:GetService("Players")

local function isEligibleForRandomItems(player: Player): boolean
	local success, policyInfo = pcall(function()
		return PolicyService:GetPolicyInfoForPlayerAsync(player)
	end)

	if not success then
		-- On failure, default to restricting the feature
		return false
	end

	return policyInfo.IsPriceFixEnabled  -- Example: check relevant policy flag
end

-- Usage: hide loot boxes if the player is not eligible
local function updateShopUI(player: Player)
	if isEligibleForRandomItems(player) then
		-- Show loot boxes in the shop
	else
		-- Hide loot boxes or show a "not available in your region" message
	end
end

Players.PlayerAdded:Connect(function(player: Player)
	player:GetPropertyChangedSignal("MembershipType"):Connect(function()
		updateShopUI(player)
	end)
	updateShopUI(player)
end)
luau
-- ServerScriptService/PolicyServiceCheck.lua
local PolicyService = game:GetService("PolicyService")
local Players = game:GetService("Players")

local function isEligibleForRandomItems(player: Player): boolean
	local success, policyInfo = pcall(function()
		return PolicyService:GetPolicyInfoForPlayerAsync(player)
	end)

	if not success then
		-- 失败时,默认限制该功能
		return false
	end

	return policyInfo.IsPriceFixEnabled  -- 示例:检查相关政策标志
end

-- 用法:如果玩家不符合条件,隐藏战利品箱
local function updateShopUI(player: Player)
	if isEligibleForRandomItems(player) then
		-- 在商店中显示战利品箱
	else
		-- 隐藏战利品箱或显示"你的地区不可用"消息
	end
end

Players.PlayerAdded:Connect(function(player: Player)
	player:GetPropertyChangedSignal("MembershipType"):Connect(function()
		updateShopUI(player)
	end)
	updateShopUI(player)
end)

Recommended Approach

推荐方法

  • Fail closed: If
    PolicyService:GetPolicyInfoForPlayerAsync()
    errors, default to restricting the feature
  • Cache results per-player to avoid repeated API calls
  • Re-check on locale change if you support in-session region switching

  • **默认关闭:**如果
    PolicyService:GetPolicyInfoForPlayerAsync()
    出错,默认限制该功能
  • 按玩家缓存结果以避免重复API调用
  • 地区变更时重新检查如果你的游戏支持会话内地区切换

12. Pricing Strategy

12. 定价策略

RobuxTypical Use
25Minimum viable price. Small cosmetic, single-use consumable
50Minor cosmetic pack, small currency bundle
75Mid-tier consumable, trail effect, small pet
100Standard GamePass, decent currency pack
250Premium GamePass (2x coins, VIP), mid currency bundle
500Major GamePass (significant gameplay advantage), large currency pack
1,000Top-tier GamePass, mega currency bundle
2,500+Whale-tier only. Use sparingly
Robux数量典型用途
25最低可行价格。小型外观、单次消耗品
50小型外观包、小额货币包
75中端消耗品、轨迹特效、小型宠物
100标准GamePass、中等货币包
250高级GamePass(2倍金币、VIP)、中端货币包
500大型GamePass(显著游戏优势)、大额货币包
1,000顶级GamePass、超大货币包
2,500+仅针对高消费玩家。谨慎使用

Pricing Tactics

定价策略

Anchoring: Show the most expensive option first in the shop UI. When a player sees "Mega Pack: 1,000 Robux" first, the "Starter Pack: 100 Robux" feels like a bargain by comparison.
Bundle Value: Offer multi-item bundles at a per-unit discount:
  • 100 Coins = 50 Robux (0.50 Robux/coin)
  • 300 Coins = 100 Robux (0.33 Robux/coin) -- "Best Value" tag
  • 1,000 Coins = 250 Robux (0.25 Robux/coin) -- "Most Popular" tag
Minimum Price Floor: Do not price anything below 25 Robux. Roblox takes a 30% marketplace fee, and extremely low-priced items generate negligible revenue while still requiring full implementation and support effort.
Odd Pricing: 49 Robux feels cheaper than 50 Robux. 99 feels cheaper than 100. Roblox players respond to this the same way real-world consumers do.
Limited-Time Offers: Create urgency with rotating shop items or seasonal GamePasses. Fear of missing out (FOMO) drives conversions, but use ethically (see Section 8).

**锚定效应:**在商店UI中先显示最贵的选项。当玩家先看到"超级包:1000 Robux","入门包:100 Robux"就会显得很划算。
**捆绑价值:**提供多物品捆绑包,单位价格更低:
  • 100金币 = 50 Robux(0.50 Robux/金币)
  • 300金币 = 100 Robux(0.33 Robux/金币) -- 标注"最佳性价比"
  • 1000金币 = 250 Robux(0.25 Robux/金币) -- 标注"最受欢迎"
最低价格底线:不要将任何物品定价低于25 Robux。Roblox收取30%的市场费用,极低价格的物品产生的收益可忽略不计,却仍需完整的实现和支持工作。
**奇数定价:**49 Robux感觉比50 Robux便宜。99比100便宜。Roblox玩家和现实消费者的反应一致。
**限时优惠:**通过轮换商店物品或季节性GamePass创造紧迫感。害怕错过(FOMO)能提升转化率,但需合理使用(见第8节)。

13. DevEx Math

13. DevEx计算

Dual Exchange Rate System

双汇率系统

As of September 5, 2025, Roblox operates a dual-rate DevEx system based on when Robux was earned:
RateValueApplies To
New Rate$0.0038/R$Robux earned on or after 10 AM PT on September 5, 2025
Old Rate$0.0035/R$Robux earned before 10 AM PT on September 5, 2025
自2025年9月5日起,Roblox根据Robux赚取时间实行双汇率DevEx系统:
汇率价值适用范围
新汇率$0.0038/R$2025年9月5日太平洋时间上午10点及之后赚取的Robux
旧汇率$0.0035/R$2025年9月5日太平洋时间上午10点之前赚取的Robux

Cash-Out Ordering Rules

提现顺序规则

  • Must clear Old Rate first: You must cash out all Old Rate Robux before you can cash out any New Rate Robux.
  • Spending does not help: Spending Robux on the platform (items, experiences, etc.) does not reduce your Old Rate balance. Spending is deducted from your total balance but does not count toward clearing Old Rate first.
  • Group funds: If you receive payment from a Group that earned Robux before the cutoff, those Robux also cash out at the Old Rate. Your Old Rate balance may increase from Group payouts.
  • 必须先清空旧汇率余额:你必须先提现所有旧汇率Robux,才能提现新汇率Robux。
  • **消费不帮助清空旧余额:**在平台上消费Robux(物品、游戏等)不会减少你的旧汇率余额。消费从总余额中扣除,但不算作清空旧汇率余额的部分。
  • **群组资金:**如果你从在截止日期前赚取Robux的群组获得付款,这些Robux也按旧汇率提现。你的旧汇率余额可能会因群组付款而增加。

Example Conversion

转换示例

Balance TypeAmountRateUSD Value
Old Rate30,000 R$$0.0035$105
New Rate30,000 R$$0.0038$114
Mixed (clear Old Rate first)30,000 Old + 30,000 NewDual$105 + $114 = $219
余额类型数量汇率USD价值
旧汇率30,000 R$$0.0035$105
新汇率30,000 R$$0.0038$114
混合(先清空旧汇率)30,000旧 + 30,000新双汇率$105 + $114 = $219

Minimum Cashout

最低提现额度

  • 30,000 Robux minimum per cash-out request.
  • Funds are reviewed on a per-request basis. First-time cashouts require creating a DevEx portal account via email invite.
  • Eligibility requirements and service requirements are defined in the DevEx Terms of Use.
Upcoming (June 2026): US creators 18+ will get a higher rate of ~$0.0054/Robux (42% increase). Requires identity verification.
  • 每次提现请求最低30,000 Robux
  • 资金按请求逐一审核。首次提现需通过邮件邀请创建DevEx门户账户。
  • 资格要求和服务要求在DevEx服务条款中定义。
**即将推出(2026年6月):**18岁以上的美国创作者将获得更高的汇率~$0.0054/Robux(增长42%)。需要身份验证。

Revenue Projection Formulas

收益预估公式

Daily Revenue (Robux) = DAU x Conversion Rate x Average Purchase (Robux)

Monthly Revenue (Robux) = Daily Revenue x 30

Monthly Revenue (USD) = Monthly Revenue (Robux) x 0.0038
Example projections at different scales (New Rate):
DAUConversion RateAvg PurchaseDaily RobuxMonthly USD
1002%100 R$200$23
1,0002%100 R$2,000$228
10,0003%150 R$45,000$5,130
100,0003%150 R$450,000$51,300
Important: If your revenue includes Old Rate Robux, the USD value will be lower until the Old Rate balance is cleared. For mixed balances, calculate separately and sum.
Typical conversion rates on Roblox: 1-5% of DAU makes a purchase on any given day. Well-optimized games with strong shop design reach the higher end.
Premium Payout addition: Premium Payouts add roughly 10-30% on top of direct purchase revenue depending on your Premium player ratio and engagement quality.
每日收益(Robux)= 日活跃用户数 x 转化率 x 平均购买额(Robux)

月度收益(Robux)= 每日收益 x 30

月度收益(USD)= 月度收益(Robux) x 0.0038
不同规模的预估示例(新汇率):
日活跃用户数转化率平均购买额每日Robux月度USD
1002%100 R$200$23
1,0002%100 R$2,000$228
10,0003%150 R$45,000$5,130
100,0003%150 R$450,000$51,300
**重要提示:**如果你的收益包含旧汇率Robux,在旧汇率余额清空前,USD价值会更低。对于混合余额,需分别计算后求和。
**Roblox上的典型转化率:**1-5%的日活跃用户会在任意一天进行购买。商店设计优化良好的游戏能达到较高水平。
**Premium收益分成补充:**根据你的Premium玩家比例和参与质量,Premium收益分成大约会在直接购买收益基础上增加10-30%。

Break-Even Calculations

收支平衡计算

Hours spent developing = X
Hourly rate target = $Y/hr
Required total earnings = X * Y
Required Robux = (X * Y) / 0.0038
Required paying players = Required Robux / Average Purchase

开发时长 = X
目标时薪 = $Y/小时
所需总收益 = X * Y
所需Robux = (X * Y) / 0.0038
所需付费玩家数 = 所需Robux / 平均购买额

14. Roblox TOS Compliance (MANDATORY)

14. Roblox服务条款合规性(强制要求)

These are not suggestions. Violating them gets your game taken down.
这些不是建议。违反这些规则会导致你的游戏被下架。

Odds Disclosure (enforced, games get removed)

概率披露(强制执行,违反会下架游戏)

Any item sold with a randomized element MUST display the exact drop chance percentages in-game. This applies to:
  • Loot boxes / mystery boxes
  • Random pet hatching
  • Gacha pulls
  • Any "chance" mechanic tied to a purchase
If a pet has a 0.1% drop rate, the player must see "0.1%" before they buy. Not "rare," not "legendary," not a color code. The exact number.
Games have been taken down for violating this. Roblox enforces it.
luau
-- Example: display odds on a loot box GUI
local oddsLabel = script.Parent.OddsLabel
oddsLabel.Text = "Drop rates: Common 60% | Uncommon 25% | Rare 10% | Epic 4% | Legendary 1%"
**任何带有随机元素的出售物品必须在游戏内显示精确的掉落概率百分比。**这适用于:
  • 战利品箱/神秘箱
  • 随机宠物孵化
  • 扭蛋抽取
  • 任何与购买绑定的"概率"机制
如果一个宠物的掉落率是0.1%,玩家在购买前必须看到"0.1%"。不能是"稀有"、"传奇"或颜色代码。必须是精确数字。
已有游戏因违反此规则被下架。Roblox会强制执行。
luau
-- 示例:在战利品箱GUI上显示概率
local oddsLabel = script.Parent.OddsLabel
oddsLabel.Text = "掉落概率:普通60% | 罕见25% | 稀有10% | 史诗4% | 传奇1%"

Presenting Products (Guidelines)

产品展示指南

Roblox requires monetization products to be presented in a way that is transparent, honest, and user-friendly:
Discounts must be genuine and fair:
  • A discount is not genuine if an item is always "on sale" for the same amount.
  • A discount is not fair if it's only offered for a very short time, pressuring users.
No misleading urgency:
  • Don't claim an item is almost out of stock or only available for a short time if it isn't true.
  • Don't use a countdown timer that isn't accurate or automatically restarts.
Language recommendations:
AvoidUse Instead
"GET IT NOW""View Item"
"LAST CHANCE, ACT NOW""See Price"
"BUY BEFORE IT'S GONE!""Open Shop"
Roblox要求变现产品的展示方式必须透明、诚实且用户友好
折扣必须真实且公平:
  • 如果物品始终以相同价格"促销",则该折扣不真实。
  • 如果折扣仅在极短时间内提供,给用户造成压力,则该折扣不公平。
禁止误导性紧迫感:
  • 不要声称物品即将售罄或仅在短时间内可用,除非情况属实。
  • 不要使用不准确或自动重启的倒计时计时器。
语言建议:
避免使用建议使用
"立即获取""查看物品"
"最后机会,立即行动""查看价格"
"购买以免错过!""打开商店"

Other TOS Rules That Affect Monetization

其他影响变现的服务条款规则

  • No gambling mechanics. Do not implement anything that resembles gambling (betting Robux, coin flips, roulette). Roblox bans these.
  • No off-platform sales. Do not direct players to buy Robux or items outside of Roblox's systems.
  • No misleading product descriptions. GamePass and DevProduct descriptions must exactly match what the player receives.
  • No purchased advantages in experiences marked as "All Ages." Stricter rules apply for experiences targeting younger audiences.
  • Refund policy. If a player reports not receiving an item, investigate and honor legitimate claims. Roblox can reverse charges.
  • PolicyService integration required. Use
    PolicyService:GetPolicyInfoForPlayerAsync()
    to restrict subscriptions, commerce products, paid random items, and immersive ads based on user eligibility.
Recommendation: Download and read the full Roblox Community Standards and Terms of Use. Feed them to the AI as context when working on monetization features.

  • **禁止赌博机制。**不要实现任何类似赌博的内容(Robux下注、抛硬币、轮盘赌)。Roblox会封禁这些内容。
  • **禁止平台外销售。**不要引导玩家在Roblox系统外购买Robux或物品。
  • **禁止误导性产品描述。**GamePass和DevProduct的描述必须与玩家实际获得的内容完全一致。
  • **标记为"全年龄"的游戏中禁止付费优势。**针对年轻受众的游戏有更严格的规则。
  • **退款政策。**如果玩家报告未收到物品,需调查并处理合理的退款请求。Roblox可撤销收费。
  • **必须集成PolicyService。**使用
    PolicyService:GetPolicyInfoForPlayerAsync()
    根据用户资格限制订阅、商业产品、付费随机物品和沉浸式广告。
**建议:**下载并阅读完整的Roblox社区准则服务条款。在开发变现功能时,将这些内容作为上下文提供给AI。

15. Ethical Monetization

15. 伦理变现

Roblox's audience skews young (a significant portion is under 16). This carries a responsibility to monetize fairly. Roblox also actively enforces policies against predatory practices.
Roblox的受众偏向年轻化(很大一部分用户未满16岁)。这意味着你有责任公平变现。Roblox也会积极打击掠夺性做法。

Do

应该做

  • Provide genuine value for every purchase. The player should feel good about what they got.
  • Allow core gameplay for free. Free players should enjoy the full game loop. Purchases should enhance, not gate.
  • Price transparently. Show the Robux cost clearly before any purchase prompt.
  • Offer earnable alternatives. If a cosmetic costs 100 Robux, also let players earn it after 10 hours of gameplay.
  • Respect declining. If a player closes a purchase prompt, do not immediately re-prompt.
  • **为每笔购买提供真实价值。**玩家应该对他们所获得的内容感到满意。
  • **允许免费玩家体验核心玩法。**免费玩家应该享受完整的游戏流程。购买应是增强体验,而非限制核心内容。
  • **定价透明。**在任何购买提示前清晰显示Robux价格。
  • **提供可赚取的替代方案。**如果一个外观售价100 Robux,也允许玩家通过10小时游戏时间赚取。
  • **尊重玩家拒绝。**如果玩家关闭购买提示,不要立即重新提示。

Do Not

不应该做

  • No pay-to-win in competitive modes. If your game has PvP, purchased items should not provide a statistical advantage.
  • No hidden costs. Never require a chain of purchases to unlock something ("buy A to unlock B to unlock C").
  • No artificial scarcity manipulation. "Only 3 left!" when supply is unlimited is deceptive.
  • No pressure tactics on children. Countdown timers, social pressure ("your friend bought this!"), and guilt messaging are inappropriate.
  • No paywalled progression. Never block a player from advancing in the story or level because they have not purchased something.
  • No misleading descriptions. GamePass and product descriptions must accurately reflect what the player receives.

  • **竞技模式中禁止付费获胜。**如果你的游戏有PvP,购买的物品不应提供统计优势。
  • **禁止隐藏成本。**永远不要要求玩家进行一系列购买才能解锁内容("购买A以解锁B,再解锁C")。
  • **禁止人为操纵稀缺性。**当供应无限时,声称"仅剩3个!"是欺骗性的。
  • **禁止对儿童施加压力。**倒计时计时器、社交压力("你的朋友买了这个!")和内疚信息都是不合适的。
  • **禁止付费墙限制进度。**永远不要因为玩家未购买物品而阻止他们推进故事或关卡。
  • **禁止误导性描述。**GamePass和产品描述必须准确反映玩家将获得的内容。

16. Best Practices

16. 最佳实践

Server-Side Purchase Verification (Always)

服务器端购买验证(始终执行)

Never grant items from the client. A client script can prompt a purchase, but the grant must always happen in a ServerScript via
ProcessReceipt
(for products) or
PromptGamePassPurchaseFinished
(for GamePasses, verified with
UserOwnsGamePassAsync
).
绝不要从客户端发放物品。客户端脚本可以触发购买提示,但必须始终在ServerScript中通过
ProcessReceipt
(针对产品)或
PromptGamePassPurchaseFinished
(针对GamePass,通过
UserOwnsGamePassAsync
验证)发放物品。

Graceful Failure Handling

优雅的失败处理

luau
-- Wrap every MarketplaceService call in pcall
local success, result = pcall(function()
	return MarketplaceService:UserOwnsGamePassAsync(player.UserId, passId)
end)

if not success then
	-- API is down or rate-limited. Fail gracefully.
	warn(`[Purchase] API call failed: {result}`)
	-- Do NOT assume they own it; do NOT assume they don't.
	-- Cache the last known state and retry later.
end
luau
-- 将每个MarketplaceService调用包裹在pcall中
local success, result = pcall(function()
	return MarketplaceService:UserOwnsGamePassAsync(player.UserId, passId)
end)

if not success then
	-- API故障或速率限制。优雅处理失败。
	warn(`[Purchase] API调用失败:{result}`)
	-- 不要假设玩家拥有该物品;也不要假设他们不拥有。
	-- 缓存最后已知状态并稍后重试。
end

Receipt Logging

收据日志

Log every purchase for customer support and debugging:
luau
-- Inside ProcessReceipt, after granting
print(`[Receipt] Player={receiptInfo.PlayerId} Product={receiptInfo.ProductId} PurchaseId={receiptInfo.PurchaseId} CurrencySpent={receiptInfo.CurrencySpent} PlaceId={receiptInfo.PlaceIdWherePurchased}`)
Keep a DataStore or external log of all purchases so you can:
  • Investigate "I paid but didn't get my item" support tickets
  • Track conversion metrics
  • Identify unusual purchase patterns (potential fraud or exploits)
记录每笔购买,用于客户支持和调试:
luau
-- 在ProcessReceipt中,发放物品后
print(`[Receipt] 玩家={receiptInfo.PlayerId} 产品={receiptInfo.ProductId} 购买ID={receiptInfo.PurchaseId} 花费货币={receiptInfo.CurrencySpent} 购买地点ID={receiptInfo.PlaceIdWherePurchased}`)
保留所有购买的DataStore或外部日志,以便:
  • 调查"我付了钱但没收到物品"的支持工单
  • 跟踪转化指标
  • 识别异常购买模式(潜在欺诈或漏洞利用)

Test Purchases in Studio

在Studio中测试购买

  • In Roblox Studio,
    ProcessReceipt
    will fire with test data.
  • UserOwnsGamePassAsync
    returns false in Studio for passes the Studio user does not own.
  • Use Studio's "Test" tab to simulate purchases.
  • Always test the full flow: prompt, purchase, grant, rejoin-and-re-grant, and the failure path.
  • 在Roblox Studio中,
    ProcessReceipt
    会使用测试数据触发。
  • UserOwnsGamePassAsync
    在Studio中对Studio用户未拥有的GamePass返回false。
  • 使用Studio的"测试"选项卡模拟购买。
  • 始终测试完整流程:提示、购买、发放、重新加入并重新发放,以及失败路径。

Natural Purchase Prompt Placement

自然的购买提示位置

Good placements:
  • In a dedicated shop GUI the player opens voluntarily
  • Contextually, when the player encounters a locked feature ("This area is VIP-only. Unlock VIP?")
  • After the player has played for several minutes and understands the game's value
Bad placements:
  • Immediately on join before the player has loaded
  • Every 30 seconds via popup
  • Blocking the screen during active gameplay

良好的位置:
  • 在玩家自愿打开的专用商店GUI中
  • 上下文触发,当玩家遇到锁定功能时("此区域仅限VIP。解锁VIP?")
  • 玩家游玩几分钟并理解游戏价值后
糟糕的位置:
  • 玩家加入后立即触发,尚未加载完成
  • 每30秒弹出一次
  • 活跃游戏期间遮挡屏幕

17. Anti-Patterns

17. 反模式

Client-Side Purchase Granting (Exploitable)

客户端购买发放(易被利用)

luau
-- BAD: Never do this
-- LocalScript
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, id, purchased)
	if purchased then
		player.Character.Humanoid.WalkSpeed = 50 -- exploiter can fire this event
	end
end)
Exploiters can fire
RemoteEvent
s and manipulate client-side logic. Always grant on the server.
luau
-- 错误:永远不要这样做
-- LocalScript
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, id, purchased)
	if purchased then
		player.Character.Humanoid.WalkSpeed = 50 -- 攻击者可以触发此事件
	end
end)
攻击者可以触发
RemoteEvent
并操纵客户端逻辑。始终在服务器端发放物品。

Improper ProcessReceipt Handling

不当的ProcessReceipt处理

luau
-- BAD: Returns PurchaseGranted without actually granting
MarketplaceService.ProcessReceipt = function(receiptInfo)
	-- "I'll grant it later"
	return Enum.ProductPurchaseDecision.PurchaseGranted -- Player never gets their item
end
luau
-- BAD: No error handling, can silently fail
MarketplaceService.ProcessReceipt = function(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	player.leaderstats.Coins.Value += 100 -- crashes if player left or leaderstats missing
	return Enum.ProductPurchaseDecision.PurchaseGranted
end
luau
-- BAD: No idempotency check, causes duplicates on retry
MarketplaceService.ProcessReceipt = function(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if player then
		player.leaderstats.Coins.Value += 100
	end
	return Enum.ProductPurchaseDecision.PurchaseGranted -- Roblox won't retry, but if
	-- you returned NotProcessedYet when the player was absent and PurchaseGranted
	-- here, the player gets double coins if there's no receipt dedup.
end
luau
-- 错误:未实际发放就返回PurchaseGranted
MarketplaceService.ProcessReceipt = function(receiptInfo)
	-- "我稍后再发放"
	return Enum.ProductPurchaseDecision.PurchaseGranted -- 玩家永远不会收到物品
end
luau
-- 错误:无错误处理,可能静默失败
MarketplaceService.ProcessReceipt = function(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	player.leaderstats.Coins.Value += 100 -- 如果玩家已离开或leaderstats不存在会崩溃
	return Enum.ProductPurchaseDecision.PurchaseGranted
end
luau
-- 错误:无幂等性检查,重试时会导致重复发放
MarketplaceService.ProcessReceipt = function(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if player then
		player.leaderstats.Coins.Value += 100
	end
	return Enum.ProductPurchaseDecision.PurchaseGranted -- Roblox不会重试,但如果
	-- 玩家不在时返回NotProcessedYet,这里返回PurchaseGranted,且无收据去重,玩家会获得双倍金币
end

Aggressive Popup Spam

激进的弹窗骚扰

Prompting purchases repeatedly annoys players and violates Roblox UX guidelines. A player who closes a prompt does not want to see it again immediately. Implement cooldowns:
luau
-- Minimum 60-second cooldown between prompts of the same type
local lastPromptTime: { [number]: number } = {}

local function safePrompt(player: Player, productId: number)
	local key = player.UserId
	local now = os.time()

	if lastPromptTime[key] and now - lastPromptTime[key] < 60 then
		return -- too soon, skip
	end

	lastPromptTime[key] = now
	MarketplaceService:PromptProductPurchase(player, productId)
end
反复触发购买提示会惹恼玩家,违反Roblox UX指南。关闭提示的玩家不想立即再次看到它。实现冷却机制:
luau
-- 同一类型提示之间至少60秒冷却
local lastPromptTime: { [number]: number } = {}

local function safePrompt(player: Player, productId: number)
	local key = player.UserId
	local now = os.time()

	if lastPromptTime[key] and now - lastPromptTime[key] < 60 then
		return -- 时间太短,跳过
	end

	lastPromptTime[key] = now
	MarketplaceService:PromptProductPurchase(player, productId)
end

Misleading Descriptions

误导性描述

Do not describe a GamePass as "2x Everything" if it only doubles coins and not XP. Do not show a product giving 1,000 coins in the icon but actually grant 100. Roblox can remove misleading assets, and players will leave negative reviews.
不要将GamePass描述为"所有内容翻倍",如果它仅翻倍金币而不翻倍经验值。不要在图标中显示产品提供1000金币,但实际只发放100。Roblox会移除误导性资产,玩家也会留下负面评价。

Hiding Costs

隐藏成本

Never make the total cost of engagement unclear. If your game has a "Battle Pass" that requires buying 10 tiers at 50 Robux each, make the full 500 Robux cost visible upfront rather than drip-feeding 50 Robux prompts.
永远不要让参与成本模糊不清。如果你的游戏有"战斗通行证",需要以50 Robux购买10个等级,请提前显示完整的500 Robux成本,而非逐步触发50 Robux的提示。