Claude Skill

roblox-gamepasses

Implement Roblox game passes, developer products, subscriptions, and Robux transfers. Use for purchase prompts, server-side fulfillment, ownership checks, recurring benefits, and monetization policy.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download nonlooped-roblox-suite-roblox-gamepasses-2904c16.zip · 21 KB
Part of nonlooped/roblox-suite — 15 skills

Install

skills CLI npx skills add https://github.com/nonlooped/roblox-suite/tree/main/roblox-gamepasses
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nonlooped-roblox-suite@llmmart
Git git clone https://github.com/nonlooped/roblox-suite.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nonlooped/roblox-suite collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

roblox-gamepasses

Primary official source: https://create.roblox.com/docs/en-us/production/monetization/passes (plus developer-products.md for contrast, and the MarketplaceService class reference).

Grant benefits on the server after verifying the purchase or ownership. Keep purchase prompts and shop display on the client.

See roblox-datastores for how to store "player owns this pass" state or associated perks, and roblox-networking for the Remote validation layer around purchase prompts.

Game passes and developer products

  • Game Pass: One-time purchase. Permanent privilege for that specific experience (VIP access, permanent item, extra slot, cosmetic unlock, etc.). Roblox tracks ownership per user per experience.
  • Developer Product: Repeatable / consumable (currency packs, potions, revives, temporary boosts). Can be bought many times. Requires ProcessReceipt callback on the server for fulfillment.
  • As of May 30, 2026, cross-experience game pass and developer product sales are disabled. Design experience-specific passes or use the Robux Transfers API for donation-style flows. MarketplaceService:PromptRobuxTransferAsync must be called from the server. Only Roblox Plus subscribers can initiate transfers, transfer amounts must be between 10 and 500 Robux, and the sender cannot equal the receiver. The recipient receives 90% and the experience earns the other 10%; Roblox takes no transfer fee. A BindReceiptHandler callback must process transfer receipts. Roblox permits an experience to grant items or perks after a successful receipt; choosing not to attach rewards is an opinionated anti-abuse/design recommendation, not a platform rule.
  • You (the creator) are 100% responsible for actually delivering the benefit. Roblox only handles the transaction and the UserOwnsGamePassAsync query.
  • Passes can be used for randomized virtual items only if you follow the Paid Random Items policy.

Creation and asset ID

  1. Creator Dashboard → your published experience → Monetization → Passes → Create a Pass.
  2. Upload circular-friendly icon (≤512×512, jpg/png/bmp; important content must survive circular crop).
  3. Name + description.
  4. After creation: hover the pass → ⋯ → Copy Asset ID. This number is the passID you use in all scripts.

For external sales on the game page Store tab: go to the pass → Sales → enable "Item for Sale" and set Robux price (1 to 1B).

Prompt purchases and grant benefits

Client side (LocalScript or UI module; only for prompting and optimistic display):

  • Call MarketplaceService:UserOwnsGamePassAsync (pcall) to decide "Buy" vs "Owned" button state.
  • If not owned, call MarketplaceService:PromptGamePassPurchase(player, passID).

Server side (Script in ServerScriptService; the only place that grants benefits):

local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local PASS_ID = 1234567890
local granted = {} -- idempotency guard: granted[player][passID]

local function grantPassBenefits(player: Player, passID: number)
    if not player or not player:IsDescendantOf(Players) then
        return
    end
    local playerGranted = granted[player]
    if not playerGranted then
        playerGranted = {}
        granted[player] = playerGranted
    end
    if playerGranted[passID] then
        return
    end
    playerGranted[passID] = true

    -- Apply the actual privilege (attributes, table entry, DataStore flag, Remote to client for visuals, etc.)
    -- This is the source of truth.
end

MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, purchasedPassID: number, wasPurchased: boolean)
    -- This event also fires on the client; never grant benefits from the client copy.
    if not player or typeof(purchasedPassID) ~= "number" then
        return
    end
    if wasPurchased and purchasedPassID == PASS_ID then
        local ok, err = pcall(function()
            grantPassBenefits(player, PASS_ID)
        end)
        if not ok then
            warn("Failed to grant pass benefits:", err)
        end
    end
end)

Players.PlayerAdded:Connect(function(player: Player)
    local success, owns = pcall(function()
        return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
    end)
    if success and owns then
        local ok, err = pcall(function()
            grantPassBenefits(player, PASS_ID)
        end)
        if not ok then
            warn("Failed to apply owned pass benefits:", err)
        end
    elseif not success then
        warn("UserOwnsGamePassAsync failed for", player.Name)
    end
end)

Players.PlayerRemoving:Connect(function(player: Player)
    granted[player] = nil
end)

GetProductInfoAsync for dynamic UI (price, name, description, IsForSale): Use MarketplaceService:GetProductInfoAsync(id, Enum.InfoType.GamePass). Do this on the client for display, but never grant based on the result. The non-async counterpart GetProductInfo still exists, but GetProductInfoAsync is preferred.

Capability requirements

Game passes and developer products require a published experience. Enable Studio Access to API Services only when a specific API used by a dedicated test experience requires it; do not treat that Studio setting as a blanket prerequisite for every MarketplaceService purchase API. Verify each API's current requirements in its Engine Reference entry.

Product recommendations

  • MarketplaceService:RankProductsAsync(arrayOfIdentifiers): pass a table of up to 50 {InfoType = Enum.InfoType.GamePass, Id = ...}. Returns a personalized ranking for the current user as {ProductIdentifier, ProductInfo} items. Use sparingly; call once at join.
  • MarketplaceService:RecommendTopProductsAsync({Enum.InfoType.GamePass, Enum.InfoType.Product}): returns up to 50 recommended products the user is likely to engage with. Results usually exclude already-owned items, but verify in your UI. Use sparingly; call once at join.

Use these results in a "Recommended for you" section of your shop. Measure whether they improve conversion in your experience.

Promotions (buy robux page bonus pool)

You can opt passes into the promotion pool so that users buying Robux packages may receive the pass for free (contextually relevant to their history).

Requirements (from the passes doc):

  • Unique pass recommended.
  • If on sale, price between 50 and 800 Robux.
  • Must have thumbnail.
  • Must comply with Community Standards.
  • Cannot be a paid random item.

Opt-in via the pass's Promotions tab in the dashboard.

Analytics and iteration

In Creator Dashboard → experience → Monetization → Passes → Analytics tab you get:

  • Top passes by sales and net Robux.
  • Time-series graphs.
  • Attribution for passes acquired via Buy Robux promotions and subsequent joins.

Use this data to decide pricing, which perks are compelling, and when to run promotions.

Security, data, and policy constraints

  • Prompt only from client. Grant and record only on the server in the PromptGamePassPurchaseFinished handler or on PlayerAdded re-check.
  • Always pcall UserOwnsGamePassAsync and Prompt... calls.
  • Re-check ownership on every relevant join/session start before granting gameplay or economy perks.
  • Store your own record of ownership + associated state in DataStores if you need history or custom metadata (Roblox does not expose full per-user pass purchase history via the Engine API).
  • For RTBF / right-to-be-forgotten, include pass-related keys in your deletion patterns (see roblox-datastores skill).
  • Never hardcode Robux prices in UI that the player sees: use GetProductInfoAsync so regional pricing and optimizations work.
  • Test purchases only on dedicated test experiences.
  • For paid random items (loot boxes / gacha passes), use PolicyService first: check PolicyService:GetPolicyInfoForPlayerAsync(player).ArePaidRandomItemsRestricted and IsPaidItemTradingAllowed before offering randomized paid content.
  • For developer products, MarketplaceService.ProcessReceipt can only be assigned once globally; assign it once in a single server script. The callback must return Enum.ProductPurchaseDecision.PurchaseGranted after successful fulfillment, or Enum.ProductPurchaseDecision.NotProcessedYet if fulfillment fails, because Roblox may redeliver the receipt until PurchaseGranted is returned.
  • Donation / tipping games using PromptRobuxTransferAsync are high-risk for abuse; implement rate limits, alt-account detection, moderation pipelines, and avoid granting in-experience advantages in exchange for transfers.

Subscriptions (recurring benefits)

Subscriptions offer users recurring benefits for a monthly fee, auto-renewing in Robux or local currency. Unlike passes (permanent), subscription benefits persist only while the user keeps paying. Up to 50 per experience; single-tiered (no mutually exclusive Bronze/Silver/Gold); regional pricing enabled by default for Robux-priced subs.

API surface (subscription IDs are strings like "EXP-11111111"):

  • MarketplaceService:GetUserSubscriptionStatusAsync(player, subscriptionId): server-only, returns {IsSubscribed: boolean}.
  • MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId): client prompt.
  • MarketplaceService.PromptSubscriptionPurchaseFinished(player, subscriptionId, didTryPurchasing): note didTryPurchasing is an attempt signal, not success; re-check status after a delay.
  • Players.UserSubscriptionStatusChanged(player, subscriptionId): server-only, fires on purchase/renewal/cancellation.
  • MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId) and GetUserSubscriptionPaymentHistoryAsync(player, subscriptionId).

Security (same posture as passes): prompt on client, check status and grant/revoke on server only, pcall everything, re-check on every join (subscriptions lapse), respect PolicyService:IsEligibleToPurchaseSubscription per player, persist nothing sensitive on the client.

Payouts: Robux-priced subs pay 70% each month. Local-currency subs pay 70% first month, 100% thereafter, with a 30-day hold. Robux subs are not refundable; local-currency subs are refundable within the hold window.

See references/subscriptions.md for the complete flow, client/server code, migration from passes, and gotchas.

Creator Rewards (replaces Premium Payouts)

As of July 24, 2025, Engagement-Based Payouts (formerly "Premium Payouts") and the Creator Affiliate program were discontinued and replaced by Creator Rewards. There is no longer a per-Premium-play-minute payout to integrate against.

Creator Rewards is a platform program with two payment types. It requires no in-experience integration:

  • Daily Engagement Rewards: 5 Robux per day per Active Spender who spends 10+ minutes in your experience, provided it's one of the first three experiences they visit that day.
  • Audience Expansion Rewards: 35% revenue share on a new/reactivated user's first $100 of qualifying purchases in their first 60 days, attributed via Share Links, direct experience links, or experience-name search.

Official source: https://create.roblox.com/docs/en-us/creator-rewards

When to use game passes vs other monetization

  • One-time permanent unlock or access → Game Pass.
  • Repeatable purchase (currency, consumables, temporary power) → Developer Product (with proper ProcessReceipt).
  • Recurring monthly benefit → Subscription (see references/subscriptions.md; MarketplaceService:GetUserSubscriptionStatusAsync etc.).

See the developer-products doc for the repeatable flow.

Scripts

  • scripts/PassPurchaseHelper.lua: a client-side helper for game pass button state, price display, and prompting.

Reference index

Files (roblox-suite)
  • references
    • creation-and-setup.md 4.9 KB
      ---
      read_when: "Create a pass, find its asset ID, or configure sales and test access"
      last_reviewed: 2026-06-17
      ---
      
      # Creation and setup of game passes
      
      **Official guide:** https://create.roblox.com/docs/en-us/production/monetization/passes
      
      ## Creating a pass
      
      1. Go to Creations in Creator Dashboard.
      2. Select your experience (must be published).
      3. Monetization → Passes → "Create a Pass".
      4. Upload icon: Must be suitable for circular crop (important visual content must be inside the circle). Recommended formats: .jpg, .png, .bmp. Max 512x512.
      5. Provide name and description.
      6. Submit.
      
      After creation, the pass appears in the list.
      
      ## Obtaining the pass ID (asset ID)
      
      - Hover over the pass thumbnail in the list.
      - Click the ⋯ menu.
      - Select "Copy Asset ID".
      
      This ID (a number like 1234567890) is what you use in all `MarketplaceService` calls:
      - `GetProductInfoAsync(id, Enum.InfoType.GamePass)` (preferred; the non-async `GetProductInfo` still exists but `GetProductInfoAsync` is recommended)
      - `PromptGamePassPurchase(player, id)`
      - `UserOwnsGamePassAsync(userId, id)`
      
      ## Enabling sales (external / game page store)
      
      For the pass to appear on the game's Store tab on Roblox.com:
      
      1. In the Passes list, select the pass or go to its detail.
      2. Sales tab.
      3. Toggle "Item for Sale".
      4. Enter Robux price (minimum 1, max 1,000,000,000).
      5. Save.
      
      The price affects your Robux earnings after Roblox fees.
      
      ## Icon best practices and validation errors
      
      Icons are validated strictly. Common issues:
      - Content outside the circular boundary gets cropped.
      - Low resolution or poor contrast.
      - Text or important details near edges.
      
      Test by viewing the pass in the in-experience purchase prompt and on the web store.
      
      ## Group vs individual ownership
      
      When publishing the animation or pass:
      - If the experience is group-owned, select the group as the creator during publish/export for the asset.
      - Same applies conceptually for passes (the experience ownership determines who can manage).
      
      ## Testing setup
      
      - If a backend API used in Studio requires it, enable **Studio Access to API Services** only for a dedicated test experience. Do not grant Studio sessions access to production data.
      - Create a separate test experience/universe that mirrors your production one.
      - Test the full flow: prompt → purchase (use small test Robux if possible, or Roblox test accounts).
      - Verify `UserOwnsGamePassAsync` returns true after purchase on new servers.
      - Test re-join behavior.
      
      ## Initial Script skeleton (before full flow)
      
      Place in ServerScriptService:
      
      ```lua
      local MarketplaceService = game:GetService("MarketplaceService")
      local Players = game:GetService("Players")
      
      local PASS_ID = 1234567890  -- Replace with your pass ID
      local granted = {} -- idempotency guard: granted[player][passID]
      
      local function grantPassBenefits(player: Player, passID: number)
          if not player or not player:IsDescendantOf(Players) then
              return
          end
          local playerGranted = granted[player]
          if not playerGranted then
              playerGranted = {}
              granted[player] = playerGranted
          end
          if playerGranted[passID] then
              return
          end
          playerGranted[passID] = true
      
          -- Apply permanent benefit here
          -- e.g. player:SetAttribute("HasVIP", true)
          -- or save to DataStore via your data manager
      end
      
      -- Server grant handler
      MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, passID: number, wasPurchased: boolean)
          -- This event also fires on the client; never grant benefits from the client copy.
          if not player or typeof(passID) ~= "number" then
              return
          end
          if wasPurchased and passID == PASS_ID then
              local ok, err = pcall(function()
                  grantPassBenefits(player, PASS_ID)
              end)
              if not ok then
                  warn("Failed to grant pass benefits:", err)
              end
          end
      end)
      
      -- Re-apply on join
      Players.PlayerAdded:Connect(function(player: Player)
          local owns = false
          local ok, err = pcall(function()
              owns = MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
          end)
          if ok and owns then
              local grantOk, grantErr = pcall(function()
                  grantPassBenefits(player, PASS_ID)
              end)
              if not grantOk then
                  warn("Failed to apply owned pass benefits:", grantErr)
              end
          elseif not ok then
              warn("Failed to check gamepass ownership for", player.Name, err)
          end
      end)
      
      Players.PlayerRemoving:Connect(function(player: Player)
          granted[player] = nil
      end)
      ```
      
      See the purchase-flow reference for the complete client prompting code and error handling.
      
      ## Common setup mistakes
      
      - Using the wrong InfoType (must be GamePass, not Product).
      - Forgetting that passes are experience-specific (after the 2026 change).
      - Not handling the case where the player already owns it (show "Owned" UI instead of prompting again).
      - Placing grant logic in a LocalScript.
      
      Next reference: purchase-flow-and-granting.md for the full end-to-end code.
      
    • policyservice.md 8.8 KB
      ---
      read_when: "Gate a feature using per-player monetization or content restrictions"
      last_reviewed: 2026-06-17
      ---
      
      # PolicyService reference
      
      **Official source:** https://create.roblox.com/docs/en-us/reference/engine/classes/PolicyService
      
      `PolicyService` queries per-player policy compliance based on geolocation, age group, and platform. Use it to gate monetization, content sharing, ads, commerce, and region-specific behavior **per player**, not globally. The same experience can have players in different policy regimes at once.
      
      `PolicyService` is a service (`game:GetService("PolicyService")`). It is `NotCreatable`, `NotReplicated`, and tagged as a Service. Its async methods **yield** and **must** be wrapped in `pcall`. They are **thread-unsafe** and require the `Basic` capability.
      
      ## Methods
      
      ### `GetPolicyInfoForPlayerAsync(player: Player): Dictionary`
      
      Returns a dictionary of policy flags for the player. **Yields**; wrap in `pcall`. Server-callable; on the client, only callable for `Players.LocalPlayer`.
      
      The returned dictionary contains these fields:
      
      | Field | Type | Gates |
      | --- | --- | --- |
      | `AreAdsAllowed` | boolean | Immersive ads. If false, do not show immersive ads to this player. |
      | `ArePaidRandomItemsRestricted` | boolean | Paid random items (loot boxes, gacha). If true, the player **cannot** interact with paid random item generators, whether via in-experience currency bought with Robux or Robux directly. |
      | `IsContentSharingAllowed` | boolean | UGC sharing (screen captures, video, image feeds). If false, disable features that let the user share content others can see. |
      | `IsEligibleToPurchaseCommerceProduct` | boolean | Real-world [commerce products](https://create.roblox.com/docs/en-us/production/monetization/commerce-products). If false, the player cannot purchase commerce products in-experience. |
      | `IsEligibleToPurchaseSubscription` | boolean | Subscriptions (see [subscriptions.md](subscriptions.md)). If false, do not offer a subscription purchase to this player. |
      | `IsPaidItemTradingAllowed` | boolean | Trading virtual items purchased with in-experience currency or Robux. If false, disable paid-item trading for this player. |
      | `IsPhotoToAvatarAllowed` | boolean | `AvatarCreationService:PromptSelectAvatarGenerationImageAsync()`. If false, the Photo-to-Avatar API is unavailable to this player. |
      | `IsSubjectToChinaPolicies` | boolean | If true, enforce China compliance changes (see the [Roblox China program post](https://devforum.roblox.com/t/new-programs-available-roblox-china-licensed-to-operate/1023361)). |
      | `IsEndlessContentLoadAllowed` | boolean | Features where content loads automatically and endlessly as the user scrolls (a feed). If false, require manual load/pagination instead. |
      | `IsEndlessContentAutoplayAllowed` | boolean | Media content (video or audio) that auto-plays endlessly without user initiation. If false, require explicit play actions. |
      | `AllowedExternalLinkReferences` | array | **Legacy.** Always returns an empty array. Do not rely on it. |
      
      ### `CanViewBrandProjectAsync(player: Player, brandProjectId: string): boolean`
      
      Determines whether a player may see a specific brand project's assets. Requires a brand project ID provided by Roblox (request one via the [brand project form](https://docs.google.com/forms/d/e/1FAIpQLSfGTRQwATB2wUg0P4HUSTtyXrhptFahJifo1ew84SyqtfSBfg/viewform)). **Yields**; wrap in `pcall`. **Server-only**. Calling from the client errors. Pattern: query on the server, then `RemoteEvent:FireClient(player, assetToShow)` with either the branded asset or a default fallback.
      
      ## Error handling
      
      Like any async call, wrap in `pcall`. Documented error messages:
      
      | Message | Reason |
      | --- | --- |
      | `Instance was not a player` | `player` parameter is not a `Player`. |
      | `Players not found` | Internal error; the `Players` service is missing. |
      | `This method cannot be called on the client for a non-local player` | Client-side call for a non-local `Player`. |
      | `GetPolicyInfoForPlayerAsync is called too many times` | More than ~100 concurrent calls before an HTTP response returns. Throttle. |
      
      ## Patterns
      
      ### Gate paid random items (loot boxes)
      
      ```lua
      local PolicyService = game:GetService("PolicyService")
      local Players = game:GetService("Players")
      
      local player = Players.LocalPlayer
      
      local success, result = pcall(function()
          return PolicyService:GetPolicyInfoForPlayerAsync(player)
      end)
      
      if not success then
          warn("PolicyService error: " .. tostring(result))
      elseif result.ArePaidRandomItemsRestricted then
          warn("Player cannot interact with paid random item generators")
          -- Hide/disable the gacha UI for this player
      end
      ```
      
      ### Gate subscription offers (server-side, per join)
      
      ```lua
      -- Server
      local PolicyService = game:GetService("PolicyService")
      local Players = game:GetService("Players")
      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      
      local showSubsRemote = ReplicatedStorage:WaitForChild("ShowSubscriptionOffers") -- RemoteEvent
      
      Players.PlayerAdded:Connect(function(player: Player)
          local ok, info = pcall(function()
              return PolicyService:GetPolicyInfoForPlayerAsync(player)
          end)
          if not ok then
              warn("PolicyService failed for " .. player.Name .. ": " .. tostring(info))
              showSubsRemote:FireClient(player, false)
              return
          end
          showSubsRemote:FireClient(player, info.IsEligibleToPurchaseSubscription == true)
      end)
      ```
      
      ### Gate brand projects (server queries, client renders)
      
      ```lua
      -- Server
      local PolicyService = game:GetService("PolicyService")
      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      local Players = game:GetService("Players")
      
      local brandedAsset = ReplicatedStorage:WaitForChild("BrandedAsset")
      local defaultAsset = Instance.new("Part")
      local RemoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
      
      Players.PlayerAdded:Connect(function(player: Player)
          local success, canView = pcall(function()
              return PolicyService:CanViewBrandProjectAsync(player, "BRP-0123456789")
          end)
          if success and canView then
              RemoteEvent:FireClient(player, brandedAsset)
          else
              RemoteEvent:FireClient(player, defaultAsset)
          end
      end)
      ```
      
      ```lua
      -- Client
      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      local RemoteEvent = ReplicatedStorage:WaitForChild("RemoteEvent")
      
      RemoteEvent.OnClientEvent:Connect(function(partToLoad)
          local clonedPart = partToLoad:Clone()
          clonedPart.Parent = workspace
      end)
      ```
      
      ## Rules and gotchas
      
      - **Every call yields and must be pcall'd.** A thrown error here is not a player kick. Handle it and degrade gracefully (usually: hide the gated feature).
      - **Throttle.** More than ~100 in-flight `GetPolicyInfoForPlayerAsync` calls before HTTP responses return will error. Cache the result per-player for the session; you rarely need to re-query mid-session unless a player's region/platform could change (rare).
      - **Call from the correct side.** `GetPolicyInfoForPlayerAsync` is server-safe and client-safe *for the local player only*; `CanViewBrandProjectAsync` is **server-only** and errors on the client.
      - **Don't gate globally.** Two players in the same server can have different flags. Gate per-player, not with a single experience-wide boolean.
      - **Combine with `LocalizationService:GetCountryRegionForPlayerAsync`** when you need the actual country/region code (a string) for finer-grained logic. `PolicyService` tells you the *restrictions*, `LocalizationService` tells you the *where*.
      - **Don't trust the client for grant decisions.** If the client reads policy flags and the server grants based on a client Remote saying "ads are allowed for me," that's exploitable. Re-query on the server for any decision that affects economy or access.
      - **`AllowedExternalLinkReferences` is legacy and always empty.** Do not build logic on it.
      - **China (`IsSubjectToChinaPolicies`)** requires experience-specific compliance changes: see the Roblox China program documentation before relying on this flag.
      
      ## When to use what
      
      | Need | Method | Side |
      | --- | --- | --- |
      | "Can this player see paid random items / trade paid items / share content / see ads / buy subscriptions / buy commerce products?" | `GetPolicyInfoForPlayerAsync` | Server (or client for LocalPlayer) |
      | "Can this player see this brand's assets?" | `CanViewBrandProjectAsync` | Server only |
      | "What country/region is this player in?" | `LocalizationService:GetCountryRegionForPlayerAsync` | Either |
      
      ## Sources
      
      - https://create.roblox.com/docs/en-us/reference/engine/classes/PolicyService
      - https://create.roblox.com/docs/en-us/production/monetization/virtual-items (Paid Random Items policy)
      - https://create.roblox.com/docs/en-us/production/monetization/subscriptions (`IsEligibleToPurchaseSubscription`)
      - https://create.roblox.com/docs/en-us/production/monetization/commerce-products (`IsEligibleToPurchaseCommerceProduct`)
      - https://devforum.roblox.com/t/new-programs-available-roblox-china-licensed-to-operate/1023361 (China policies)
      
    • purchase-flow-and-granting.md 6.4 KB
      ---
      read_when: "Implement pass purchase prompts, ownership checks, and server grants"
      last_reviewed: 2026-06-17
      ---
      
      # Purchase flow and server granting
      
      ## Full recommended flow
      
      ### Client side (prompting and UI state)
      
      ```lua
      local MarketplaceService = game:GetService("MarketplaceService")
      local Players = game:GetService("Players")
      
      local player = Players.LocalPlayer
      local PASS_ID = 1234567890  -- Replace with your pass ID
      
      local function getProductInfoAsync()
          local success, productInfo = pcall(function()
              return MarketplaceService:GetProductInfoAsync(PASS_ID, Enum.InfoType.GamePass)
          end)
          if success then
              return productInfo
          end
          warn("Failed to get product info:", productInfo)
          return nil
      end
      
      local function updatePassButton(button)
          local success, owns = pcall(function()
              return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
          end)
          
          if success then
              if owns then
                  button.Text = "Owned"
                  button.Active = false
              else
                  button.Text = "Buy Pass"
                  button.Active = true
              end
          else
              button.Text = "Check Failed"
          end
      end
      
      -- Example button connection
      local buyButton = script.Parent
      buyButton.Activated:Connect(function()
          local success, owns = pcall(function()
              return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
          end)
          
          if success and not owns then
              local promptOk, promptErr = pcall(function()
                  MarketplaceService:PromptGamePassPurchase(player, PASS_ID)
              end)
              if not promptOk then
                  warn("Failed to prompt purchase:", promptErr)
              end
          elseif success and owns then
              -- Already owns
          else
              warn("Ownership check failed")
          end
      end)
      
      -- Optional: refresh state after purchase finished (via Remote or on re-join)
      ```
      
      ### Server side (authoritative granting)
      
      The critical part. **Important:** `PromptGamePassPurchaseFinished` fires on both the client and the server. You must grant benefits only in the server handler; the client copy is for UI updates only.
      
      ```lua
      local MarketplaceService = game:GetService("MarketplaceService")
      local Players = game:GetService("Players")
      
      local PASS_ID = 1234567890  -- Replace with your pass ID
      local granted = {} -- idempotency guard: granted[player][passID]
      
      local function grantPassBenefits(player: Player, passID: number)
          if not player or not player:IsDescendantOf(Players) then
              return
          end
          local playerGranted = granted[player]
          if not playerGranted then
              playerGranted = {}
              granted[player] = playerGranted
          end
          if playerGranted[passID] then
              return
          end
          playerGranted[passID] = true
      
          -- Apply the benefit. This must be server-only.
          -- Examples:
          -- player:SetAttribute("VIP", true)
          -- YourDataManager:GrantPassPerks(player, PASS_ID)
          -- Fire a RemoteEvent to client for visual unlock (but don't trust client to apply logic)
      
          -- Persist if needed (cross-ref roblox-datastores skill)
      end
      
      -- Handle new purchases
      MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, purchasedID: number, wasPurchased: boolean)
          if not player or typeof(purchasedID) ~= "number" then
              return
          end
          if wasPurchased and purchasedID == PASS_ID then
              print(player.Name .. " successfully purchased the pass.")
      
              local ok, err = pcall(function()
                  grantPassBenefits(player, PASS_ID)
              end)
              if not ok then
                  warn("Failed to grant pass benefits:", err)
              end
          end
      end)
      
      -- Re-grant on every join (in case of data loss, new server, etc.)
      Players.PlayerAdded:Connect(function(player: Player)
          local success, ownsPass = pcall(function()
              return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
          end)
      
          if success and ownsPass then
              local ok, err = pcall(function()
                  grantPassBenefits(player, PASS_ID)
              end)
              if not ok then
                  warn("Failed to apply owned pass benefits for", player.Name, err)
              end
          elseif not success then
              warn("Gamepass ownership check failed for", player.Name)
              -- Optionally retry later or fall back to cached data
          end
      end)
      
      Players.PlayerRemoving:Connect(function(player: Player)
          granted[player] = nil
      end)
      ```
      
      ## Key rules for the flow
      
      - `PromptGamePassPurchase` and initial `UserOwnsGamePassAsync` checks can be on the client for UX.
      - All actual granting of power/economy/items **must** happen on the server in the `PromptGamePassPurchaseFinished` event or the `PlayerAdded` re-check.
      - `PromptGamePassPurchaseFinished` fires on the client too; never grant benefits from the client copy.
      - Always wrap ownership and prompt calls in `pcall`.
      - `UserOwnsGamePassAsync` will return true for a freshly purchased pass when the player joins a new server after the purchase.
      - Do not rely solely on the purchase-finished event for players who bought the pass while offline or on another server.
      - Use an idempotency guard so benefits are not granted multiple times if the event fires more than once.
      - For developer products, `MarketplaceService.ProcessReceipt` can only be assigned **once** globally. The callback must return `Enum.ProductPurchaseDecision.PurchaseGranted` after successful fulfillment, or `Enum.ProductPurchaseDecision.NotProcessedYet` if fulfillment fails, because Roblox may redeliver the receipt until `PurchaseGranted` is returned.
      
      ## Handling purchase failures / edge cases
      
      - Network issues during prompt: the `PromptGamePassPurchaseFinished` may fire with `wasPurchased = false`.
      - Player cancels the prompt.
      - Insufficient Robux.
      - The pass is no longer for sale.
      
      In the finished handler, only act on `wasPurchased == true`.
      
      For UI, you can listen for the finished event on the client too (via a Remote from server) to update "Owned" state immediately without waiting for re-join.
      
      ## Integration with data stores
      
      After granting in the server handler, immediately save the fact that this player owns the pass (or the specific perks) using your data persistence system.
      
      On load (PlayerAdded), prefer the `UserOwnsGamePassAsync` result as the source of truth, then merge with any local saved state.
      
      See roblox-datastores skill for safe profile loading patterns.
      
      ## Scripts Folder example
      
      A simple client helper can live in scripts/PassPurchaseHelper.lua (adapt and require from your UI modules).
      
    • rules-policies-and-security.md 4.8 KB
      ---
      read_when: "Review monetization restrictions, paid random items, or purchase security"
      last_reviewed: 2026-10-17
      ---
      
      # Rules, policies, and security for game passes
      
      ## Policy changes in 2026
      
      As of **May 30, 2026**, cross-experience game pass and developer product sales are disabled. You can no longer sell a pass or dev product from Experience A inside Experience B. Sales on an experience's own details page (EDP) remain available for passes and developer products owned by that experience.
      
      If your game previously relied on cross-experience pass sales (common in donation/tipping games), migrate to experience-specific passes and/or the [Robux Transfers API](https://create.roblox.com/docs/en-us/production/monetization/robux-transfers). `MarketplaceService:PromptRobuxTransferAsync` must be called from the server. Only **Roblox Plus** subscribers can initiate transfers, transfer amounts must be between **10 and 500 Robux**, and the sender cannot equal the receiver. The recipient receives **90%** and the experience earns the other **10%**; Roblox takes no transfer fee. A `BindReceiptHandler` callback must process transfer receipts. Roblox permits items or perks after a successful transfer receipt. Avoiding quid-pro-quo rewards can still be a useful anti-abuse or game-design choice, but it is not a platform requirement.
      
      ## What game passes can and cannot do
      
      Allowed:
      - Permanent access (VIP areas, servers)
      - Permanent unlocks (items, classes, slots)
      - Cosmetic or quality-of-life upgrades
      
      Restricted / Policy-sensitive:
      - Randomized virtual items / loot boxes (must follow Paid Random Items policy)
      - Anything that could be seen as gambling without proper disclosures
      
      Passes must comply with Roblox Community Standards.
      
      ### Paid Random Items policy check
      
      Before offering any paid randomized virtual item (loot box, gacha, random crate, etc.), query `PolicyService:GetPolicyInfoForPlayerAsync(player)` and check:
      
      - `ArePaidRandomItemsRestricted`: if true for this user, do not offer paid random items.
      - `IsPaidItemTradingAllowed`: if false, do not allow paid item trading.
      
      Respecting these flags per-player is required by Roblox policy.
      
      ## Capability requirements
      
      Do not apply a blanket "API Services required" rule to all `MarketplaceService` purchase APIs. Publish the experience where the relevant product or transfer API requires it, and check that API's current Engine Reference requirements. Enable **Studio Access to API Services** only for APIs that require it and only in a dedicated test experience.
      
      ## Security rules (critical)
      
      1. **Prompt on client only.** Never call `PromptGamePassPurchase` from the server in response to untrusted client input.
      2. **Grant on server only.** The `PromptGamePassPurchaseFinished` handler (and `PlayerAdded` re-check) are the only places that should mutate player state based on pass ownership.
      3. **Always re-verify.** Even after a successful purchase event, call `UserOwnsGamePassAsync` on the next join. Do not cache the "just purchased" state forever.
      4. **pcall everything.** Marketplace calls can fail due to network, throttling, or player actions.
      5. **Do not trust client signals.** A RemoteEvent saying "I just bought the pass" must be ignored for granting purposes.
      
      ## Data persistence integration
      
      When a player owns a pass, you typically want to:
      - Apply runtime benefits (attributes, speed multipliers, access flags)
      - Persist the ownership or derived perks in your DataStore profile
      
      Recommended: Treat `UserOwnsGamePassAsync` as the source of truth on load. Use your DataStore only for additional pass-related custom data (e.g. "unlocked skin variants for this pass").
      
      See roblox-datastores skill for proper loading/saving patterns around this.
      
      ## Right to be forgotten (RTBF)
      
      If you implement automated data deletion for user requests, include any keys that store pass-related custom data.
      
      Use the Data Stores Manager or Open Cloud to inspect/delete when needed.
      
      ## Testing and compliance
      
      - Test the full ownership flow on a test experience.
      - Verify that players who buy the pass while in one server see the benefit when they join a different server.
      - Make sure "Already owns" states are shown correctly so players aren't prompted again.
      - Document what the pass actually gives (in description and in-game UI) to avoid support tickets and policy issues.
      
      ## Common violations to avoid
      
      - Granting benefits based only on a client Remote.
      - Selling the same permanent benefit as both a game pass and a developer product (causes confusion and potential policy problems).
      - Hardcoding Robux prices in UI (breaks regional pricing and optimizations).
      - Using passes for temporary effects that should be developer products.
      
      Treat these as a starting checklist, then re-check current policy, threat-model the experience, and test each purchase/receipt path. This guidance cannot guarantee security or policy compliance.
      
    • subscriptions.md 12 KB
      ---
      read_when: "Create recurring benefits or handle subscription status and renewals"
      last_reviewed: 2026-06-17
      ---
      
      # Subscriptions
      
      **Official source:** https://create.roblox.com/docs/en-us/production/monetization/subscriptions
      
      Subscriptions offer users recurring benefits for a monthly fee. Unlike [passes](purchase-flow-and-granting.md), whose benefits are granted indefinitely, subscription benefits persist only while the user keeps paying. Subscriptions are managed through `MarketplaceService` and the Creator Dashboard.
      
      ## When to use subscriptions vs passes vs developer products
      
      - **Pass**: one-time permanent unlock (VIP, permanent item).
      - **Developer Product**: repeatable/consumable (currency pack, potion, revive).
      - **Subscription**: recurring monthly benefit (monthly cosmetic bundle, ongoing XP boost, VIP-tier perks that should gate while unpaid).
      
      ## Characteristics
      
      - **Auto-renewing**, not one-time. Priced in Robux **or** local currency.
      - **Single-tiered:** multiple subscriptions in the same experience can be owned simultaneously; mutually exclusive "Bronze/Silver/Gold" tiering of the *same* benefit set is **not** supported.
      - **Regional Pricing** is enabled by default for Robux-priced subscriptions and cannot be turned off. Not available for local-currency subscriptions.
      - Up to **50 subscriptions** per experience (active + inactive combined).
      - Subscriptions are **ineligible for cross-selling** by other experiences and for affiliate fees.
      
      ## Robux vs local currency
      
      |  | Robux | Local currency |
      | --- | --- | --- |
      | Eligibility | All creators | Requires ID- or phone-verified account |
      | Platforms | All platforms | Web, App Store, Google Play |
      | Countries | All Roblox-supported | Excludes Argentina, China, Colombia, India, Indonesia, Japan, Russia, Taiwan, Türkiye, UAE, Ukraine, Vietnam |
      | Price | Any amount ≥ 49 Robux | One of $2.99 / $4.99 / $7.99 / $9.99 / $14.99 |
      | Regional Pricing | Enabled by default | Unavailable |
      | Payout | 70% of subscription value each month | 70% first month, 100% thereafter |
      | Refunds | Not eligible | Eligible within the 30-day hold window |
      
      Local-currency earnings follow a 30-day hold; Robux-priced earnings follow the standard ~5-day hold (same as passes/products).
      
      ## Product types
      
      When creating, choose one:
      - **Durable**: permanent items that persist after acquisition (e.g. a weapon). If a bundle mixes durable + consumable, choose Durable.
      - **Consumable**: temporary, re-purchasable, expires after use (e.g. a potion that grants a temporary boost).
      - **Currency**: an in-experience medium of exchange.
      
      You **cannot** change the product type after creation. The price of a Robux subscription can be changed only **once every 60 days**, and price increases require Roblox to give users ≥30 days' notice. Local-currency subscription prices cannot be changed. Delete and recreate to change price.
      
      ## Creating and activating
      
      1. Creator Dashboard → your experience → Monetization → Subscriptions → Create Subscription.
      2. Upload cover image, unique name, clear description.
      3. Pick payment option (Robux ≥49, or one of the local-currency tiers).
      4. Pick product type (Durable / Consumable / Currency).
      5. Create.
      6. To put it up for sale: ⋮ → Activate. Active subscriptions appear on the experience's Store tab.
      
      Before first activation you must confirm a **shortened experience name**. This is permanent and cannot be changed, and it appears alongside the subscription name at purchase time. It does **not** change your experience's name on Roblox.
      
      ## Subscription states
      
      - **Active**: available for sale; subscribers can renew at the start of the next period.
      - **Inactive**: unavailable for sale.
      
      To take off sale: ⋯ → Take Off Sale. You can either let existing subscribers renew, or cancel future renewals. If you're not removing the benefits permanently, let subscribers renew.
      
      ## Deleting
      
      Deleting an active subscription triggers **full refunds for active subscribers** and **zero Robux for you**. Prefer: take off sale → cancel renewals → wait out the period → then delete. Deleting a local-currency subscription requires refunding all current subscribers (Robux subscriptions are not refundable). Deletion requires the last four digits of the subscription ID for confirmation.
      
      ## API surface
      
      Subscription IDs are **strings** like `"EXP-11111111"`, not numbers.
      
      | Method | Side | Purpose |
      | --- | --- | --- |
      | `MarketplaceService:GetUserSubscriptionStatusAsync(player, subscriptionId)` | Server | Returns `{IsSubscribed: boolean}` (plus internal fields). **Server-only.** |
      | `MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)` | Client | Prompts the user to purchase. |
      | `MarketplaceService.PromptSubscriptionPurchaseFinished` | Both | `(player, subscriptionId, didTryPurchasing)`. Fires after the prompt closes. |
      | `Players.UserSubscriptionStatusChanged` | Server | `(player, subscriptionId)`. Fires on purchase, renewal, cancellation. **Server-only.** |
      | `MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId)` | Server | Returns product info, including whether priced in Robux or local currency. |
      | `MarketplaceService:GetUserSubscriptionPaymentHistoryAsync(player, subscriptionId)` | Server | Returns the user's payment history for the subscription. |
      
      ### Checking status (server)
      
      ```lua
      local MarketplaceService = game:GetService("MarketplaceService")
      local Players = game:GetService("Players")
      
      local SUBSCRIPTION_ID = "EXP-11111111" -- Replace with your subscription ID
      
      local function grantAward(player: Player)
          -- Grant the subscription benefit here (server-only, source of truth)
      end
      
      local function revokeAwardIfGranted(player: Player)
          -- Called for players who do NOT have the subscription.
          -- If you persist subscription state to DataStores, undo it here.
      end
      
      local function checkSubStatus(player: Player)
          local success, response = pcall(function()
              return MarketplaceService:GetUserSubscriptionStatusAsync(player, SUBSCRIPTION_ID)
          end)
          if not success then
              warn(`Error while checking subscription: {response}`)
              return
          end
          if response.IsSubscribed then
              grantAward(player)
          else
              revokeAwardIfGranted(player)
          end
      end
      
      local function onUserSubscriptionStatusChanged(player: Player, subscriptionId: string)
          if subscriptionId == SUBSCRIPTION_ID then
              checkSubStatus(player)
          end
      end
      
      Players.PlayerAdded:Connect(checkSubStatus)
      Players.UserSubscriptionStatusChanged:Connect(onUserSubscriptionStatusChanged)
      ```
      
      ### Prompting a purchase (client)
      
      `PromptSubscriptionPurchaseFinished` fires with `didTryPurchasing` (note: this indicates the user *attempted* to purchase, not that it succeeded). Subscription registration can take time, so re-check status ~10 seconds after the prompt closes.
      
      ```lua
      local MarketplaceService = game:GetService("MarketplaceService")
      local Players = game:GetService("Players")
      
      local SUBSCRIPTION_ID = "EXP-11111111"
      local purchaseButton = script.Parent.PromptPurchaseSubscription -- your button
      
      local function playerHasSubscription()
          local success, result = pcall(function()
              return MarketplaceService:GetUserSubscriptionStatusAsync(Players.LocalPlayer, SUBSCRIPTION_ID)
          end)
          if not success then return false end
          return result.IsSubscribed
      end
      
      local function hideButtonIfPlayerHasSubscription()
          if playerHasSubscription() then
              purchaseButton.Visible = false
          end
      end
      
      local function onPromptSubscriptionPurchaseFinished(player: Player, subscriptionId: string, didTryPurchasing: boolean)
          if didTryPurchasing then
              task.delay(10, hideButtonIfPlayerHasSubscription)
          end
      end
      
      hideButtonIfPlayerHasSubscription()
      
      purchaseButton.Activated:Connect(function()
          MarketplaceService:PromptSubscriptionPurchase(Players.LocalPlayer, SUBSCRIPTION_ID)
          hideButtonIfPlayerHasSubscription()
      end)
      
      MarketplaceService.PromptSubscriptionPurchaseFinished:Connect(onPromptSubscriptionPurchaseFinished)
      ```
      
      ### Secure client→server status fetch (RemoteFunction)
      
      `GetUserSubscriptionStatusAsync` is server-only. To let the client query status, expose a `RemoteFunction`:
      
      ```lua
      -- Server
      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      local MarketplaceService = game:GetService("MarketplaceService")
      
      local getSubscriptionStatusRemote = Instance.new("RemoteFunction")
      getSubscriptionStatusRemote.Name = "GetSubscriptionStatus"
      getSubscriptionStatusRemote.Parent = ReplicatedStorage
      
      getSubscriptionStatusRemote.OnServerInvoke = function(player: Player, subscriptionId: string)
          assert(typeof(subscriptionId) == "string")
          return MarketplaceService:GetUserSubscriptionStatusAsync(player, subscriptionId)
      end
      ```
      
      ```lua
      -- Client
      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      local getSubscriptionStatusRemote = ReplicatedStorage:WaitForChild("GetSubscriptionStatus")
      
      local function playerHasSubscription(subscriptionId: string)
          local success, result = pcall(function()
              return getSubscriptionStatusRemote:InvokeServer(subscriptionId)
          end)
          if not success then return false end
          return result.IsSubscribed
      end
      ```
      
      ## Replacing a pass with a subscription
      
      When migrating, existing pass holders must keep the benefit they paid for; take the pass off sale so new users buy the subscription. Subscription benefits can be revoked (pass benefits cannot), so if you previously persisted pass benefits to a DataStore you must "undo" them when the subscription lapses. Listen for both `PromptGamePassPurchaseFinished` (legacy) and `UserSubscriptionStatusChanged` (new).
      
      ## Security rules (critical)
      
      - **Prompt on client, check status on server.** `GetUserSubscriptionStatusAsync` is server-only by design.
      - **Grant/revoke on server only**, in `UserSubscriptionStatusChanged` and the `PlayerAdded` re-check.
      - **Always pcall** `GetUserSubscriptionStatusAsync`, `PromptSubscriptionPurchase`, and the remote fetch.
      - **Re-check on every join.** Don't trust a cached "subscribed" state forever; subscriptions lapse.
      - **Persist nothing sensitive on the client.** Use your DataStore profile (see roblox-datastores) for any subscription-derived state.
      - **Respect region/platform eligibility.** Only offer subscriptions in supported regions and platforms. `PolicyService:IsEligibleToPurchaseSubscription` tells you whether the player can buy (see roblox-gamepasses PolicyService reference).
      - **Idempotency guard** your grant/revoke so duplicate `UserSubscriptionStatusChanged` events don't double-apply.
      
      ## Analytics
      
      Creator Dashboard → Monetization → Subscriptions → Analytics tab tracks:
      - **Subscriptions** (total active), **Estimated revenue** (net of fees).
      - **Subscriber breakdown**: New / Renewed / Resurrected (previously canceled).
      - **Cancellations** (not the same as refunds; canceled = won't renew but paid in full for the cycle).
      - **Subscriptions by platform** and **Platform earnings**.
      
      Real-time subscription events (cancelled, purchased, refunded, renewed) are also available via **Open Cloud webhooks** (see roblox-open-cloud skill).
      
      ## Gotchas
      
      - `PromptSubscriptionPurchaseFinished`'s `didTryPurchasing` is **not** a success signal. Re-check status after a delay.
      - Subscription registration can lag; a 10-second delay before re-checking is the documented pattern.
      - Local-currency refunds within the hold window cancel the payout; outside the window they deduct from your Robux balance (and from the group owner's balance if the group can't cover it).
      - Changing a Robux subscription's price is rate-limited to once per 60 days; local-currency prices are immutable.
      - Shortened experience name is permanent: set it carefully.
      - Subscriptions don't support cross-experience selling or affiliate fees.
      
      ## Sources
      
      - https://create.roblox.com/docs/en-us/production/monetization/subscriptions
      - https://create.roblox.com/docs/en-us/reference/engine/classes/MarketplaceService (subscription methods)
      - https://create.roblox.com/docs/en-us/reference/engine/classes/Players (`UserSubscriptionStatusChanged`)
      - https://create.roblox.com/docs/en-us/cloud/webhooks/webhook-notifications (subscription webhook events)
      
  • scripts
    • PassPurchaseHelper.lua 6.8 KB · in bundle
  • SKILL.md 12.7 KB
    ---
    name: roblox-gamepasses
    description: "Implement Roblox game passes, developer products, subscriptions, and Robux transfers. Use for purchase prompts, server-side fulfillment, ownership checks, recurring benefits, and monetization policy."
    last_reviewed: 2026-07-16
    ---
    
    # roblox-gamepasses
    
    **Primary official source:** https://create.roblox.com/docs/en-us/production/monetization/passes (plus developer-products.md for contrast, and the MarketplaceService class reference).
    
    Grant benefits on the server after verifying the purchase or ownership. Keep purchase prompts and shop display on the client.
    
    See roblox-datastores for how to store "player owns this pass" state or associated perks, and roblox-networking for the Remote validation layer around purchase prompts.
    
    ## Game passes and developer products
    
    - **Game Pass**: One-time purchase. Permanent privilege for that specific experience (VIP access, permanent item, extra slot, cosmetic unlock, etc.). Roblox tracks ownership per user per experience.
    - **Developer Product**: Repeatable / consumable (currency packs, potions, revives, temporary boosts). Can be bought many times. Requires ProcessReceipt callback on the server for fulfillment.
    - As of **May 30, 2026**, cross-experience game pass and developer product sales are disabled. Design experience-specific passes or use the [Robux Transfers API](https://create.roblox.com/docs/en-us/production/monetization/robux-transfers) for donation-style flows. `MarketplaceService:PromptRobuxTransferAsync` must be called from the server. Only **Roblox Plus** subscribers can initiate transfers, transfer amounts must be between **10 and 500 Robux**, and the sender cannot equal the receiver. The recipient receives **90%** and the experience earns the other **10%**; Roblox takes no transfer fee. A `BindReceiptHandler` callback must process transfer receipts. Roblox permits an experience to grant items or perks after a successful receipt; choosing not to attach rewards is an opinionated anti-abuse/design recommendation, not a platform rule.
    - You (the creator) are 100% responsible for actually delivering the benefit. Roblox only handles the transaction and the UserOwnsGamePassAsync query.
    - Passes can be used for randomized virtual items only if you follow the Paid Random Items policy.
    
    ## Creation and asset ID
    
    1. Creator Dashboard → your published experience → Monetization → Passes → Create a Pass.
    2. Upload circular-friendly icon (≤512×512, jpg/png/bmp; important content must survive circular crop).
    3. Name + description.
    4. After creation: hover the pass → ⋯ → Copy Asset ID. This number is the passID you use in all scripts.
    
    For external sales on the game page Store tab: go to the pass → Sales → enable "Item for Sale" and set Robux price (1 to 1B).
    
    ## Prompt purchases and grant benefits
    
    **Client side (LocalScript or UI module; only for prompting and optimistic display):**
    - Call MarketplaceService:UserOwnsGamePassAsync (pcall) to decide "Buy" vs "Owned" button state.
    - If not owned, call MarketplaceService:PromptGamePassPurchase(player, passID).
    
    **Server side (Script in ServerScriptService; the only place that grants benefits):**
    ```lua
    local MarketplaceService = game:GetService("MarketplaceService")
    local Players = game:GetService("Players")
    
    local PASS_ID = 1234567890
    local granted = {} -- idempotency guard: granted[player][passID]
    
    local function grantPassBenefits(player: Player, passID: number)
        if not player or not player:IsDescendantOf(Players) then
            return
        end
        local playerGranted = granted[player]
        if not playerGranted then
            playerGranted = {}
            granted[player] = playerGranted
        end
        if playerGranted[passID] then
            return
        end
        playerGranted[passID] = true
    
        -- Apply the actual privilege (attributes, table entry, DataStore flag, Remote to client for visuals, etc.)
        -- This is the source of truth.
    end
    
    MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, purchasedPassID: number, wasPurchased: boolean)
        -- This event also fires on the client; never grant benefits from the client copy.
        if not player or typeof(purchasedPassID) ~= "number" then
            return
        end
        if wasPurchased and purchasedPassID == PASS_ID then
            local ok, err = pcall(function()
                grantPassBenefits(player, PASS_ID)
            end)
            if not ok then
                warn("Failed to grant pass benefits:", err)
            end
        end
    end)
    
    Players.PlayerAdded:Connect(function(player: Player)
        local success, owns = pcall(function()
            return MarketplaceService:UserOwnsGamePassAsync(player.UserId, PASS_ID)
        end)
        if success and owns then
            local ok, err = pcall(function()
                grantPassBenefits(player, PASS_ID)
            end)
            if not ok then
                warn("Failed to apply owned pass benefits:", err)
            end
        elseif not success then
            warn("UserOwnsGamePassAsync failed for", player.Name)
        end
    end)
    
    Players.PlayerRemoving:Connect(function(player: Player)
        granted[player] = nil
    end)
    ```
    
    **GetProductInfoAsync for dynamic UI (price, name, description, IsForSale):**
    Use `MarketplaceService:GetProductInfoAsync(id, Enum.InfoType.GamePass)`. Do this on the client for display, but never grant based on the result. The non-async counterpart `GetProductInfo` still exists, but `GetProductInfoAsync` is preferred.
    
    ## Capability requirements
    
    Game passes and developer products require a published experience. Enable **Studio Access to API Services** only when a specific API used by a dedicated test experience requires it; do not treat that Studio setting as a blanket prerequisite for every `MarketplaceService` purchase API. Verify each API's current requirements in its Engine Reference entry.
    
    ## Product recommendations
    
    - `MarketplaceService:RankProductsAsync(arrayOfIdentifiers)`: pass a table of up to 50 `{InfoType = Enum.InfoType.GamePass, Id = ...}`. Returns a personalized ranking for the current user as `{ProductIdentifier, ProductInfo}` items. Use sparingly; call once at join.
    - `MarketplaceService:RecommendTopProductsAsync({Enum.InfoType.GamePass, Enum.InfoType.Product})`: returns up to 50 recommended products the user is likely to engage with. Results usually exclude already-owned items, but verify in your UI. Use sparingly; call once at join.
    
    Use these results in a "Recommended for you" section of your shop. Measure whether they improve conversion in your experience.
    
    ## Promotions (buy robux page bonus pool)
    
    You can opt passes into the promotion pool so that users buying Robux packages may receive the pass for free (contextually relevant to their history).
    
    Requirements (from the passes doc):
    - Unique pass recommended.
    - If on sale, price between 50 and 800 Robux.
    - Must have thumbnail.
    - Must comply with Community Standards.
    - Cannot be a paid random item.
    
    Opt-in via the pass's Promotions tab in the dashboard.
    
    ## Analytics and iteration
    
    In Creator Dashboard → experience → Monetization → Passes → Analytics tab you get:
    - Top passes by sales and net Robux.
    - Time-series graphs.
    - Attribution for passes acquired via Buy Robux promotions and subsequent joins.
    
    Use this data to decide pricing, which perks are compelling, and when to run promotions.
    
    ## Security, data, and policy constraints
    
    - Prompt only from client. Grant and record only on the server in the PromptGamePassPurchaseFinished handler or on PlayerAdded re-check.
    - Always pcall `UserOwnsGamePassAsync` and `Prompt...` calls.
    - Re-check ownership on every relevant join/session start before granting gameplay or economy perks.
    - Store your own record of ownership + associated state in DataStores if you need history or custom metadata (Roblox does not expose full per-user pass purchase history via the Engine API).
    - For RTBF / right-to-be-forgotten, include pass-related keys in your deletion patterns (see roblox-datastores skill).
    - Never hardcode Robux prices in UI that the player sees: use `GetProductInfoAsync` so regional pricing and optimizations work.
    - Test purchases only on dedicated test experiences.
    - For paid random items (loot boxes / gacha passes), use `PolicyService` first: check `PolicyService:GetPolicyInfoForPlayerAsync(player).ArePaidRandomItemsRestricted` and `IsPaidItemTradingAllowed` before offering randomized paid content.
    - For developer products, `MarketplaceService.ProcessReceipt` can only be assigned **once** globally; assign it once in a single server script. The callback must return `Enum.ProductPurchaseDecision.PurchaseGranted` after successful fulfillment, or `Enum.ProductPurchaseDecision.NotProcessedYet` if fulfillment fails, because Roblox may redeliver the receipt until `PurchaseGranted` is returned.
    - Donation / tipping games using `PromptRobuxTransferAsync` are high-risk for abuse; implement rate limits, alt-account detection, moderation pipelines, and avoid granting in-experience advantages in exchange for transfers.
    
    ## Subscriptions (recurring benefits)
    
    Subscriptions offer users recurring benefits for a monthly fee, auto-renewing in Robux or local currency. Unlike passes (permanent), subscription benefits persist only while the user keeps paying. Up to 50 per experience; single-tiered (no mutually exclusive Bronze/Silver/Gold); regional pricing enabled by default for Robux-priced subs.
    
    **API surface** (subscription IDs are **strings** like `"EXP-11111111"`):
    - `MarketplaceService:GetUserSubscriptionStatusAsync(player, subscriptionId)`: **server-only**, returns `{IsSubscribed: boolean}`.
    - `MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)`: client prompt.
    - `MarketplaceService.PromptSubscriptionPurchaseFinished(player, subscriptionId, didTryPurchasing)`: note `didTryPurchasing` is an *attempt* signal, not success; re-check status after a delay.
    - `Players.UserSubscriptionStatusChanged(player, subscriptionId)`: **server-only**, fires on purchase/renewal/cancellation.
    - `MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId)` and `GetUserSubscriptionPaymentHistoryAsync(player, subscriptionId)`.
    
    **Security (same posture as passes):** prompt on client, check status and grant/revoke on server only, pcall everything, re-check on every join (subscriptions lapse), respect `PolicyService:IsEligibleToPurchaseSubscription` per player, persist nothing sensitive on the client.
    
    **Payouts:** Robux-priced subs pay 70% each month. Local-currency subs pay 70% first month, 100% thereafter, with a 30-day hold. Robux subs are not refundable; local-currency subs are refundable within the hold window.
    
    See [references/subscriptions.md](references/subscriptions.md) for the complete flow, client/server code, migration from passes, and gotchas.
    
    ## Creator Rewards (replaces Premium Payouts)
    
    As of **July 24, 2025**, Engagement-Based Payouts (formerly "Premium Payouts") and the Creator Affiliate program were **discontinued and replaced by Creator Rewards**. There is no longer a per-Premium-play-minute payout to integrate against.
    
    Creator Rewards is a platform program with two payment types. It requires no in-experience integration:
    - **Daily Engagement Rewards**: 5 Robux per day per Active Spender who spends 10+ minutes in your experience, provided it's one of the first three experiences they visit that day.
    - **Audience Expansion Rewards**: 35% revenue share on a new/reactivated user's first $100 of qualifying purchases in their first 60 days, attributed via Share Links, direct experience links, or experience-name search.
    
    Official source: https://create.roblox.com/docs/en-us/creator-rewards
    
    ## When to use game passes vs other monetization
    
    - One-time permanent unlock or access → Game Pass.
    - Repeatable purchase (currency, consumables, temporary power) → Developer Product (with proper ProcessReceipt).
    - Recurring monthly benefit → Subscription (see references/subscriptions.md; `MarketplaceService:GetUserSubscriptionStatusAsync` etc.).
    
    See the developer-products doc for the repeatable flow.
    
    ## Scripts
    
    - `scripts/PassPurchaseHelper.lua`: a client-side helper for game pass button state, price display, and prompting.
    
    
    <!-- catalog:references:start -->
    ## Reference index
    
    - [creation-and-setup.md](references/creation-and-setup.md): Create a pass, find its asset ID, or configure sales and test access.
    - [policyservice.md](references/policyservice.md): Gate a feature using per-player monetization or content restrictions.
    - [purchase-flow-and-granting.md](references/purchase-flow-and-granting.md): Implement pass purchase prompts, ownership checks, and server grants.
    - [rules-policies-and-security.md](references/rules-policies-and-security.md): Review monetization restrictions, paid random items, or purchase security.
    - [subscriptions.md](references/subscriptions.md): Create recurring benefits or handle subscription status and renewals.
    <!-- catalog:references:end -->
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related