一键导入
roblox-monetization
ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
DataStores, ProfileStore, session locking, data persistence patterns.
Animations, particles, tweens, ContentProvider, visual effects.
GUI systems, layout, responsiveness, cross-platform UI. ScreenGuis, UIListLayout, constraint-based design.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
| name | roblox-monetization |
| description | ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction. |
| last_reviewed | "2026-05-21T00:00:00.000Z" |
Load this reference when:
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 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.
| Method / Event | Purpose |
|---|---|
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) |
Place this in ServerScriptService:
-- 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)
-- 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)
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.
| Method / Event | Purpose |
|---|---|
MarketplaceService:PromptProductPurchase(player, productId) | Show the purchase prompt |
MarketplaceService.ProcessReceipt | CRITICAL callback Roblox invokes to confirm granting |
ProcessReceipt is the single most important callback in Roblox monetization. Roblox calls it and expects one of two return values:
| Return Value | Meaning |
|---|---|
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:
MarketplaceService.ProcessReceipt. If two scripts both assign it, only the last one takes effect and the other is silently overwritten.PurchaseGranted ONLY after successfully persisting the granted item (DataStore save confirmed).NotProcessedYet so Roblox retries.Place this in ServerScriptService:
-- 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
-- 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)
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.
-- 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)
You can prompt non-Premium players to subscribe:
-- 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)
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).
Avoid: mid-gameplay interruptions, mandatory ads, ads that block progression.
| Robux | Typical Use |
|---|---|
| 25 | Minimum viable price. Small cosmetic, single-use consumable |
| 50 | Minor cosmetic pack, small currency bundle |
| 75 | Mid-tier consumable, trail effect, small pet |
| 100 | Standard GamePass, decent currency pack |
| 250 | Premium GamePass (2x coins, VIP), mid currency bundle |
| 500 | Major GamePass (significant gameplay advantage), large currency pack |
| 1,000 | Top-tier GamePass, mega currency bundle |
| 2,500+ | Whale-tier only. Use sparingly |
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:
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).
As of 2026, the Developer Exchange (DevEx) rate is approximately:
1 Robux earned = ~$0.0038 USD
This means:
Upcoming (June 2026): US creators 18+ will get a higher rate of ~$0.0054/Robux (42% increase). Requires identity verification.
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:
| DAU | Conversion Rate | Avg Purchase | Daily Robux | Monthly USD |
|---|---|---|---|---|
| 100 | 2% | 100 R$ | 200 | $23 |
| 1,000 | 2% | 100 R$ | 2,000 | $228 |
| 10,000 | 3% | 150 R$ | 45,000 | $5,130 |
| 100,000 | 3% | 150 R$ | 450,000 | $51,300 |
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.
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
These are not suggestions. Violating them gets your game taken down.
Any item sold with a randomized element MUST display the exact drop chance percentages in-game. This applies to:
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.
-- 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%"
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.
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.
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).
-- 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
Log every purchase for customer support and debugging:
-- 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:
ProcessReceipt will fire with test data.UserOwnsGamePassAsync returns false in Studio for passes the Studio user does not own.Good placements:
Bad placements:
-- 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 RemoteEvents and manipulate client-side logic. Always grant on the server.
-- 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
-- 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
-- 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
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:
-- 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
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.
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.