Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.
Roblox monetization
Product types
| Type | Buy | Use for | Grant via |
|---|---|---|---|
| Developer product | Repeatedly | Currency, consumables, boosts, revives | MarketplaceService.ProcessReceipt (server) |
| Pass | Once, permanent | VIP, permanent perks, extra slots | UserOwnsGamePassAsync check on join + purchase-finished event |
| Subscription | Monthly, revocable | Recurring perks (daily currency, VIP) | GetUserSubscriptionStatusAsync + UserSubscriptionStatusChanged |
| Rewarded video ad | Free (watch ad) | Small rewards (≈3–10 Robux value) | Developer product granted through ProcessReceipt |
| Commerce product | Real money (physical goods) | Merch with bundled digital benefits | CommerceService; digital benefits via ProcessReceipt |
| Private server | Monthly | Paid private servers | Configured in Creator Dashboard |
All granting happens on the server. Never grant from client events like PromptProductPurchaseFinished — clients can fake them.
Developer products: ProcessReceipt
Rules:
- Exactly one server script sets
MarketplaceService.ProcessReceipt. - Return
PurchaseGrantedonly after the grant is persisted. Otherwise returnNotProcessedYet;
Roblox retries later (including on the player's next join) with the same PurchaseId.
- Handlers must be idempotent: record handled
PurchaseIds 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.
--!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 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).
Passes
--!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.
Subscriptions
- Check with
MarketplaceService:GetUserSubscriptionStatusAsync(player, "EXP-...")→{ IsSubscribed, IsRenewing }
on join and on Players.UserSubscriptionStatusChanged(player, subscriptionId).
- Benefits must be revocable: when
IsSubscribedis 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.
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.
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
PolicyServicefor ads, social links, and trading.
Rewarded video ads, commerce products, Premium/Roblox Plus hooks: references/ads-and-extras.md.
Related skills
roblox-data-stores (persisting grants), roblox-security (economy exploits), roblox-ui (shop UI), roblox-analytics-liveops (economy events, funnels, experiments).

