roblox-monetization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox monetization

Roblox 变现

Product types

产品类型

TypeBuyUse forGrant via
Developer productRepeatedlyCurrency, consumables, boosts, revives
MarketplaceService.ProcessReceipt
(server)
PassOnce, permanentVIP, permanent perks, extra slots
UserOwnsGamePassAsync
check on join + purchase-finished event
SubscriptionMonthly, revocableRecurring perks (daily currency, VIP)
GetUserSubscriptionStatusAsync
+
UserSubscriptionStatusChanged
Rewarded video adFree (watch ad)Small rewards (≈3–10 Robux value)Developer product granted through
ProcessReceipt
Commerce productReal money (physical goods)Merch with bundled digital benefits
CommerceService
; digital benefits via
ProcessReceipt
Private serverMonthlyPaid private serversConfigured in Creator Dashboard
All granting happens on the server. Never grant from client events like
PromptProductPurchaseFinished
— clients can fake them.
类型购买方式适用于通过以下方式发放
Developer product(开发者产品)可重复购买货币、消耗品、增益、复活
MarketplaceService.ProcessReceipt
(服务器)
Pass(通行证)购买一次,永久有效VIP、永久特权、额外槽位加入时通过
UserOwnsGamePassAsync
检查 + 购买完成事件
Subscription(订阅)按月购买,可取消周期性权益(每日货币、VIP)
GetUserSubscriptionStatusAsync
+
UserSubscriptionStatusChanged
Rewarded video ad(激励视频广告)免费(观看广告)小额奖励(约值 3–10 Robux)通过
ProcessReceipt
发放的 Developer product
Commerce product(商业产品)真实货币(实体商品)捆绑数字权益的周边商品
CommerceService
;数字权益通过
ProcessReceipt
发放
Private server(私人服务器)按月付费付费私人服务器在 Creator Dashboard 中配置
所有发放操作都必须在 服务器 上完成。绝不根据
PromptProductPurchaseFinished
等客户端事件发放权益——客户端可以伪造这些事件。

Developer products: ProcessReceipt

Developer product:ProcessReceipt

Rules:
  • Exactly one server script sets
    MarketplaceService.ProcessReceipt
    .
  • Return
    PurchaseGranted
    only after the grant is persisted. Otherwise return
    NotProcessedYet
    ; Roblox retries later (including on the player's next join) with the same
    PurchaseId
    .
  • Handlers must be idempotent: record handled
    PurchaseId
    s in the player's saved data so a retry never grants twice.
  • Receipts arrive for players who may not be loaded yet or may have left: wait for their data, or return
    NotProcessedYet
    .
luau
--!strict
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

type PlayerData = { coins: number, purchaseIds: { string } }
type ReceiptInfo = {
	PlayerId: number,
	ProductId: number,
	PurchaseId: string,
	CurrencySpent: number,
	CurrencyType: Enum.CurrencyType,
	PlaceIdWherePurchased: number,
}

local MAX_REMEMBERED_PURCHASES = 100

-- Provided by your data layer (ProfileStore profile.Data, etc.).
local function getLoadedData(_player: Player): PlayerData?
	return nil
end
local function saveNow(_player: Player): boolean
	return true
end

local PRODUCTS: { [number]: (Player, PlayerData) -> () } = {
	[1234567] = function(_player, data) -- 100 coins
		data.coins += 100
	end,
}

local function processReceipt(receipt: ReceiptInfo): Enum.ProductPurchaseDecision
	local player = Players:GetPlayerByUserId(receipt.PlayerId)
	if not player then
		return Enum.ProductPurchaseDecision.NotProcessedYet -- granted on their next visit
	end

	local data = getLoadedData(player)
	local deadline = os.clock() + 20
	while not data and player.Parent == Players and os.clock() < deadline do
		task.wait(0.5)
		data = getLoadedData(player)
	end
	if not data then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if table.find(data.purchaseIds, receipt.PurchaseId) then
		return Enum.ProductPurchaseDecision.PurchaseGranted -- already granted earlier
	end

	local handler = PRODUCTS[receipt.ProductId]
	if not handler then
		warn(`No handler for product {receipt.ProductId}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	local ok, err = pcall(handler, player, data)
	if not ok then
		warn(`Grant failed for {receipt.PurchaseId}: {err}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	table.insert(data.purchaseIds, receipt.PurchaseId)
	while #data.purchaseIds > MAX_REMEMBERED_PURCHASES do
		table.remove(data.purchaseIds, 1)
	end

	if not saveNow(player) then
		return Enum.ProductPurchaseDecision.NotProcessedYet -- the idempotency list prevents a double grant
	end
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = processReceipt :: any
With ProfileStore, "persisted" means waiting until the
PurchaseId
appears in
profile.LastSavedData
(or calling
profile:Save()
and waiting); see ProfileStore's developer products guide.
Prompt from the client (UI) — the server still grants:
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)
.
规则:
  • 恰好一个服务器脚本设置
    MarketplaceService.ProcessReceipt
    。
  • 只有在权益发放已经持久化后,才能返回
    PurchaseGranted
    ;否则应返回
    NotProcessedYet
    。Roblox 稍后会重试(包括玩家下次加入时),并使用相同的
    PurchaseId
    。
  • 处理函数必须具备幂等性:将已处理的
    PurchaseId
    记录在玩家存档中,确保重试永远不会重复发放权益。
  • 收据可能对应尚未加载数据或已经离开的玩家:等待其数据加载完成,或返回
    NotProcessedYet
    。
luau
--!strict
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

type PlayerData = { coins: number, purchaseIds: { string } }
type ReceiptInfo = {
	PlayerId: number,
	ProductId: number,
	PurchaseId: string,
	CurrencySpent: number,
	CurrencyType: Enum.CurrencyType,
	PlaceIdWherePurchased: number,
}

local MAX_REMEMBERED_PURCHASES = 100

-- Provided by your data layer (ProfileStore profile.Data, etc.).
local function getLoadedData(_player: Player): PlayerData?
	return nil
end
local function saveNow(_player: Player): boolean
	return true
end

local PRODUCTS: { [number]: (Player, PlayerData) -> () } = {
	[1234567] = function(_player, data) -- 100 coins
		data.coins += 100
	end,
}

local function processReceipt(receipt: ReceiptInfo): Enum.ProductPurchaseDecision
	local player = Players:GetPlayerByUserId(receipt.PlayerId)
	if not player then
		return Enum.ProductPurchaseDecision.NotProcessedYet -- granted on their next visit
	end

	local data = getLoadedData(player)
	local deadline = os.clock() + 20
	while not data and player.Parent == Players and os.clock() < deadline do
		task.wait(0.5)
		data = getLoadedData(player)
	end
	if not data then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	if table.find(data.purchaseIds, receipt.PurchaseId) then
		return Enum.ProductPurchaseDecision.PurchaseGranted -- already granted earlier
	end

	local handler = PRODUCTS[receipt.ProductId]
	if not handler then
		warn(`No handler for product {receipt.ProductId}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	local ok, err = pcall(handler, player, data)
	if not ok then
		warn(`Grant failed for {receipt.PurchaseId}: {err}`)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	table.insert(data.purchaseIds, receipt.PurchaseId)
	while #data.purchaseIds > MAX_REMEMBERED_PURCHASES do
		table.remove(data.purchaseIds, 1)
	end

	if not saveNow(player) then
		return Enum.ProductPurchaseDecision.NotProcessedYet -- the idempotency list prevents a double grant
	end
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = processReceipt :: any
使用 ProfileStore 时,“已持久化”是指等待
PurchaseId
出现在
profile.LastSavedData
中(或调用
profile:Save()
并等待保存完成);参阅 ProfileStore 的开发者产品指南。
客户端购买提示(UI)——服务器仍负责发放权益:
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)
。

Passes

Pass

luau
--!strict
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local VIP_PASS_ID = 987654

local function grantVip(player: Player)
	player:SetAttribute("VIP", true) -- apply perks from one place
end

local function checkPass(player: Player)
	local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, VIP_PASS_ID)
	if ok and owns then
		grantVip(player)
	end
end

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

-- Fires on the server too; re-verify ownership rather than trusting `purchased` alone.
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)
	if purchased and passId == VIP_PASS_ID then
		checkPass(player)
	end
end)
UserOwnsGamePassAsync
results are cached per server; the purchase-finished handler covers in-session buys.
luau
--!strict
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local VIP_PASS_ID = 987654

local function grantVip(player: Player)
	player:SetAttribute("VIP", true) -- apply perks from one place
end

local function checkPass(player: Player)
	local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, VIP_PASS_ID)
	if ok and owns then
		grantVip(player)
	end
end

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

-- Fires on the server too; re-verify ownership rather than trusting `purchased` alone.
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)
	if purchased and passId == VIP_PASS_ID then
		checkPass(player)
	end
end)
UserOwnsGamePassAsync
的查询结果按服务器实例缓存;购买完成处理器用于覆盖当前会话内完成的购买。

Subscriptions

Subscription

  • Check with
    MarketplaceService:GetUserSubscriptionStatusAsync(player, "EXP-...")
    →
    { IsSubscribed, IsRenewing }
    on join and on
    Players.UserSubscriptionStatusChanged(player, subscriptionId)
    .
  • Benefits must be revocable: when
    IsSubscribed
    is false, remove perks you persisted.
  • Prompt with
    MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)
    .
  • Replacing a pass with a subscription: keep honoring existing pass owners, take the pass off sale.
  • 使用
    MarketplaceService:GetUserSubscriptionStatusAsync(player, "EXP-...")
    →
    { IsSubscribed, IsRenewing }
    检查订阅状态,在玩家加入时以及触发
    Players.UserSubscriptionStatusChanged(player, subscriptionId)
    时执行。
  • 权益必须可撤销:当
    IsSubscribed
    为 false 时,移除已持久化的权益。
  • 使用
    MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)
    发起购买提示。
  • 用 Subscription 替换 Pass 时:继续认可现有 Pass 持有者的权益,并将 Pass 下架。

Shops that sell

商店销售

  • Personalize order with
    MarketplaceService:RankProductsAsync(identifiers)
    (call once on join — strict rate limit) and "Top picks" with
    RecommendTopProductsAsync({ Enum.InfoType.Product, Enum.InfoType.GamePass })
    (needs a sale in the last 28 days; call in
    task.spawn
    ).
  • Show live prices from
    GetProductInfoAsync(id, Enum.InfoType.Product)
    /
    GetDeveloperProductsAsync()
    — never hardcode Robux prices: managed pricing (regional pricing + price optimization) shows different users different prices.
    receipt.CurrencySpent
    has what the user actually paid.
  • Contextual offers (revive on death, boost when stuck) outperform static menus. Also list items in the game's Shop tab in Creator Hub.
  • 使用
    MarketplaceService:RankProductsAsync(identifiers)
    对商品排序进行个性化(在加入时调用一次——有严格限流);使用
    RecommendTopProductsAsync({ Enum.InfoType.Product, Enum.InfoType.GamePass })
    获取“热门推荐”(过去 28 天内必须发生过销售;在
    task.spawn
    中调用)。
  • 通过
    GetProductInfoAsync(id, Enum.InfoType.Product)
    /
    GetDeveloperProductsAsync()
    显示实时价格——绝不硬编码 Robux 价格:管理型定价(区域定价 + 价格优化)会向不同用户显示不同价格。
    receipt.CurrencySpent
    记录用户实际支付的价格。
  • 情境化优惠(死亡时复活、卡关时提供增益)比静态菜单效果更好。此外,还应在 Creator Hub 中游戏的 Shop 标签页下列出商品。

Compliance

合规

  • Paid random items (loot boxes, gacha, spins bought with Robux or Robux-bought currency): disclose odds before purchase, and check
    PolicyService:GetPolicyInfoForPlayerAsync(player)
    :
    • ArePaidRandomItemsRestricted
      → offer a free earnable path, a disclosed fixed sequence, or direct purchase.
    • IsPaidItemTradingAllowed
      → disable trading of paid items when false.
  • Regional pricing arbitrage: gate gifting/trading of purchased items using
    MarketplaceService:GetUsersPriceLevelsAsync(userIds)
    (1–1000, % of global price); fetch at join, don't cache across sessions.
  • Rewarded video rewards must be developer products, not random, and must not harm the player's character while the ad plays.
  • Respect age/region restrictions returned by
    PolicyService
    for ads, social links, and trading.
Rewarded video ads, commerce products, Premium/Roblox Plus hooks: references/ads-and-extras.md.
  • 付费随机物品(战利品箱、gacha、转盘,使用 Robux 或 Robux 购买的货币消费):必须在购买前披露概率,并检查
    PolicyService:GetPolicyInfoForPlayerAsync(player)
    :
    • ArePaidRandomItemsRestricted
      → 提供可通过免费方式获取的路径、已公开的固定顺序,或直接购买。
    • IsPaidItemTradingAllowed
      → 为 false 时禁止交易付费物品。
  • 区域定价套利:使用
    MarketplaceService:GetUsersPriceLevelsAsync(userIds)
    (1–1000,表示全球价格的百分比)限制赠送或交易已购物品;在玩家加入时获取,不要跨会话缓存。
  • 激励视频奖励必须是 Developer product,不能是随机物品,而且广告播放期间不得对玩家角色造成伤害。
  • 遵守
    PolicyService
    针对广告、社交链接和交易返回的年龄/地区限制。
激励视频广告、Commerce product、Premium/Roblox Plus 集成钩子:references/ads-and-extras.md。

Related skills

相关 Skill

roblox-data-stores
(persisting grants),
roblox-security
(economy exploits),
roblox-ui
(shop UI),
roblox-analytics-liveops
(economy events, funnels, experiments).
roblox-data-stores
(持久化权益发放)、
roblox-security
(经济系统漏洞)、
roblox-ui
(商店 UI)、
roblox-analytics-liveops
(经济事件、漏斗、实验)。