
Roblox Gamepasses
- 53 installs
- 11 repo stars
- Updated August 3, 2026
- nonlooped/roblox-suite
Rule-accurate Roblox monetization: game passes, developer products via ProcessReceipt, and subscriptions with server-authoritative granting and PolicyService gating.
About
Covers Roblox monetization including game passes, developer products (ProcessReceipt), and subscriptions, with server-authoritative granting on PlayerAdded and purchase completion plus PolicyService gating. A developer uses it for any in-game purchase, perk, or recurring benefit.
- Server-authoritative granting via ProcessReceipt and PlayerAdded
- PolicyService gating for paid random items and subscription eligibility
Roblox Gamepasses by the numbers
- 53 all-time installs (skills.sh)
- +15 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #159 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nonlooped/roblox-suite --skill roblox-gamepassesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 53 |
|---|---|
| repo stars | ★ 11 |
| Last updated | August 3, 2026 |
| Repository | nonlooped/roblox-suite ↗ |
What it does
Rule-accurate Roblox monetization: game passes, developer products via ProcessReceipt, and subscriptions with server-authoritative granting and PolicyService gating.
Files
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).
This skill focuses on getting game passes right the first time — correct ownership checking, server-only fulfillment, proper error handling, and modern personalization features that most older tutorials ignore.
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 Pass vs Developer Product (rules that matter)
- 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:PromptRobuxTransferAsyncmust be called from the server. Only Roblox Plus subscribers can initiate transfers, amounts are clamped to 10–500 Robux per transaction, the sender cannot equal the receiver, Roblox takes a 10% platform fee, and the recipient receives 90%. ABindReceiptHandlercallback must process transfer receipts; donations are high-risk for abuse and should include anti-abuse checks (rate limits, alt detection, moderation, no quid-pro-quo rewards). - 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 & 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).
The Authoritative Purchase + Grant Flow (Inside Experience)
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
All MarketplaceService purchase APIs (PromptGamePassPurchase, PromptPurchase, developer-product ProcessReceipt, PromptRobuxTransferAsync, etc.) require API Services to be enabled for the place (Home tab → Game Settings → Security → Enable Studio Access to API Services for Studio testing; live places use the deployed configuration). Game passes and developer products also require the experience to be published.
Personalization & Recommendations (use these)
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.
Surface these in "Recommended for you" or "Top picks" sections of your in-experience shop. This measurably improves conversion.
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 & 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 Gotchas (non-negotiable)
- Prompt only from client. Grant and record only on the server in the PromptGamePassPurchaseFinished handler or on PlayerAdded re-check.
- Always pcall
UserOwnsGamePassAsyncandPrompt...calls. - Re-check ownership on every relevant join/session start before granting powerful or economy-affecting 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
GetProductInfoAsyncso regional pricing and optimizations work. - Test purchases only on dedicated test experiences.
- For paid random items (loot boxes / gacha passes), use
PolicyServicefirst: checkPolicyService:GetPolicyInfoForPlayerAsync(player).ArePaidRandomItemsRestrictedandIsPaidItemTradingAllowedbefore offering randomized paid content. - For developer products,
MarketplaceService.ProcessReceiptcan only be assigned once globally; assign it once in a single server script. The callback must returnEnum.ProductPurchaseDecision.PurchaseGrantedafter successful fulfillment, orEnum.ProductPurchaseDecision.NotProcessedYetif fulfillment fails, because Roblox may redeliver the receipt untilPurchaseGrantedis returned. - Donation / tipping games using
PromptRobuxTransferAsyncare 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)— notedidTryPurchasingis 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)andGetUserSubscriptionPaymentHistoryAsync(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 pays creators in two ways (no in-experience integration required — it's a platform-side program, but you should know it exists):
- 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:GetUserSubscriptionStatusAsyncetc.).
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.
This skill + roblox-datastores + roblox-networking gives you a complete, secure, modern game pass implementation that follows current rules and best practices.
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-asyncGetProductInfostill exists butGetProductInfoAsyncis 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
- Enable Game Settings → Security → Enable Studio Access to API Services only on a dedicated test place; never enable it on a live production place.
- 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
UserOwnsGamePassAsyncreturns true after purchase on new servers. - Test re-join behavior.
Initial Script Skeleton (before full flow)
Place in ServerScriptService:
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 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. If false, the player cannot purchase commerce products in-experience. |
IsEligibleToPurchaseSubscription | boolean | Subscriptions (see 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). |
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). 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)
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
endGate subscription offers (server-side, per join)
-- 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)
-- 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)-- 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
GetPolicyInfoForPlayerAsynccalls 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.
GetPolicyInfoForPlayerAsyncis server-safe and client-safe for the local player only;CanViewBrandProjectAsyncis 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 —
PolicyServicetells you the restrictions,LocalizationServicetells 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 Server Granting
Full Recommended Flow
Client Side (prompting and UI state)
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.
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
PromptGamePassPurchaseand initialUserOwnsGamePassAsyncchecks can be on the client for UX.- All actual granting of power/economy/items must happen on the server in the
PromptGamePassPurchaseFinishedevent or thePlayerAddedre-check. PromptGamePassPurchaseFinishedfires on the client too; never grant benefits from the client copy.- Always wrap ownership and prompt calls in
pcall. UserOwnsGamePassAsyncwill 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.ProcessReceiptcan only be assigned once globally. The callback must returnEnum.ProductPurchaseDecision.PurchaseGrantedafter successful fulfillment, orEnum.ProductPurchaseDecision.NotProcessedYetif fulfillment fails, because Roblox may redeliver the receipt untilPurchaseGrantedis returned.
Handling Purchase Failures / Edge Cases
- Network issues during prompt: the
PromptGamePassPurchaseFinishedmay fire withwasPurchased = 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 for Game Passes
Important Policy Changes (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. MarketplaceService:PromptRobuxTransferAsync must be called from the server. Only Roblox Plus subscribers can initiate transfers, amounts are clamped to 10–500 Robux per transaction, the sender cannot equal the receiver, Roblox takes a 10% platform fee, and the recipient receives 90%. A BindReceiptHandler callback must process transfer receipts; donations are high-risk for abuse and should include anti-abuse checks (rate limits, alt detection, moderation, no quid-pro-quo rewards).
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
All MarketplaceService purchase APIs (PromptGamePassPurchase, PromptPurchase, developer-product ProcessReceipt, PromptRobuxTransferAsync, personalization calls, etc.) require API Services to be enabled for the place. In Studio this is controlled by Game Settings → Security → Enable Studio Access to API Services for local testing; live published places rely on the experience's deployed settings. Game passes, developer products, and transfers also require the experience to be published.
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.
Follow these and the implementation will be both secure and policy-compliant.
Subscriptions
Official source: https://create.roblox.com/docs/en-us/production/monetization/subscriptions
Subscriptions offer users recurring benefits for a monthly fee. Unlike passes, 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 & 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)
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.
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:
-- 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-- 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
endReplacing 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.
GetUserSubscriptionStatusAsyncis server-only by design. - Grant/revoke on server only, in
UserSubscriptionStatusChangedand thePlayerAddedre-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:IsEligibleToPurchaseSubscriptiontells you whether the player can buy (see roblox-gamepasses PolicyService reference). - Idempotency guard your grant/revoke so duplicate
UserSubscriptionStatusChangedevents 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'sdidTryPurchasingis 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)
--!strict
--[[
PassPurchaseHelper.lua
Client-side helper for game pass purchase UI state and prompting.
Usage:
local PassHelper = require(...)
local helper = PassHelper.new(PASS_ID)
helper:connectButton(myBuyButton)
helper:refreshState()
]]
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
type ProductInfo = {
IsForSale: boolean,
PriceInRobux: number?,
Name: string?,
Description: string?,
[string]: any,
}
export type PassPurchaseHelper = {
passID: number,
_player: Player?,
_owns: boolean,
_button: GuiButton?,
_activatedConnection: RBXScriptConnection?,
_promptFinishedConnection: RBXScriptConnection?,
new: (passID: number) -> PassPurchaseHelper,
checkOwnership: (self: PassPurchaseHelper) -> (boolean, boolean?, string?),
promptPurchase: (self: PassPurchaseHelper) -> (boolean, string?),
connectButton: (self: PassPurchaseHelper, button: GuiButton) -> (),
refreshState: (self: PassPurchaseHelper) -> (),
destroy: (self: PassPurchaseHelper) -> (),
}
local PassPurchaseHelper = {}
PassPurchaseHelper.__index = PassPurchaseHelper
function PassPurchaseHelper.new(passID: number): PassPurchaseHelper
if typeof(passID) ~= "number" or passID <= 0 or passID % 1 ~= 0 then
error("PassPurchaseHelper.new expects a positive integer passID, got " .. tostring(passID), 2)
end
local self = setmetatable({}, PassPurchaseHelper) :: PassPurchaseHelper
self.passID = passID
self._player = Players.LocalPlayer
self._owns = false
self._button = nil
self._activatedConnection = nil
self._promptFinishedConnection = nil
self._promptFinishedConnection = MarketplaceService.PromptGamePassPurchaseFinished:Connect(
function(player: Player, purchasedPassID: number, purchaseSuccess: boolean)
if player == self._player and purchaseSuccess and purchasedPassID == self.passID then
self._owns = true
self:_updateButton("Owned", false)
end
end
)
if self._player then
task.spawn(function()
self:_refreshButtonState()
end)
else
task.spawn(function()
self._player = Players.LocalPlayer
if not self._player then
self._player = Players.PlayerAdded:Wait()
end
self:_refreshButtonState()
end)
end
return self
end
function PassPurchaseHelper:_updateButton(text: string, active: boolean): ()
local button = self._button
if not button then
return
end
button.Text = text
button.Active = active
button.AutoButtonColor = active
end
function PassPurchaseHelper:_getProductInfoAsync(): (boolean, ProductInfo?, string?)
local success, productInfo = pcall(function()
return MarketplaceService:GetProductInfoAsync(self.passID, Enum.InfoType.GamePass)
end)
if success then
return true, productInfo :: ProductInfo?, nil
end
return false, nil, tostring(productInfo)
end
function PassPurchaseHelper:checkOwnership(): (boolean, boolean?, string?)
local player = self._player
if not player then
return false, nil, "LocalPlayer not available"
end
local success, result = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, self.passID)
end)
if success then
self._owns = result :: boolean
return true, result :: boolean, nil
end
return false, nil, tostring(result)
end
function PassPurchaseHelper:promptPurchase(): (boolean, string?)
local player = self._player
if not player then
return false, "LocalPlayer not available"
end
local ownsSuccess, owns, ownsErr = self:checkOwnership()
if not ownsSuccess then
return false, ownsErr or "Ownership check failed"
end
if owns then
return false, "Already owns"
end
local infoSuccess, productInfo, infoErr = self:_getProductInfoAsync()
if not infoSuccess then
return false, "Unable to verify pass sale status: " .. (infoErr or "unknown")
end
if not productInfo or not productInfo.IsForSale then
return false, "Pass is not for sale"
end
local promptSuccess, promptErr = pcall(function()
MarketplaceService:PromptGamePassPurchase(player, self.passID)
end)
if not promptSuccess then
warn("Failed to prompt gamepass purchase:", promptErr)
return false, tostring(promptErr)
end
return true, nil
end
function PassPurchaseHelper:connectButton(button: GuiButton): ()
self._button = button
if self._activatedConnection then
self._activatedConnection:Disconnect()
self._activatedConnection = nil
end
self._activatedConnection = button.Activated:Connect(function()
local ownsSuccess, owns, ownsErr = self:checkOwnership()
if ownsSuccess and not owns then
self:promptPurchase()
elseif not ownsSuccess then
warn("PassPurchaseHelper ownership check failed:", ownsErr)
end
end)
task.spawn(function()
self:_refreshButtonState()
end)
end
function PassPurchaseHelper:_refreshButtonState(): ()
local button = self._button
if not button then
return
end
local player = self._player
if not player then
self:_updateButton("Loading...", false)
return
end
local infoSuccess, productInfo, infoErr = self:_getProductInfoAsync()
if not infoSuccess or not productInfo then
warn("Failed to get product info:", infoErr)
self:_updateButton("Not Available", false)
return
end
if not productInfo.IsForSale then
self:_updateButton("Not For Sale", false)
return
end
local ownsSuccess, owns, ownsErr = self:checkOwnership()
if not ownsSuccess then
warn("Failed to check gamepass ownership:", ownsErr)
self:_updateButton("Check Failed", false)
return
end
if owns then
self:_updateButton("Owned", false)
else
local price = productInfo.PriceInRobux or "?"
self:_updateButton("Purchase " .. price .. "R$", true)
end
end
function PassPurchaseHelper:refreshState(): ()
task.spawn(function()
self:_refreshButtonState()
end)
end
function PassPurchaseHelper:destroy(): ()
if self._activatedConnection then
self._activatedConnection:Disconnect()
self._activatedConnection = nil
end
if self._promptFinishedConnection then
self._promptFinishedConnection:Disconnect()
self._promptFinishedConnection = nil
end
self._button = nil
end
return PassPurchaseHelper