roblox-monetization
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRoblox monetization
Roblox 变现
Product types
产品类型
| Type | Buy | Use for | Grant via |
|---|---|---|---|
| Developer product | Repeatedly | Currency, consumables, boosts, revives | |
| Pass | Once, permanent | VIP, permanent perks, extra slots | |
| Subscription | Monthly, revocable | Recurring perks (daily currency, VIP) | |
| Rewarded video ad | Free (watch ad) | Small rewards (≈3–10 Robux value) | Developer product granted through |
| Commerce product | Real money (physical goods) | Merch with bundled digital benefits | |
| Private server | Monthly | Paid private servers | Configured in Creator Dashboard |
All granting happens on the server. Never grant from client events like
— clients can fake them.
PromptProductPurchaseFinished| 类型 | 购买方式 | 适用于 | 通过以下方式发放 |
|---|---|---|---|
| Developer product(开发者产品) | 可重复购买 | 货币、消耗品、增益、复活 | |
| Pass(通行证) | 购买一次,永久有效 | VIP、永久特权、额外槽位 | 加入时通过 |
| Subscription(订阅) | 按月购买,可取消 | 周期性权益(每日货币、VIP) | |
| Rewarded video ad(激励视频广告) | 免费(观看广告) | 小额奖励(约值 3–10 Robux) | 通过 |
| Commerce product(商业产品) | 真实货币(实体商品) | 捆绑数字权益的周边商品 | |
| Private server(私人服务器) | 按月付费 | 付费私人服务器 | 在 Creator Dashboard 中配置 |
所有发放操作都必须在 服务器 上完成。绝不根据 等客户端事件发放权益——客户端可以伪造这些事件。
PromptProductPurchaseFinishedDeveloper products: ProcessReceipt
Developer product:ProcessReceipt
Rules:
- Exactly one server script sets .
MarketplaceService.ProcessReceipt - Return only after the grant is persisted. Otherwise return
PurchaseGranted; Roblox retries later (including on the player's next join) with the sameNotProcessedYet.PurchaseId - Handlers must be idempotent: record handled s in the player's saved data so a retry never grants twice.
PurchaseId - 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 :: anyWith ProfileStore, "persisted" means waiting until the appears in
(or calling and waiting); see ProfileStore's developer products guide.
PurchaseIdprofile.LastSavedDataprofile:Save()Prompt from the client (UI) — the server still grants:
.
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)规则:
- 恰好一个服务器脚本设置 。
MarketplaceService.ProcessReceipt - 只有在权益发放已经持久化后,才能返回 ;否则应返回
PurchaseGranted。Roblox 稍后会重试(包括玩家下次加入时),并使用相同的NotProcessedYet。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 时,“已持久化”是指等待 出现在 中(或调用 并等待保存完成);参阅 ProfileStore 的开发者产品指南。
PurchaseIdprofile.LastSavedDataprofile:Save()客户端购买提示(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)UserOwnsGamePassAsyncluau
--!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)UserOwnsGamePassAsyncSubscriptions
Subscription
- Check with →
MarketplaceService:GetUserSubscriptionStatusAsync(player, "EXP-...")on join and on{ IsSubscribed, IsRenewing }.Players.UserSubscriptionStatusChanged(player, subscriptionId) - Benefits must be revocable: when is false, remove perks you persisted.
IsSubscribed - 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) - 权益必须可撤销:当 为 false 时,移除已持久化的权益。
IsSubscribed - 使用 发起购买提示。
MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId) - 用 Subscription 替换 Pass 时:继续认可现有 Pass 持有者的权益,并将 Pass 下架。
Shops that sell
商店销售
- Personalize order with (call once on join — strict rate limit) and "Top picks" with
MarketplaceService:RankProductsAsync(identifiers)(needs a sale in the last 28 days; call inRecommendTopProductsAsync({ Enum.InfoType.Product, Enum.InfoType.GamePass })).task.spawn - Show live prices from /
GetProductInfoAsync(id, Enum.InfoType.Product)— never hardcode Robux prices: managed pricing (regional pricing + price optimization) shows different users different prices.GetDeveloperProductsAsync()has what the user actually paid.receipt.CurrencySpent - 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)获取“热门推荐”(过去 28 天内必须发生过销售;在RecommendTopProductsAsync({ Enum.InfoType.Product, Enum.InfoType.GamePass })中调用)。task.spawn - 通过 /
GetProductInfoAsync(id, Enum.InfoType.Product)显示实时价格——绝不硬编码 Robux 价格:管理型定价(区域定价 + 价格优化)会向不同用户显示不同价格。GetDeveloperProductsAsync()记录用户实际支付的价格。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)- → offer a free earnable path, a disclosed fixed sequence, or direct purchase.
ArePaidRandomItemsRestricted - → disable trading of paid items when false.
IsPaidItemTradingAllowed
- Regional pricing arbitrage: gate gifting/trading of purchased items using
(1–1000, % of global price); fetch at join, don't cache across sessions.
MarketplaceService:GetUsersPriceLevelsAsync(userIds) - 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 for ads, social links, and trading.
PolicyService
Rewarded video ads, commerce products, Premium/Roblox Plus hooks: references/ads-and-extras.md.
- 付费随机物品(战利品箱、gacha、转盘,使用 Robux 或 Robux 购买的货币消费):必须在购买前披露概率,并检查 :
PolicyService:GetPolicyInfoForPlayerAsync(player)- → 提供可通过免费方式获取的路径、已公开的固定顺序,或直接购买。
ArePaidRandomItemsRestricted - → 为 false 时禁止交易付费物品。
IsPaidItemTradingAllowed
- 区域定价套利:使用 (1–1000,表示全球价格的百分比)限制赠送或交易已购物品;在玩家加入时获取,不要跨会话缓存。
MarketplaceService:GetUsersPriceLevelsAsync(userIds) - 激励视频奖励必须是 Developer product,不能是随机物品,而且广告播放期间不得对玩家角色造成伤害。
- 遵守 针对广告、社交链接和交易返回的年龄/地区限制。
PolicyService
激励视频广告、Commerce product、Premium/Roblox Plus 集成钩子:references/ads-and-extras.md。
Related skills
相关 Skill
roblox-data-storesroblox-securityroblox-uiroblox-analytics-liveopsroblox-data-storesroblox-securityroblox-uiroblox-analytics-liveops