Use when adding ways to earn Robux in a Roblox game - developer products, game passes, subscriptions, the built-in Shop, selling avatar items in-game, Robux transfers or donations, managed/regional pricing - or when writing receipt handling (BindReceiptHandler or ProcessReceipt). Covers the 2025-2026 APIs and policies and how to grant purchases exactly once.
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-monetization" and install it as a Claude Code skill in this project (.claude/skills/roblox-monetization/SKILL.md).
Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.
What your AI reads
SKILL.md
---
name: roblox-monetization
description: Use when adding ways to earn Robux in a Roblox game - developer products, game passes, subscriptions, the built-in Shop, selling avatar items in-game, Robux transfers or donations, managed/regional pricing - or when writing receipt handling (BindReceiptHandler or ProcessReceipt). Covers the 2025-2026 APIs and policies and how to grant purchases exactly once.
---
# Monetization that works
Roblox games earn from **developer products** (buy again and again: currency, boosts, revives), **passes**
(one-time, permanent perks), **subscriptions** (monthly perks), **avatar items** sold in-game, **Robux transfers**,
private servers, and engagement-based payouts. All purchases go through `MarketplaceService`. For the short
checklist, also load the `roblox-purchase-safety` skill.
## Developer products: receipts
The prompt only *asks*. Payment is proven only by the **receipt** delivered to your handler on the server.
`PromptProductPurchaseFinished` is not proof of payment; never grant from it.
`MarketplaceService:BindReceiptHandler` is the newer way to receive receipts. It returns a connection, can be bound
per product (`filter` array of product IDs) or as one catch-all, and the handler returns `Enum.ReceiptDecision`.
Bound handlers take precedence; receipts they don't match fall through to the legacy `ProcessReceipt` callback,
so you can migrate gradually. Binding the same product twice, or two catch-alls, throws.
Rules that matter:
- The receipt can be delivered again (failed save, player rejoined, a second server while the first is still
running). Grant **exactly once** per `PurchaseId`, and use session-locked player data so two servers can't both
write.
- Save the grant and the `PurchaseId` **together**, and return `Processed` only after the save succeeded.
- Return `NotProcessedYet` when you can't finish (player gone, data not loaded, save failed). It is redelivered the
next time the buyer purchases something or joins a server.
- Products can be bought **outside your game** (Store tab, Roblox homepage and search, after buying Robux) when they
are **Listed**. The receipt arrives when the player joins, so the handler must work at join time. Roblox checks
your receipt handling and stops surfacing products that aren't processed correctly.
```luau
-- ServerScriptService/Receipts.server.luau
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local PlayerData = require(script.Parent.PlayerData) -- your session-locked data module (e.g. built on ProfileStore)
local REWARDS: { [number]: (data: any) -> () } = {
[123456] = function(data) data.Coins += 1000 end,
[234567] = function(data) data.Gems += 50 end,
}
MarketplaceService:BindReceiptHandler(Enum.ReceiptType.DeveloperProduct, function(receipt)
local player = Players:GetPlayerByUserId(receipt.PlayerId)
local data = player and PlayerData.get(player) -- nil until the player's data has loaded
local grant = REWARDS[receipt.ProductId]
if not (player and data and grant) then
return Enum.ReceiptDecision.NotProcessedYet
end
if not data.PurchaseIds[receipt.PurchaseId] then
grant(data)
data.PurchaseIds[receipt.PurchaseId] = os.time()
end
-- Save even on a repeat delivery: the earlier attempt may have granted in memory but failed to save
if not PlayerData.saveAsync(player) then
return Enum.ReceiptDecision.NotProcessedYet
end
return Enum.ReceiptDecision.Processed
end)
```
Receipt fields: `PurchaseId`, `PlayerId`, `ProductId`, `PlaceIdWherePurchased`, `CurrencyType`, `CurrencySpent`
(the price actually paid, which can differ per player under managed pricing), `ReceiptType`. Trim
`PurchaseIds` to the most recent hundred or so.
Legacy `ProcessReceipt` follows the same rules with `Enum.ProductPurchaseDecision.PurchaseGranted` /
`NotProcessedYet`. Assign it once, in one server script. If no handler exists at all, receipts are auto-acknowledged
and lost.
## Passes
```luau
local VIP_PASS = 1234567
local function ownsPass(player: Player, passId: number): boolean
local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, passId)
return ok and owns
end
Players.PlayerAdded:Connect(function(player)
if ownsPass(player, VIP_PASS) then
giveVip(player)
end
end)
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, wasPurchased)
if wasPurchased and passId == VIP_PASS then
giveVip(player)
end
end)
```
`UserOwnsGamePassAsync` is cached: it updates when `PromptGamePassPurchaseFinished` fires, always returns true on
the first join after a purchase, and can take minutes to notice a purchase made outside the game during a session.
Grant pass perks from ownership; don't store them as if they were consumables.
## Subscriptions
Monthly, auto-renewing perks bought in Robux (49 Robux or more, regional pricing always on) or local currency
(fixed USD tiers, needs an ID- or phone-verified creator). Up to 50 per game; tiers can't be mutually exclusive.
Subscription IDs look like `"EXP-..."`.
```luau
-- Server
local SUB_ID = "EXP-11111111"
local function refreshSubscription(player: Player)
local ok, status = pcall(MarketplaceService.GetUserSubscriptionStatusAsync, MarketplaceService, player, SUB_ID)
if ok then
setSubscriber(player, status.IsSubscribed) -- grant or revoke the perks
end
end
Players.PlayerAdded:Connect(refreshSubscription)
Players.UserSubscriptionStatusChanged:Connect(function(player, subscriptionId)
if subscriptionId == SUB_ID then
refreshSubscription(player)
end
end)
```
Prompt with `MarketplaceService:PromptSubscriptionPurchase(player, SUB_ID)`. Status checks must run on the server.
Honor the benefits for the full term and give the same benefits on every platform.
## Show real prices (managed pricing)
**Managed Pricing** (Creator Hub > Monetization) combines price optimization tests and regional pricing. Regional
prices stay between 30% and 100% of your default price, and new items are enrolled by default. It only works if
your UI reads prices at runtime instead of hard-coding them:
```luau
-- LocalScript, so the player sees their own price
local info = MarketplaceService:GetProductInfoAsync(PRODUCT_ID, Enum.InfoType.Product) -- or Enum.InfoType.GamePass
priceLabel.Text = `{info.PriceInRobux} R$`
```
`GetDeveloperProductsAsync()` returns a `Pages` object of all products. Use the Dynamic Price Check tool in Creator
Hub to find hard-coded prices. If players can trade or gift items bought with Robux, call
`MarketplaceService:GetUsersPriceLevelsAsync(userIds)` at join (price level 1 to 1000, 1000 = full price) and block
transfers that would exploit regional price gaps. Don't cache price levels across sessions.
## The built-in Shop and recommendations
Every game has a personalized **Shop** listing your passes and Listed developer products. Open it with
`MarketplaceService:OpenShop(player)`; enable the global Shop button and edit categories in Creator Hub > Monetization
> Shop. For your own store UI, `RankProductsAsync` (up to 50 products, 10 calls per minute) and
`RecommendTopProductsAsync({ Enum.InfoType.GamePass, Enum.InfoType.Product })` (5 calls per minute) order items per
player. Load them once per session.
## Avatar items in your game
You can sell avatar items (accessories, clothing, bundles) exclusively in your game and earn the in-game commission.
The item creator adds your start place ID to the item's Sale Location; you enable it under Monetization >
Avatar Items. Then:
- `MarketplaceService:PromptPurchase(player, assetId)` for one item. Call it from the server if the sale location
is "Experience By Place ID (API Only)".
- `MarketplaceService:PromptBulkPurchase(player, lineItems, {})` (server only, up to 20 items) for whole outfits:
`{ { Type = Enum.MarketplaceProductType.AvatarAsset, Id = "123" } }`.
- Roblox delivers the item; `PromptPurchaseFinished` / `PromptBulkPurchaseFinished` are for UI. Check
`PlayerOwnsAssetAsync` before granting any in-game bonus.
`PromptPurchase` does not work for Creator Store items sold for USD, so paid models and plugins can't be sold inside
a game.
## Robux transfers (donations)
Since May 30, 2026, games can no longer sell other games' passes or developer products. Donation and "give Robux"
games use **Robux transfers** instead: Roblox Plus subscribers send 10 to 500 Robux; the receiver gets 90% and your
game 10%. Call `MarketplaceService:PromptRobuxTransferAsync(sender, receiverUserId, amount)` from the server (it
returns a transfer request ID) and handle `Enum.ReceiptType.RobuxTransferSender` and `RobuxTransferReceiver` with
`BindReceiptHandler` before granting any reward.
## Paid random items
Eggs, crates, spins and luck boosts bought with Robux, or with currency bought with Robux, must show every outcome
and its exact odds (percentages summing to 100%) before purchase. Check
`PolicyService:GetPolicyInfoForPlayerAsync(player).ArePaidRandomItemsRestricted` and hide them where restricted.
## Mistakes to avoid
- Granting from `PromptProductPurchaseFinished`, the client, or before the save succeeded.
- Two scripts assigning `ProcessReceipt`, or binding the same product twice.
- Not checking `PurchaseId`, so a redelivered receipt grants twice.
- Hard-coded prices in UI (breaks managed pricing and shows wrong prices).
- Product IDs, prices or rewards accepted from the client. Keep them in server tables.
- Relying on cross-game pass or product sales (disabled since May 30, 2026).
- Selling random rewards for Robux without odds disclosure.