Game Pass vs Developer Product: which one to use and how to script both
TL;DR
- Game Pass = one-time purchase, stored on the player's account permanently. Use it for permanent upgrades (VIP, 2x XP, unlock a class…).
- Developer Product = repeatable purchase, can be bought unlimited times. Use it for consumables (in-game currency, extra lives, skip-stage tokens…).
- Both go through
MarketplaceService, but the scripting pattern is completely different — and mixing them up is one of the most common beginner monetisation mistakes.
The core difference: one-time vs repeatable
The question comes up constantly: "I want to sell coins in my game — Game Pass or Developer Product?"
The answer is almost always a Developer Product. Here's why.
Game Passes are tied to the player's Roblox account. Once bought, the pass lives in their inventory forever. Your server checks ownership with UserOwnsGamePassAsync — if it returns true, the benefit is applied every session automatically. This is exactly what you want for permanent unlocks: a VIP rank, a special sword, double XP, access to a private area, etc.
Developer Products work differently. They don't persist in inventory at all — Roblox doesn't track "how many times a player bought this." Instead, every completed purchase fires the ProcessReceipt callback on your server, and you are responsible for applying the reward and saving the state. Because they can be bought over and over again, they're the right tool for in-game currency, consumable boosts, or anything a player might want to purchase multiple times.
A classic trap: a developer creates a "100 Coins" Game Pass. The player buys it once, gets 100 coins, then can never buy it again. Using a Developer Product instead lets the player top up their wallet as many times as they want.
Setting things up in Creator Hub
Both types are managed from Creator Hub → your experience → Monetization.
- Game Passes: Monetization → Game Passes → "Create a Game Pass", upload a 512×512 icon, write a name and description, set your price. The pass ID on that page is what you'll paste into your script.
- Developer Products: Monetization → Developer Products — same flow.
Roblox takes a 30% commission on all sales for both types, so factor that into your pricing.
Once created, neither does anything on its own — you must write the server-side logic.
Scripting Game Passes
Game pass logic lives in a Script inside ServerScriptService. The key API is MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId).
Always run this check on the server, never in a LocalScript — client-side checks can be spoofed by exploiters.
-- ServerScriptService/GamePassHandler (Script)
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local VIP_PASS_ID = 123456789 -- replace with your actual pass ID
local function onPlayerAdded(player)
local success, ownsPass = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, VIP_PASS_ID)
end)
if success and ownsPass then
player:SetAttribute("IsVIP", true)
print(player.Name .. " owns the VIP pass!")
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
-- Handle players already in-server before the script loaded
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end
You can also prompt a purchase from a UI button:
MarketplaceService:PromptGamePassPurchase(player, VIP_PASS_ID)
After the player buys, listen to PromptGamePassPurchaseFinished to apply the benefit immediately in the same session without waiting for a rejoin.
Scripting Developer Products — ProcessReceipt
Developer Products require a fundamentally different approach. You must implement MarketplaceService.ProcessReceipt. This is the only reliable way to grant the purchase — do not rely on PromptProductPurchaseFinished for granting rewards, as that event can fire even when the backend purchase has failed.
The callback receives a receiptInfo table with ProductId, PlayerId, and PurchaseId. Return Enum.ProductPurchaseDecision.PurchaseGranted when the reward is safely applied, or Enum.ProductPurchaseDecision.NotProcessedYet to keep the receipt in the queue for a retry.
-- ServerScriptService/DevProductHandler (Script)
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local COINS_PRODUCT_ID = 987654321 -- 100 coins bundle
local SKIP_STAGE_PRODUCT = 987654322 -- skip current stage
local playerData = {}
Players.PlayerAdded:Connect(function(player)
playerData[player.UserId] = { coins = 0, stage = 1 }
end)
Players.PlayerRemoving:Connect(function(player)
playerData[player.UserId] = nil
end)
-- Set ProcessReceipt ONCE per server — multiple assignments overwrite each other
MarketplaceService.ProcessReceipt = function(receiptInfo)
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
if not player then
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local data = playerData[player.UserId]
if not data then
return Enum.ProductPurchaseDecision.NotProcessedYet
end
if receiptInfo.ProductId == COINS_PRODUCT_ID then
data.coins += 100
print(player.Name .. " received 100 coins. Total:", data.coins)
elseif receiptInfo.ProductId == SKIP_STAGE_PRODUCT then
data.stage += 1
print(player.Name .. " skipped to stage", data.stage)
end
-- Only return PurchaseGranted after the reward is safely saved
return Enum.ProductPurchaseDecision.PurchaseGranted
end
Key rules from the official Roblox docs:
- Set
ProcessReceiptonly once per server — it must handle all your Developer Products viaif/elseifonProductId. - If your DataStore save fails, return
NotProcessedYetso Roblox retries rather than silently losing the grant. - If a player buys and disconnects before
ProcessReceiptfires, Roblox will re-queue the receipt and call the callback the next time that player joins any server running your game.
Quick comparison table
| Feature | Game Pass | Developer Product |
|---|---|---|
| Purchase type | One-time | Repeatable (unlimited) |
| Stays in inventory? | Yes, forever | No |
| Check ownership | UserOwnsGamePassAsync | N/A — use ProcessReceipt |
| Prompt purchase | PromptGamePassPurchase | PromptProductPurchase |
| Good for | VIP, unlocks, permanent perks | Currency, consumables, skips |
| Roblox cut | 30% | 30% |
Common pitfalls
- Game Pass for in-game currency. Players buy it once and can never top up again — always use a Developer Product for anything repeatable.
- Ownership check in a LocalScript. Exploiters can manipulate the client. Always use a server
ScriptforUserOwnsGamePassAsync. - Returning
PurchaseGrantedbefore saving. Save to DataStore first, then returnPurchaseGrantedto avoid lost grants on crashes. - Multiple
ProcessReceiptassignments. Only the last one takes effect, silently dropping all previous handlers. - Granting inside
PromptProductPurchaseFinished. This fires when the prompt closes, not when the purchase is confirmed. UseProcessReceiptexclusively.
FAQ
Can I check if a player has ever bought a Developer Product? Not natively — Roblox doesn't store purchase history for you. Save a flag in a DataStore inside your ProcessReceipt callback if you need to track this.
Can I make a Game Pass free (0 Robux)? Yes. It still lands in inventory and is checkable with UserOwnsGamePassAsync. Handy for testing or "free follow" gating.
I see GamePassService in old code. Should I use it? No — it's deprecated. Use MarketplaceService for both game passes and developer products.
If you're looking for ready-made monetisation systems to build on top of these patterns, check the GM Market marketplace for Roblox scripts. But understanding the Game Pass vs Developer Product distinction above will save you a lot of debugging time regardless of which path you take.