---
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.
