Game Passes vs. Developer Products
Roblox offers two monetization primitives. Game passes are one-time purchases that grant permanent perks — VIP access, double XP, exclusive cosmetics, extra inventory slots. A player buys a game pass once and owns it forever. Developer products are repeatable purchases for consumable items — 100 coins, a revive token, a temporary speed boost. A player can buy the same developer product unlimited times. Your shop will likely use both. Use MarketplaceService:UserOwnsGamePassAsync(userId, passId) on the server to check game pass ownership. For developer products, use MarketplaceService.ProcessReceipt to handle each purchase. Choose carefully which items should be one-time (passes) versus repeatable (products) — making a powerful item a developer product instead of a game pass means players can buy unlimited power, which can break game balance.
Server-Side Receipt Processing
Receipt processing is the most critical part of your shop system. When a player buys a developer product, Roblox calls your MarketplaceService.ProcessReceipt callback on the server. This callback receives a receiptInfo table containing the player's UserId, the ProductId, and a unique PurchaseId. Your callback must grant the item, save the purchase to the player's data, and return Enum.ProductPurchaseDecision.PurchaseGranted. If you return anything else (or the callback errors), Roblox will retry the callback later — this is how the system handles server crashes during purchases. The critical rule: always save the PurchaseId to the player's DataStore and check for it at the start of ProcessReceipt. If the PurchaseId already exists, the item was already granted and you should return PurchaseGranted without granting again. This prevents duplicate item grants on retries.
- Always set ProcessReceipt in a server Script, never a LocalScript
- Save PurchaseId to the player's DataStore before returning PurchaseGranted
- Check for existing PurchaseId at callback start to prevent duplicate grants
- Wrap the callback in pcall — an unhandled error means Roblox retries indefinitely
- Log every transaction with UserId, ProductId, and PurchaseId for debugging
Designing the Shop UI
The shop UI should be clean, scannable, and make purchasing frictionless. Use a grid or list layout with clear item cards. Each card needs: an item image or icon, the item name, a brief description of what it does, and the price with a prominent "Buy" button. Group items by category (Passes, Currency, Consumables, Cosmetics) with tab navigation. Show ownership state — if the player already owns a game pass, change the button to "Owned" and disable it. For developer products, show the player's current quantity (e.g., "You have 340 coins"). Display Robux prices using MarketplaceService:GetProductInfo() so they stay in sync if you change prices on the website. Use UIListLayout with padding and UISizeConstraint for responsive layouts that work on mobile screens.
- Grid layout with 2-3 columns on desktop, 1 column on mobile
- Item cards: icon (left), name + description (center), price + buy button (right)
- Category tabs at the top: Passes, Currency, Boosts, Cosmetics
- Owned state: green checkmark, "Owned" label, disabled button for purchased passes
- Confirmation dialog before purchase to prevent accidental buys
Prompting Purchases and Handling Callbacks
When the player clicks "Buy," call MarketplaceService:PromptGamePassPurchase(player, passId) for game passes or MarketplaceService:PromptProductPurchase(player, productId) for developer products. These open Roblox's native purchase dialog. Listen for the result on the client with MarketplaceService.PromptGamePassPurchaseFinished and PromptProductPurchaseFinished signals. Update the UI immediately when a purchase succeeds — change the button to "Owned," update currency display, or play a celebration effect. The actual item grant happens server-side through ProcessReceipt (for products) or by checking UserOwnsGamePassAsync (for passes). Never grant items on the client based on the purchase prompt result — always validate server-side.
In-Game Currency Systems
Many games sell a premium currency (gems, crystals, tokens) via developer products, then let players spend that currency on in-game items. This adds a layer between real money and items, letting you price items flexibly without creating new developer products for each one. Implement currency as a value in the player's save data, managed entirely on the server. The shop UI reads the currency balance via a RemoteFunction, and purchase requests go through a RemoteEvent that the server validates (does the player have enough currency, is the item valid, is the price correct). Never store item prices on the client — an exploiter could modify client-side prices to buy items for free. KitsBlox UI kits include shop templates with currency display, purchase flow, and inventory integration already built.
Testing Purchases in Studio
You cannot test real Robux transactions in Studio, but you can test the flow. MarketplaceService works in Studio's test modes — purchase prompts will appear and you can simulate accepting or declining. Use Studio's Team Test mode to test multiplayer purchase scenarios. For ProcessReceipt testing, manually fire the callback with mock receiptInfo tables. Log every step of the purchase flow (prompt sent, prompt result, receipt received, item granted, data saved) so you can trace issues in production. Before publishing, test edge cases: what happens if the player disconnects during a purchase? What if the DataStore save fails? What if ProcessReceipt is called twice with the same PurchaseId?
