roblox-opencode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/commands/setup-game.md +108 -0
- package/commands/sync-check.md +53 -0
- package/core/roblox-core.md +93 -0
- package/dist/server.js +167 -0
- package/package.json +35 -0
- package/skills/roblox-analytics/SKILL.md +277 -0
- package/skills/roblox-analytics/references/event-batcher.luau +75 -0
- package/skills/roblox-animation-vfx/SKILL.md +1325 -0
- package/skills/roblox-architecture/SKILL.md +863 -0
- package/skills/roblox-architecture/references/combat-systems.md +1381 -0
- package/skills/roblox-code-review/SKILL.md +687 -0
- package/skills/roblox-data/SKILL.md +889 -0
- package/skills/roblox-data/references/inventory-systems.md +1729 -0
- package/skills/roblox-debug/SKILL.md +99 -0
- package/skills/roblox-gui/SKILL.md +1103 -0
- package/skills/roblox-gui-fusion/SKILL.md +150 -0
- package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
- package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
- package/skills/roblox-gui-fusion/references/shop.luau +411 -0
- package/skills/roblox-luau-mastery/SKILL.md +1519 -0
- package/skills/roblox-monetization/SKILL.md +1084 -0
- package/skills/roblox-monetization/references/process-receipt.luau +131 -0
- package/skills/roblox-networking/SKILL.md +669 -0
- package/skills/roblox-networking/references/remote-validator.luau +193 -0
- package/skills/roblox-publish-checklist/SKILL.md +128 -0
- package/skills/roblox-runtime/SKILL.md +753 -0
- package/skills/roblox-sharp-edges/SKILL.md +295 -0
- package/skills/roblox-sync/SKILL.md +126 -0
- package/skills/roblox-testing/SKILL.md +943 -0
- package/skills/roblox-tooling/SKILL.md +150 -0
- package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
- package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
- package/vendor/LICENSES/promise-LICENSE +21 -0
- package/vendor/LICENSES/t-LICENSE +21 -0
- package/vendor/LICENSES/testez-LICENSE +201 -0
- package/vendor/README.md +84 -0
- package/vendor/fusion/Animation/ExternalTime.luau +84 -0
- package/vendor/fusion/Animation/Spring.luau +322 -0
- package/vendor/fusion/Animation/Stopwatch.luau +128 -0
- package/vendor/fusion/Animation/Tween.luau +187 -0
- package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
- package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
- package/vendor/fusion/Animation/lerpType.luau +164 -0
- package/vendor/fusion/Animation/packType.luau +100 -0
- package/vendor/fusion/Animation/springCoefficients.luau +80 -0
- package/vendor/fusion/Animation/unpackType.luau +103 -0
- package/vendor/fusion/Colour/Oklab.luau +70 -0
- package/vendor/fusion/Colour/sRGB.luau +55 -0
- package/vendor/fusion/External.luau +168 -0
- package/vendor/fusion/ExternalDebug.luau +70 -0
- package/vendor/fusion/Graph/Observer.luau +114 -0
- package/vendor/fusion/Graph/castToGraph.luau +29 -0
- package/vendor/fusion/Graph/change.luau +81 -0
- package/vendor/fusion/Graph/depend.luau +33 -0
- package/vendor/fusion/Graph/evaluate.luau +56 -0
- package/vendor/fusion/Instances/Attribute.luau +58 -0
- package/vendor/fusion/Instances/AttributeChange.luau +47 -0
- package/vendor/fusion/Instances/AttributeOut.luau +63 -0
- package/vendor/fusion/Instances/Child.luau +21 -0
- package/vendor/fusion/Instances/Children.luau +148 -0
- package/vendor/fusion/Instances/Hydrate.luau +33 -0
- package/vendor/fusion/Instances/New.luau +53 -0
- package/vendor/fusion/Instances/OnChange.luau +50 -0
- package/vendor/fusion/Instances/OnEvent.luau +54 -0
- package/vendor/fusion/Instances/Out.luau +69 -0
- package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
- package/vendor/fusion/Instances/defaultProps.luau +194 -0
- package/vendor/fusion/LICENSE +21 -0
- package/vendor/fusion/Logging/formatError.luau +49 -0
- package/vendor/fusion/Logging/messages.luau +52 -0
- package/vendor/fusion/Logging/parseError.luau +25 -0
- package/vendor/fusion/Memory/checkLifetime.luau +134 -0
- package/vendor/fusion/Memory/deriveScope.luau +24 -0
- package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
- package/vendor/fusion/Memory/doCleanup.luau +79 -0
- package/vendor/fusion/Memory/innerScope.luau +34 -0
- package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
- package/vendor/fusion/Memory/needsDestruction.luau +17 -0
- package/vendor/fusion/Memory/poisonScope.luau +34 -0
- package/vendor/fusion/Memory/scopePool.luau +55 -0
- package/vendor/fusion/Memory/scoped.luau +27 -0
- package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
- package/vendor/fusion/RobloxExternal.luau +98 -0
- package/vendor/fusion/State/Computed.luau +139 -0
- package/vendor/fusion/State/For/Disassembly.luau +211 -0
- package/vendor/fusion/State/For/ForTypes.luau +30 -0
- package/vendor/fusion/State/For/init.luau +110 -0
- package/vendor/fusion/State/ForKeys.luau +94 -0
- package/vendor/fusion/State/ForPairs.luau +97 -0
- package/vendor/fusion/State/ForValues.luau +94 -0
- package/vendor/fusion/State/Value.luau +88 -0
- package/vendor/fusion/State/castToState.luau +26 -0
- package/vendor/fusion/State/peek.luau +31 -0
- package/vendor/fusion/State/updateAll.luau +1 -0
- package/vendor/fusion/Types.luau +314 -0
- package/vendor/fusion/Utility/Contextual.luau +91 -0
- package/vendor/fusion/Utility/Safe.luau +23 -0
- package/vendor/fusion/Utility/isSimilar.luau +29 -0
- package/vendor/fusion/Utility/merge.luau +35 -0
- package/vendor/fusion/Utility/nameOf.luau +35 -0
- package/vendor/fusion/Utility/never.luau +14 -0
- package/vendor/fusion/Utility/nicknames.luau +11 -0
- package/vendor/fusion/Utility/xtypeof.luau +27 -0
- package/vendor/fusion/init.luau +82 -0
- package/vendor/profilestore/init.luau +2243 -0
- package/vendor/promise/init.luau +1982 -0
- package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
- package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
- package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
- package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
- package/vendor/rbxutil/buffer-util/Types.luau +60 -0
- package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
- package/vendor/rbxutil/buffer-util/init.luau +41 -0
- package/vendor/rbxutil/buffer-util/package.json +16 -0
- package/vendor/rbxutil/buffer-util/wally.toml +9 -0
- package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
- package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
- package/vendor/rbxutil/comm/Client/init.luau +135 -0
- package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
- package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
- package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
- package/vendor/rbxutil/comm/Server/init.luau +140 -0
- package/vendor/rbxutil/comm/Types.luau +18 -0
- package/vendor/rbxutil/comm/Util.luau +27 -0
- package/vendor/rbxutil/comm/init.luau +35 -0
- package/vendor/rbxutil/comm/wally.toml +13 -0
- package/vendor/rbxutil/component/init.luau +759 -0
- package/vendor/rbxutil/component/init.test.luau +311 -0
- package/vendor/rbxutil/component/wally.toml +14 -0
- package/vendor/rbxutil/concur/init.luau +542 -0
- package/vendor/rbxutil/concur/init.test.luau +364 -0
- package/vendor/rbxutil/concur/wally.toml +8 -0
- package/vendor/rbxutil/enum-list/init.luau +101 -0
- package/vendor/rbxutil/enum-list/init.test.luau +91 -0
- package/vendor/rbxutil/enum-list/wally.toml +8 -0
- package/vendor/rbxutil/find/index.d.ts +20 -0
- package/vendor/rbxutil/find/init.luau +44 -0
- package/vendor/rbxutil/find/package.json +17 -0
- package/vendor/rbxutil/find/wally.toml +8 -0
- package/vendor/rbxutil/input/Gamepad.luau +559 -0
- package/vendor/rbxutil/input/Keyboard.luau +124 -0
- package/vendor/rbxutil/input/Mouse.luau +278 -0
- package/vendor/rbxutil/input/PreferredInput.luau +91 -0
- package/vendor/rbxutil/input/Touch.luau +120 -0
- package/vendor/rbxutil/input/init.luau +33 -0
- package/vendor/rbxutil/input/wally.toml +12 -0
- package/vendor/rbxutil/loader/index.d.ts +15 -0
- package/vendor/rbxutil/loader/init.luau +137 -0
- package/vendor/rbxutil/loader/wally.toml +8 -0
- package/vendor/rbxutil/log/index.d.ts +38 -0
- package/vendor/rbxutil/log/init.luau +746 -0
- package/vendor/rbxutil/log/wally.toml +8 -0
- package/vendor/rbxutil/net/init.luau +190 -0
- package/vendor/rbxutil/net/wally.toml +8 -0
- package/vendor/rbxutil/option/index.d.ts +44 -0
- package/vendor/rbxutil/option/init.luau +489 -0
- package/vendor/rbxutil/option/init.test.luau +342 -0
- package/vendor/rbxutil/option/wally.toml +8 -0
- package/vendor/rbxutil/pid/index.d.ts +53 -0
- package/vendor/rbxutil/pid/init.luau +195 -0
- package/vendor/rbxutil/pid/package.json +16 -0
- package/vendor/rbxutil/pid/wally.toml +9 -0
- package/vendor/rbxutil/quaternion/index.d.ts +117 -0
- package/vendor/rbxutil/quaternion/init.luau +570 -0
- package/vendor/rbxutil/quaternion/package.json +16 -0
- package/vendor/rbxutil/quaternion/wally.toml +9 -0
- package/vendor/rbxutil/query/index.d.ts +43 -0
- package/vendor/rbxutil/query/init.luau +117 -0
- package/vendor/rbxutil/query/package.json +18 -0
- package/vendor/rbxutil/query/wally.toml +9 -0
- package/vendor/rbxutil/sequent/index.d.ts +28 -0
- package/vendor/rbxutil/sequent/init.luau +340 -0
- package/vendor/rbxutil/sequent/package.json +16 -0
- package/vendor/rbxutil/sequent/wally.toml +9 -0
- package/vendor/rbxutil/ser/init.luau +175 -0
- package/vendor/rbxutil/ser/init.test.luau +50 -0
- package/vendor/rbxutil/ser/wally.toml +11 -0
- package/vendor/rbxutil/shake/index.d.ts +36 -0
- package/vendor/rbxutil/shake/init.luau +532 -0
- package/vendor/rbxutil/shake/init.test.luau +267 -0
- package/vendor/rbxutil/shake/package.json +16 -0
- package/vendor/rbxutil/shake/wally.toml +9 -0
- package/vendor/rbxutil/signal/index.d.ts +100 -0
- package/vendor/rbxutil/signal/init.luau +432 -0
- package/vendor/rbxutil/signal/init.test.luau +190 -0
- package/vendor/rbxutil/signal/package.json +17 -0
- package/vendor/rbxutil/signal/wally.toml +9 -0
- package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
- package/vendor/rbxutil/silo/Util.luau +55 -0
- package/vendor/rbxutil/silo/init.luau +338 -0
- package/vendor/rbxutil/silo/init.test.luau +215 -0
- package/vendor/rbxutil/silo/wally.toml +8 -0
- package/vendor/rbxutil/spring/index.d.ts +40 -0
- package/vendor/rbxutil/spring/init.luau +97 -0
- package/vendor/rbxutil/spring/package.json +17 -0
- package/vendor/rbxutil/spring/wally.toml +8 -0
- package/vendor/rbxutil/stream/index.d.ts +88 -0
- package/vendor/rbxutil/stream/init.luau +597 -0
- package/vendor/rbxutil/stream/package.json +18 -0
- package/vendor/rbxutil/stream/wally.toml +9 -0
- package/vendor/rbxutil/streamable/Streamable.luau +202 -0
- package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
- package/vendor/rbxutil/streamable/init.luau +8 -0
- package/vendor/rbxutil/streamable/wally.toml +12 -0
- package/vendor/rbxutil/symbol/init.luau +56 -0
- package/vendor/rbxutil/symbol/init.test.luau +37 -0
- package/vendor/rbxutil/symbol/wally.toml +8 -0
- package/vendor/rbxutil/table-util/init.luau +938 -0
- package/vendor/rbxutil/table-util/init.test.luau +439 -0
- package/vendor/rbxutil/table-util/wally.toml +8 -0
- package/vendor/rbxutil/task-queue/index.d.ts +27 -0
- package/vendor/rbxutil/task-queue/init.luau +97 -0
- package/vendor/rbxutil/task-queue/wally.toml +8 -0
- package/vendor/rbxutil/timer/index.d.ts +81 -0
- package/vendor/rbxutil/timer/init.luau +249 -0
- package/vendor/rbxutil/timer/init.test.luau +73 -0
- package/vendor/rbxutil/timer/wally.toml +11 -0
- package/vendor/rbxutil/tree/index.d.ts +15 -0
- package/vendor/rbxutil/tree/init.luau +137 -0
- package/vendor/rbxutil/tree/wally.toml +8 -0
- package/vendor/rbxutil/trove/index.d.ts +46 -0
- package/vendor/rbxutil/trove/init.luau +787 -0
- package/vendor/rbxutil/trove/init.test.luau +203 -0
- package/vendor/rbxutil/trove/wally.toml +8 -0
- package/vendor/rbxutil/typed-remote/init.luau +196 -0
- package/vendor/rbxutil/typed-remote/wally.toml +8 -0
- package/vendor/rbxutil/wait-for/index.d.ts +17 -0
- package/vendor/rbxutil/wait-for/init.luau +257 -0
- package/vendor/rbxutil/wait-for/init.test.luau +182 -0
- package/vendor/rbxutil/wait-for/wally.toml +11 -0
- package/vendor/t/t.lua +1350 -0
- package/vendor/testez/Context.lua +26 -0
- package/vendor/testez/Expectation.lua +311 -0
- package/vendor/testez/ExpectationContext.lua +38 -0
- package/vendor/testez/LifecycleHooks.lua +89 -0
- package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
- package/vendor/testez/Reporters/TextReporter.lua +106 -0
- package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
- package/vendor/testez/TestBootstrap.lua +147 -0
- package/vendor/testez/TestEnum.lua +28 -0
- package/vendor/testez/TestPlan.lua +304 -0
- package/vendor/testez/TestPlanner.lua +40 -0
- package/vendor/testez/TestResults.lua +112 -0
- package/vendor/testez/TestRunner.lua +188 -0
- package/vendor/testez/TestSession.lua +243 -0
- package/vendor/testez/init.lua +40 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: roblox-monetization
|
|
3
|
+
description: ProcessReceipt correctness, prompt APIs, purchase reconciliation, session-lock interaction.
|
|
4
|
+
last_reviewed: 2026-05-26
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Roblox Monetization Systems Reference
|
|
10
|
+
|
|
11
|
+
## 1. Overview
|
|
12
|
+
|
|
13
|
+
**Load this reference when:**
|
|
14
|
+
|
|
15
|
+
- Adding in-game purchases (GamePasses, Developer Products)
|
|
16
|
+
- Designing or revising a monetization strategy
|
|
17
|
+
- Optimizing revenue (pricing, placement, conversion funnels)
|
|
18
|
+
- Implementing Premium Payouts or Rewarded Video Ads
|
|
19
|
+
- Calculating DevEx projections
|
|
20
|
+
- Reviewing monetization ethics and Roblox policy compliance
|
|
21
|
+
|
|
22
|
+
Roblox provides four primary monetization channels: **GamePasses** (one-time permanent unlocks), **Developer Products** (consumable/repeatable purchases), **Premium Payouts** (revenue from Premium subscribers playing your game), and **Rewarded Video Ads** (ad-based revenue). Each channel serves a different purpose and should be combined strategically.
|
|
23
|
+
|
|
24
|
+
**Key principle:** All purchase granting MUST happen on the server. Never trust the client to determine what a player owns or has purchased.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
**Load Full Reference below only when you need specific API implementations or pricing formulas.**
|
|
31
|
+
|
|
32
|
+
Key rules:
|
|
33
|
+
- GamePasses: one-time purchase, check with UserOwnsGamePassAsync on join + cache.
|
|
34
|
+
- Developer Products: consumable, ProcessReceipt is the ONLY place to grant items.
|
|
35
|
+
- ProcessReceipt contract: grant item THEN return PurchaseGranted. If grant fails, return NotProcessedYet. Never return PurchaseGranted before granting.
|
|
36
|
+
- All purchase logic is SERVER-SIDE. Client only prompts.
|
|
37
|
+
- PromptGamePassPurchase / PromptProductPurchase from client, handle on server.
|
|
38
|
+
- TOS: odds disclosure MANDATORY for random items. Games get removed without it.
|
|
39
|
+
- TOS: no real-world trading, no misleading purchase UI, no pay-to-win that ruins gameplay.
|
|
40
|
+
- DevEx: dual-rate system. New Rate $0.0038/R$ (earned after Sept 5, 2025). Old Rate $0.0035/R$ (earned before). Must clear Old Rate balance first before New Rate kicks in.
|
|
41
|
+
- Premium Payouts: engagement-based, detect with player.MembershipType.
|
|
42
|
+
- Subscriptions: recurring monthly revenue via PromptSubscriptionPurchase. Tiered benefits.
|
|
43
|
+
- Private Servers: monetizable via PromptCreatePrivateServer / PromptPurchasePrivateServer.
|
|
44
|
+
- Paid Access: one-time Robux or local currency fee via PromptPurchaseExperience. Common for closed betas.
|
|
45
|
+
- Immersive Ads: AdService image/portal/video ad units. Earn via ad views, separate from Rewarded Video Ads.
|
|
46
|
+
- PolicyService: must-check for compliance (age/region restrictions on subscriptions, random items, ads).
|
|
47
|
+
- Commerce Products: sell physical merchandise through Roblox.
|
|
48
|
+
- Creator Store: sell plugins ($4.99+) and models ($2.99+) for USD. 30-day escrow hold.
|
|
49
|
+
- Never store purchase state only in DataStore without session locking (use ProfileStore).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Full Reference
|
|
54
|
+
|
|
55
|
+
## 2. GamePasses
|
|
56
|
+
|
|
57
|
+
GamePasses are **one-time permanent purchases** tied to the player's account. Once bought, the player owns it forever across all sessions. Ideal for VIP perks, permanent stat boosts, cosmetic bundles, and feature unlocks.
|
|
58
|
+
|
|
59
|
+
### Core API
|
|
60
|
+
|
|
61
|
+
| Method / Event | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId)` | Check if player owns a GamePass |
|
|
64
|
+
| `MarketplaceService:PromptGamePassPurchase(player, gamePassId)` | Show the purchase prompt to a player |
|
|
65
|
+
| `MarketplaceService.PromptGamePassPurchaseFinished` | Fires when the prompt closes (purchased or cancelled) |
|
|
66
|
+
|
|
67
|
+
### Complete GamePass System (Server Script)
|
|
68
|
+
|
|
69
|
+
Place this in `ServerScriptService`:
|
|
70
|
+
|
|
71
|
+
```luau
|
|
72
|
+
-- ServerScriptService/GamePassService.lua
|
|
73
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
74
|
+
local Players = game:GetService("Players")
|
|
75
|
+
|
|
76
|
+
-- ===== CONFIGURATION =====
|
|
77
|
+
-- Map each GamePass ID to a function that grants its perks.
|
|
78
|
+
-- Add new passes here; the rest of the system handles them automatically.
|
|
79
|
+
local GAME_PASSES = {
|
|
80
|
+
[123456789] = {
|
|
81
|
+
name = "VIP",
|
|
82
|
+
grant = function(player: Player)
|
|
83
|
+
-- Example: tag the player so other scripts can check
|
|
84
|
+
player:SetAttribute("IsVIP", true)
|
|
85
|
+
|
|
86
|
+
-- Example: give a permanent speed boost
|
|
87
|
+
local character = player.Character or player.CharacterAdded:Wait()
|
|
88
|
+
local humanoid = character:FindFirstChildOfClass("Humanoid")
|
|
89
|
+
if humanoid then
|
|
90
|
+
humanoid.WalkSpeed = 24
|
|
91
|
+
end
|
|
92
|
+
end,
|
|
93
|
+
},
|
|
94
|
+
[987654321] = {
|
|
95
|
+
name = "2x Coins",
|
|
96
|
+
grant = function(player: Player)
|
|
97
|
+
player:SetAttribute("CoinMultiplier", 2)
|
|
98
|
+
end,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
-- ===== GRANT PERKS ON JOIN =====
|
|
103
|
+
-- Check every configured GamePass when the player joins.
|
|
104
|
+
local function onPlayerAdded(player: Player)
|
|
105
|
+
for gamePassId, passInfo in GAME_PASSES do
|
|
106
|
+
local success, ownsPass = pcall(function()
|
|
107
|
+
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
|
|
108
|
+
end)
|
|
109
|
+
|
|
110
|
+
if success and ownsPass then
|
|
111
|
+
local grantSuccess, grantErr = pcall(passInfo.grant, player)
|
|
112
|
+
if not grantSuccess then
|
|
113
|
+
warn(`[GamePass] Failed to grant "{passInfo.name}" to {player.Name}: {grantErr}`)
|
|
114
|
+
end
|
|
115
|
+
elseif not success then
|
|
116
|
+
warn(`[GamePass] Failed to check ownership of {passInfo.name} for {player.Name}: {ownsPass}`)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
-- Re-grant perks on every respawn (speed, accessories, etc.)
|
|
121
|
+
player.CharacterAdded:Connect(function()
|
|
122
|
+
for gamePassId, passInfo in GAME_PASSES do
|
|
123
|
+
if player:GetAttribute("IsVIP") or player:GetAttribute("CoinMultiplier") then
|
|
124
|
+
-- Only re-grant if we already confirmed ownership
|
|
125
|
+
local success, ownsPass = pcall(function()
|
|
126
|
+
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId)
|
|
127
|
+
end)
|
|
128
|
+
if success and ownsPass then
|
|
129
|
+
pcall(passInfo.grant, player)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
-- ===== GRANT PERKS ON PURCHASE (mid-session) =====
|
|
137
|
+
-- If the player buys a GamePass while already in-game, grant immediately.
|
|
138
|
+
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player: Player, gamePassId: number, wasPurchased: boolean)
|
|
139
|
+
if not wasPurchased then
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local passInfo = GAME_PASSES[gamePassId]
|
|
144
|
+
if not passInfo then
|
|
145
|
+
return
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
local success, err = pcall(passInfo.grant, player)
|
|
149
|
+
if success then
|
|
150
|
+
print(`[GamePass] Granted "{passInfo.name}" to {player.Name} (mid-session purchase)`)
|
|
151
|
+
else
|
|
152
|
+
warn(`[GamePass] Failed to grant "{passInfo.name}" to {player.Name}: {err}`)
|
|
153
|
+
end
|
|
154
|
+
end)
|
|
155
|
+
|
|
156
|
+
-- ===== INITIALIZE =====
|
|
157
|
+
for _, player in Players:GetPlayers() do
|
|
158
|
+
task.spawn(onPlayerAdded, player)
|
|
159
|
+
end
|
|
160
|
+
Players.PlayerAdded:Connect(onPlayerAdded)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Prompting Purchases (Server or Client)
|
|
164
|
+
|
|
165
|
+
```luau
|
|
166
|
+
-- Client-side: prompt a GamePass purchase from a button, shop GUI, etc.
|
|
167
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
168
|
+
local Players = game:GetService("Players")
|
|
169
|
+
|
|
170
|
+
local VIP_PASS_ID = 123456789
|
|
171
|
+
|
|
172
|
+
local function promptVIPPurchase()
|
|
173
|
+
MarketplaceService:PromptGamePassPurchase(Players.LocalPlayer, VIP_PASS_ID)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
-- Connect to a shop button
|
|
177
|
+
script.Parent.MouseButton1Click:Connect(promptVIPPurchase)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 3. Developer Products
|
|
183
|
+
|
|
184
|
+
Developer Products are **consumable/repeatable purchases**. Players can buy them multiple times. Ideal for currency packs, temporary boosts, extra lives, loot crates, and skip-timers.
|
|
185
|
+
|
|
186
|
+
### Core API
|
|
187
|
+
|
|
188
|
+
| Method / Event | Purpose |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `MarketplaceService:PromptProductPurchase(player, productId)` | Show the purchase prompt |
|
|
191
|
+
| `MarketplaceService.ProcessReceipt` | **CRITICAL** callback Roblox invokes to confirm granting |
|
|
192
|
+
|
|
193
|
+
### The ProcessReceipt Contract
|
|
194
|
+
|
|
195
|
+
`ProcessReceipt` is the single most important callback in Roblox monetization. Roblox calls it and expects one of two return values:
|
|
196
|
+
|
|
197
|
+
| Return Value | Meaning |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `Enum.ProductPurchaseDecision.PurchaseGranted` | Item was successfully granted. Roblox finalizes the purchase. **Returning this without actually granting is a policy violation and causes player complaints.** |
|
|
200
|
+
| `Enum.ProductPurchaseDecision.NotProcessedYet` | Granting failed or could not be confirmed. Roblox will **retry** calling ProcessReceipt later (including on rejoin). |
|
|
201
|
+
|
|
202
|
+
**Rules:**
|
|
203
|
+
- Only ONE script can set `MarketplaceService.ProcessReceipt`. If two scripts both assign it, only the last one takes effect and the other is silently overwritten.
|
|
204
|
+
- Return `PurchaseGranted` ONLY after successfully persisting the granted item (DataStore save confirmed).
|
|
205
|
+
- If DataStore save fails, return `NotProcessedYet` so Roblox retries.
|
|
206
|
+
- Always handle the case where the player has left the game before ProcessReceipt fires.
|
|
207
|
+
|
|
208
|
+
### Complete Developer Product System (Server Script)
|
|
209
|
+
|
|
210
|
+
Place this in `ServerScriptService`:
|
|
211
|
+
|
|
212
|
+
```luau
|
|
213
|
+
-- ServerScriptService/DeveloperProductService.lua
|
|
214
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
215
|
+
local DataStoreService = game:GetService("DataStoreService")
|
|
216
|
+
local Players = game:GetService("Players")
|
|
217
|
+
|
|
218
|
+
local purchaseHistoryStore = DataStoreService:GetDataStore("PurchaseHistory")
|
|
219
|
+
|
|
220
|
+
-- ===== CONFIGURATION =====
|
|
221
|
+
-- Map each product ID to a handler that grants the item.
|
|
222
|
+
-- The handler receives the player and must return true on success.
|
|
223
|
+
local PRODUCTS = {
|
|
224
|
+
[111111111] = {
|
|
225
|
+
name = "100 Coins",
|
|
226
|
+
grant = function(player: Player): boolean
|
|
227
|
+
local leaderstats = player:FindFirstChild("leaderstats")
|
|
228
|
+
if not leaderstats then
|
|
229
|
+
return false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
local coins = leaderstats:FindFirstChild("Coins")
|
|
233
|
+
if not coins then
|
|
234
|
+
return false
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
coins.Value += 100
|
|
238
|
+
return true
|
|
239
|
+
end,
|
|
240
|
+
},
|
|
241
|
+
[222222222] = {
|
|
242
|
+
name = "500 Coins",
|
|
243
|
+
grant = function(player: Player): boolean
|
|
244
|
+
local leaderstats = player:FindFirstChild("leaderstats")
|
|
245
|
+
if not leaderstats then
|
|
246
|
+
return false
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
local coins = leaderstats:FindFirstChild("Coins")
|
|
250
|
+
if not coins then
|
|
251
|
+
return false
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
coins.Value += 500
|
|
255
|
+
return true
|
|
256
|
+
end,
|
|
257
|
+
},
|
|
258
|
+
[333333333] = {
|
|
259
|
+
name = "Speed Boost (60s)",
|
|
260
|
+
grant = function(player: Player): boolean
|
|
261
|
+
local character = player.Character
|
|
262
|
+
if not character then
|
|
263
|
+
return false
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
local humanoid = character:FindFirstChildOfClass("Humanoid")
|
|
267
|
+
if not humanoid then
|
|
268
|
+
return false
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
humanoid.WalkSpeed = 32
|
|
272
|
+
task.delay(60, function()
|
|
273
|
+
if humanoid and humanoid.Parent then
|
|
274
|
+
humanoid.WalkSpeed = 16
|
|
275
|
+
end
|
|
276
|
+
end)
|
|
277
|
+
return true
|
|
278
|
+
end,
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
-- ===== PROCESS RECEIPT CALLBACK =====
|
|
283
|
+
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
|
|
284
|
+
-- 1. Check if this purchase was already granted (idempotency guard)
|
|
285
|
+
local purchaseKey = `{receiptInfo.PlayerId}_{receiptInfo.PurchaseId}`
|
|
286
|
+
|
|
287
|
+
local alreadyGranted = false
|
|
288
|
+
local lookupSuccess, lookupErr = pcall(function()
|
|
289
|
+
alreadyGranted = purchaseHistoryStore:GetAsync(purchaseKey)
|
|
290
|
+
end)
|
|
291
|
+
|
|
292
|
+
if not lookupSuccess then
|
|
293
|
+
-- Cannot verify history; retry later to avoid duplicates
|
|
294
|
+
warn(`[Product] DataStore lookup failed for {purchaseKey}: {lookupErr}`)
|
|
295
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if alreadyGranted then
|
|
299
|
+
-- Already granted in a previous attempt; finalize
|
|
300
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
-- 2. Find the product handler
|
|
304
|
+
local productInfo = PRODUCTS[receiptInfo.ProductId]
|
|
305
|
+
if not productInfo then
|
|
306
|
+
warn(`[Product] No handler for product ID {receiptInfo.ProductId}`)
|
|
307
|
+
-- Unknown product: still return NotProcessedYet so it can be handled
|
|
308
|
+
-- after a code update adds the missing handler
|
|
309
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
-- 3. Find the player (they may have left before ProcessReceipt fires)
|
|
313
|
+
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
|
|
314
|
+
if not player then
|
|
315
|
+
-- Player left; retry on their next join
|
|
316
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
-- 4. Grant the item
|
|
320
|
+
local grantSuccess = false
|
|
321
|
+
local grantOk, grantErr = pcall(function()
|
|
322
|
+
grantSuccess = productInfo.grant(player)
|
|
323
|
+
end)
|
|
324
|
+
|
|
325
|
+
if not grantOk then
|
|
326
|
+
warn(`[Product] Grant error for "{productInfo.name}" to {player.Name}: {grantErr}`)
|
|
327
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
if not grantSuccess then
|
|
331
|
+
warn(`[Product] Grant returned false for "{productInfo.name}" to {player.Name}`)
|
|
332
|
+
return Enum.ProductPurchaseDecision.NotProcessedYet
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
-- 5. Record the purchase BEFORE returning PurchaseGranted
|
|
336
|
+
local saveSuccess, saveErr = pcall(function()
|
|
337
|
+
purchaseHistoryStore:SetAsync(purchaseKey, true)
|
|
338
|
+
end)
|
|
339
|
+
|
|
340
|
+
if not saveSuccess then
|
|
341
|
+
-- Grant succeeded but save failed. This is the hardest edge case.
|
|
342
|
+
-- Returning PurchaseGranted risks no record if we crash before saving.
|
|
343
|
+
-- Returning NotProcessedYet risks a duplicate grant on retry.
|
|
344
|
+
-- Best practice: return PurchaseGranted since the player already received
|
|
345
|
+
-- the item, and log the failure for manual reconciliation.
|
|
346
|
+
warn(`[Product] CRITICAL: Grant succeeded but history save failed for {purchaseKey}: {saveErr}`)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
print(`[Product] Granted "{productInfo.name}" to {player.Name} (PurchaseId: {receiptInfo.PurchaseId})`)
|
|
350
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
-- ===== ASSIGN CALLBACK (only one script can do this) =====
|
|
354
|
+
MarketplaceService.ProcessReceipt = processReceipt
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Prompting Developer Product Purchases (Client)
|
|
358
|
+
|
|
359
|
+
```luau
|
|
360
|
+
-- Client-side shop button example
|
|
361
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
362
|
+
local Players = game:GetService("Players")
|
|
363
|
+
|
|
364
|
+
local COINS_100_PRODUCT_ID = 111111111
|
|
365
|
+
|
|
366
|
+
script.Parent.MouseButton1Click:Connect(function()
|
|
367
|
+
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, COINS_100_PRODUCT_ID)
|
|
368
|
+
end)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 4. Premium Payouts
|
|
374
|
+
|
|
375
|
+
Roblox automatically pays developers based on how much time **Premium subscribers** spend in their game. There is no purchase prompt; you earn passively. The more engagement time from Premium players, the higher the payout.
|
|
376
|
+
|
|
377
|
+
### Detecting Premium Players
|
|
378
|
+
|
|
379
|
+
```luau
|
|
380
|
+
-- ServerScriptService/PremiumService.lua
|
|
381
|
+
local Players = game:GetService("Players")
|
|
382
|
+
|
|
383
|
+
local function grantPremiumPerks(player: Player)
|
|
384
|
+
player:SetAttribute("IsPremium", true)
|
|
385
|
+
|
|
386
|
+
-- Example perks to incentivize Premium play time:
|
|
387
|
+
-- Extra daily reward, exclusive cosmetics, bonus XP, premium-only areas
|
|
388
|
+
local leaderstats = player:FindFirstChild("leaderstats")
|
|
389
|
+
if leaderstats then
|
|
390
|
+
local coins = leaderstats:FindFirstChild("Coins")
|
|
391
|
+
if coins then
|
|
392
|
+
coins.Value += 50 -- daily Premium login bonus
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
local function revokePremiumPerks(player: Player)
|
|
398
|
+
player:SetAttribute("IsPremium", false)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
local function onPlayerAdded(player: Player)
|
|
402
|
+
-- Check on join
|
|
403
|
+
if player.MembershipType == Enum.MembershipType.Premium then
|
|
404
|
+
grantPremiumPerks(player)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
-- Real-time detection: player may subscribe or unsubscribe mid-session
|
|
408
|
+
player:GetPropertyChangedSignal("MembershipType"):Connect(function()
|
|
409
|
+
if player.MembershipType == Enum.MembershipType.Premium then
|
|
410
|
+
grantPremiumPerks(player)
|
|
411
|
+
else
|
|
412
|
+
revokePremiumPerks(player)
|
|
413
|
+
end
|
|
414
|
+
end)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
for _, player in Players:GetPlayers() do
|
|
418
|
+
task.spawn(onPlayerAdded, player)
|
|
419
|
+
end
|
|
420
|
+
Players.PlayerAdded:Connect(onPlayerAdded)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Premium Upsell
|
|
424
|
+
|
|
425
|
+
You can prompt non-Premium players to subscribe:
|
|
426
|
+
|
|
427
|
+
```luau
|
|
428
|
+
-- Client-side: prompt a Premium subscription upsell
|
|
429
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
430
|
+
local Players = game:GetService("Players")
|
|
431
|
+
|
|
432
|
+
local player = Players.LocalPlayer
|
|
433
|
+
|
|
434
|
+
if player.MembershipType ~= Enum.MembershipType.Premium then
|
|
435
|
+
MarketplaceService:PromptPremiumPurchase(player)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
-- Listen for result
|
|
439
|
+
MarketplaceService.PromptPremiumPurchaseFinished:Connect(function()
|
|
440
|
+
-- MembershipType will update automatically on the server if they subscribed
|
|
441
|
+
end)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 5. Rewarded Video Ads
|
|
447
|
+
|
|
448
|
+
Players opt-in to watching a short video ad in exchange for an in-game reward. Revenue per completed view. API is via `AdService` - use mcp-roblox-docs for current method signatures (API has changed during beta).
|
|
449
|
+
|
|
450
|
+
### Placement Best Practices
|
|
451
|
+
|
|
452
|
+
- **Between rounds** - natural break, player is already waiting
|
|
453
|
+
- **In lobby / waiting area** - low-stakes moment, nothing else to do
|
|
454
|
+
- **After death (optional revive)** - high motivation, clear value proposition
|
|
455
|
+
- **Daily bonus multiplier** - "Watch ad to double your daily reward"
|
|
456
|
+
|
|
457
|
+
**Avoid:** mid-gameplay interruptions, mandatory ads, ads that block progression.
|
|
458
|
+
|
|
459
|
+
### Reward Value
|
|
460
|
+
|
|
461
|
+
- Target 3-10 Robux equivalent value per completed view
|
|
462
|
+
- Too low: players won't bother. Too high: undermines paid products.
|
|
463
|
+
- Implement a server-side cooldown (5+ minutes) to prevent spam
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## 6. Subscriptions
|
|
468
|
+
|
|
469
|
+
Subscriptions provide recurring monthly revenue. Players pay a monthly Robux fee and receive ongoing benefits. This creates predictable income and higher lifetime value per player.
|
|
470
|
+
|
|
471
|
+
### Core API
|
|
472
|
+
|
|
473
|
+
| Method / Event | Purpose |
|
|
474
|
+
|---|---|
|
|
475
|
+
| `MarketplaceService:PromptSubscriptionPurchase(player, subscriptionId)` | Show the subscription purchase prompt |
|
|
476
|
+
| `MarketplaceService.PromptSubscriptionPurchaseFinished` | Fires when the subscription purchase prompt closes (does NOT confirm purchase - use UserHasSubscriptionAsync to verify) |
|
|
477
|
+
| `MarketplaceService:GetSubscriptionProductInfoAsync(subscriptionId)` | Get subscription tier details (price, name, description) |
|
|
478
|
+
| `MarketplaceService:UserHasSubscriptionAsync(userId, subscriptionId)` | Check if a player has an active subscription |
|
|
479
|
+
|
|
480
|
+
### Subscription Configuration
|
|
481
|
+
|
|
482
|
+
Subscriptions are configured in the **Creator Dashboard > Monetization > Subscriptions**. Each subscription has:
|
|
483
|
+
|
|
484
|
+
- **Name** - Displayed to the player
|
|
485
|
+
- **Description** - What benefits they receive
|
|
486
|
+
- **Price** - Monthly Robux cost (25 R$ minimum)
|
|
487
|
+
- **Benefits** - Defined by your game; granted server-side
|
|
488
|
+
|
|
489
|
+
### Implementation (Server Script)
|
|
490
|
+
|
|
491
|
+
```luau
|
|
492
|
+
-- ServerScriptService/SubscriptionService.lua
|
|
493
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
494
|
+
local Players = game:GetService("Players")
|
|
495
|
+
|
|
496
|
+
local SUBSCRIPTIONS = {
|
|
497
|
+
["premium_monthly"] = {
|
|
498
|
+
id = 123456789,
|
|
499
|
+
name = "Premium Monthly",
|
|
500
|
+
grant = function(player: Player)
|
|
501
|
+
player:SetAttribute("Subscriber", true)
|
|
502
|
+
player:SetAttribute("MonthlyBonus", 500)
|
|
503
|
+
end,
|
|
504
|
+
revoke = function(player: Player)
|
|
505
|
+
player:SetAttribute("Subscriber", false)
|
|
506
|
+
player:SetAttribute("MonthlyBonus", 0)
|
|
507
|
+
end,
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
-- Grant on join if subscription is active
|
|
512
|
+
local function onPlayerAdded(player: Player)
|
|
513
|
+
for key, sub in SUBSCRIPTIONS do
|
|
514
|
+
local success, hasSub = pcall(function()
|
|
515
|
+
return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
|
|
516
|
+
end)
|
|
517
|
+
if success and hasSub then
|
|
518
|
+
sub.grant(player)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
-- Handle prompt close (NOTE: this does NOT confirm purchase succeeded)
|
|
524
|
+
-- Use UserHasSubscriptionAsync to verify actual subscription status
|
|
525
|
+
MarketplaceService.PromptSubscriptionPurchaseFinished:Connect(function(player: Player, subscriptionId: string, didTryPurchasing: boolean)
|
|
526
|
+
if not didTryPurchasing then return end
|
|
527
|
+
-- Player attempted purchase - verify it actually went through
|
|
528
|
+
for key, sub in SUBSCRIPTIONS do
|
|
529
|
+
if sub.id == subscriptionId then
|
|
530
|
+
local success, hasSub = pcall(function()
|
|
531
|
+
return MarketplaceService:UserHasSubscriptionAsync(player.UserId, sub.id)
|
|
532
|
+
end)
|
|
533
|
+
if success and hasSub then
|
|
534
|
+
sub.grant(player)
|
|
535
|
+
end
|
|
536
|
+
break
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end)
|
|
540
|
+
|
|
541
|
+
for _, player in Players:GetPlayers() do
|
|
542
|
+
task.spawn(onPlayerAdded, player)
|
|
543
|
+
end
|
|
544
|
+
Players.PlayerAdded:Connect(onPlayerAdded)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Subscription Design Best Practices
|
|
548
|
+
|
|
549
|
+
- **Tiered value:** Offer 2-3 tiers (Bronze/Silver/Gold or Basic/Pro/Ultimate) at increasing prices
|
|
550
|
+
- **Clear benefits:** List exact benefits in the subscription description. "2x coins" is better than "exclusive rewards"
|
|
551
|
+
- **Recurring currency:** Give a daily or monthly currency stipend that incentivizes logging in
|
|
552
|
+
- **Exclusive content:** Cosmetics, titles, frames, and emotes that are permanently unlocked for subscribers
|
|
553
|
+
- **Non-disruptive:** Free players should still enjoy the full game loop. Subscribers get bonuses, not exclusive gameplay
|
|
554
|
+
- **Cancellation:** Use `UserHasSubscriptionAsync` on player join to detect lapsed subscriptions and revoke benefits. The prompt event only fires when the UI closes, not on actual cancellation.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 7. Private Servers
|
|
559
|
+
|
|
560
|
+
Private servers let players pay a monthly Robux fee for a dedicated server instance they control. Players can invite friends, play in private, host events, or farm resources without interference.
|
|
561
|
+
|
|
562
|
+
### Core API
|
|
563
|
+
|
|
564
|
+
| Method / Event | Purpose |
|
|
565
|
+
|---|---|
|
|
566
|
+
| `MarketplaceService:PromptCreatePrivateServer(player, placeId)` | Show the create/purchase prompt for a new private server |
|
|
567
|
+
| `MarketplaceService:PromptPurchasePrivateServer(player, privateServerId)` | Show the renewal prompt for an existing private server |
|
|
568
|
+
| `MarketplaceService.PrivateServerPurchaseFinished` | Fires when a private server is purchased or renewed |
|
|
569
|
+
|
|
570
|
+
### Setup
|
|
571
|
+
|
|
572
|
+
1. Navigate to your experience in Creator Dashboard
|
|
573
|
+
2. Go to **Monetization > Private Servers**
|
|
574
|
+
3. Set the monthly price in Robux (min 10 R$, can change every 60 days)
|
|
575
|
+
4. Configure any server-specific settings
|
|
576
|
+
|
|
577
|
+
### Notes
|
|
578
|
+
|
|
579
|
+
- **Price changes** are limited to once every 60 days. Plan pricing carefully.
|
|
580
|
+
- **Revenue:** You earn 50% of the subscription fee (Roblox takes the other 50%).
|
|
581
|
+
- **Permissions:** Private server owners can configure who can join via the server's settings page.
|
|
582
|
+
- **VipServer:** The legacy `VipServer` API is deprecated. Use the new Private Server APIs.
|
|
583
|
+
|
|
584
|
+
### Common Use Cases
|
|
585
|
+
|
|
586
|
+
- **Competitive practice:** Teams/guilds rent a server to practice strategies
|
|
587
|
+
- **Roleplay communities:** Persistent worlds for friend groups
|
|
588
|
+
- **Resource farming:** Dedicated server for grinding without competition
|
|
589
|
+
- **Content creators:** Record/stream without interference from other players
|
|
590
|
+
- **Classes/events:** Educators or event hosts run private sessions
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 8. Paid Access (Entry Fee)
|
|
595
|
+
|
|
596
|
+
Paid access charges a one-time fee - in Robux or local currency - for entry to your experience. Commonly used for closed betas, premium experiences, or content packs.
|
|
597
|
+
|
|
598
|
+
### Core API
|
|
599
|
+
|
|
600
|
+
| Method / Event | Purpose |
|
|
601
|
+
|---|---|
|
|
602
|
+
| `MarketplaceService:PromptPurchaseExperience(player)` | Prompt the player to purchase access |
|
|
603
|
+
| `MarketplaceService.PromptPurchaseExperienceFinished` | Fires when the prompt closes |
|
|
604
|
+
| `MarketplaceService:UserOwnsGamePassAsync` | Check if the player has purchased access (uses a hidden GamePass) |
|
|
605
|
+
|
|
606
|
+
### Implementation
|
|
607
|
+
|
|
608
|
+
```luau
|
|
609
|
+
-- Server: Check access on join
|
|
610
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
611
|
+
|
|
612
|
+
-- Roblox assigns a hidden GamePass ID when you enable paid access
|
|
613
|
+
-- Check it with UserOwnsGamePassAsync on PlayerAdded
|
|
614
|
+
local ACCESS_PASS_ID = 123456789 -- Replace with your experience's ID
|
|
615
|
+
|
|
616
|
+
local function onPlayerAdded(player: Player)
|
|
617
|
+
local success, hasAccess = pcall(function()
|
|
618
|
+
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, ACCESS_PASS_ID)
|
|
619
|
+
end)
|
|
620
|
+
|
|
621
|
+
if success and hasAccess then
|
|
622
|
+
-- Player has purchased access, let them in
|
|
623
|
+
else
|
|
624
|
+
-- Player has not purchased access
|
|
625
|
+
-- Teleport them to the purchase experience or show a purchase prompt
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
```luau
|
|
631
|
+
-- Client: Prompt purchase
|
|
632
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
633
|
+
local Players = game:GetService("Players")
|
|
634
|
+
|
|
635
|
+
local function promptPurchase()
|
|
636
|
+
MarketplaceService:PromptPurchaseExperience(Players.LocalPlayer)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
MarketplaceService.PromptPurchaseExperienceFinished:Connect(function(player: Player, wasPurchased: boolean)
|
|
640
|
+
if wasPurchased then
|
|
641
|
+
-- Player purchased access, teleport to the main experience
|
|
642
|
+
end
|
|
643
|
+
end)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Types
|
|
647
|
+
|
|
648
|
+
| Type | Priced In | Payout |
|
|
649
|
+
|------|-----------|--------|
|
|
650
|
+
| **Robux** | Robux (one-time) | Standard Robux payout |
|
|
651
|
+
| **Local Currency** | User's local currency (fallback USD) | USD payout |
|
|
652
|
+
|
|
653
|
+
### Use Cases
|
|
654
|
+
|
|
655
|
+
- **Closed beta:** Let most engaged users test early
|
|
656
|
+
- **Standalone experiences:** One-time purchase games (premium content packs)
|
|
657
|
+
- **Ticket/event access:** Temporary access for limited-time events
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
## 9. Immersive Ads
|
|
662
|
+
|
|
663
|
+
Immersive ads allow Roblox to serve advertiser content inside your experience. You earn revenue from ad views. Separate from Rewarded Video Ads (which are player-initiated opt-in).
|
|
664
|
+
|
|
665
|
+
### Ad Formats
|
|
666
|
+
|
|
667
|
+
| Format | Description | Placement |
|
|
668
|
+
|--------|-------------|-----------|
|
|
669
|
+
| **Image Ad** | Static image displayed on an AdPanel or AdPortal | On a surface, billboard, or screen in your experience |
|
|
670
|
+
| **Portal Ad** | Interactive portal that teleports to another experience | Ground-level portal the player can walk through |
|
|
671
|
+
| **Video Ad** | Video player ad unit | On a screen or surface |
|
|
672
|
+
| **Branded Ad** | Custom branded content integrated into the experience | Sponsored items, branded environments |
|
|
673
|
+
|
|
674
|
+
### Core API
|
|
675
|
+
|
|
676
|
+
| API | Purpose |
|
|
677
|
+
|---|---|
|
|
678
|
+
| `AdService` | Service for managing ad units |
|
|
679
|
+
| `AdPortal` | Instance class for portal ad units |
|
|
680
|
+
| `AdGui` | Instance class for image/video ad units placed in 3D space |
|
|
681
|
+
|
|
682
|
+
### Placement Best Practices
|
|
683
|
+
|
|
684
|
+
- **Natural integration:** Place ads where real-world billboards or screens would exist (stadium walls, shop windows, city buildings)
|
|
685
|
+
- **Non-intrusive:** Ads should not block gameplay, navigation, or UI
|
|
686
|
+
- **Contextual:** An ad for a racing game fits on a billboard in your racing game's loading area
|
|
687
|
+
- **No interaction required:** Players should not be required to watch or interact with ads to progress
|
|
688
|
+
- **Respect PolicyService:** Check `PolicyService:GetPolicyInfoForPlayerAsync()` to ensure ads are shown only to eligible users (age/region restrictions)
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 10. Commerce Products and Creator Store
|
|
693
|
+
|
|
694
|
+
### Commerce Products
|
|
695
|
+
|
|
696
|
+
Commerce Products allow you to sell **physical goods** (merchandise) through Roblox. Configured in the Creator Dashboard under **Monetization > Commerce Products**.
|
|
697
|
+
|
|
698
|
+
- Requires seller onboarding and eligibility verification
|
|
699
|
+
- Products are synced to Roblox for purchase
|
|
700
|
+
- Supports fulfillment tracking
|
|
701
|
+
|
|
702
|
+
### Creator Store (Plugins and Models)
|
|
703
|
+
|
|
704
|
+
Sell development assets to other creators:
|
|
705
|
+
|
|
706
|
+
| Asset Type | Minimum Price | Revenue Share |
|
|
707
|
+
|------------|---------------|---------------|
|
|
708
|
+
| **Plugin** | $4.99 USD | Taxes and payment processing fees only |
|
|
709
|
+
| **Model** | $2.99 USD | Taxes and payment processing fees only |
|
|
710
|
+
|
|
711
|
+
**Escrow hold:** Roblox holds your share of each sale for **30 days** from the date of purchase.
|
|
712
|
+
|
|
713
|
+
### Marketplace (Catalog) Commissions
|
|
714
|
+
|
|
715
|
+
When users purchase your catalog items (accessories, clothes) within your experience via the avatar inspect menu or avatar editor service, you earn a commission on each sale.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## 11. PolicyService Compliance
|
|
720
|
+
|
|
721
|
+
Roblox requires you to use `PolicyService` to restrict certain monetization features based on the player's age, location, and platform.
|
|
722
|
+
|
|
723
|
+
### When to Check PolicyService
|
|
724
|
+
|
|
725
|
+
- **Subscriptions / Commerce Products:** Only show purchase options to eligible users
|
|
726
|
+
- **Paid random items (loot boxes, gacha):** Must block for users in restricted regions
|
|
727
|
+
- **Immersive ads:** Only show ad units to eligible users
|
|
728
|
+
- **Paid item trading:** Must check eligibility
|
|
729
|
+
|
|
730
|
+
### Implementation
|
|
731
|
+
|
|
732
|
+
```luau
|
|
733
|
+
-- ServerScriptService/PolicyServiceCheck.lua
|
|
734
|
+
local PolicyService = game:GetService("PolicyService")
|
|
735
|
+
local Players = game:GetService("Players")
|
|
736
|
+
|
|
737
|
+
local function isEligibleForRandomItems(player: Player): boolean
|
|
738
|
+
local success, policyInfo = pcall(function()
|
|
739
|
+
return PolicyService:GetPolicyInfoForPlayerAsync(player)
|
|
740
|
+
end)
|
|
741
|
+
|
|
742
|
+
if not success then
|
|
743
|
+
-- On failure, default to restricting the feature
|
|
744
|
+
return false
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
return policyInfo.IsPriceFixEnabled -- Example: check relevant policy flag
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
-- Usage: hide loot boxes if the player is not eligible
|
|
751
|
+
local function updateShopUI(player: Player)
|
|
752
|
+
if isEligibleForRandomItems(player) then
|
|
753
|
+
-- Show loot boxes in the shop
|
|
754
|
+
else
|
|
755
|
+
-- Hide loot boxes or show a "not available in your region" message
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
Players.PlayerAdded:Connect(function(player: Player)
|
|
760
|
+
player:GetPropertyChangedSignal("MembershipType"):Connect(function()
|
|
761
|
+
updateShopUI(player)
|
|
762
|
+
end)
|
|
763
|
+
updateShopUI(player)
|
|
764
|
+
end)
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Recommended Approach
|
|
768
|
+
|
|
769
|
+
- **Fail closed:** If `PolicyService:GetPolicyInfoForPlayerAsync()` errors, default to restricting the feature
|
|
770
|
+
- **Cache results per-player** to avoid repeated API calls
|
|
771
|
+
- **Re-check on locale change** if you support in-session region switching
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 12. Pricing Strategy
|
|
776
|
+
|
|
777
|
+
| Robux | Typical Use |
|
|
778
|
+
|---|---|
|
|
779
|
+
| **25** | Minimum viable price. Small cosmetic, single-use consumable |
|
|
780
|
+
| **50** | Minor cosmetic pack, small currency bundle |
|
|
781
|
+
| **75** | Mid-tier consumable, trail effect, small pet |
|
|
782
|
+
| **100** | Standard GamePass, decent currency pack |
|
|
783
|
+
| **250** | Premium GamePass (2x coins, VIP), mid currency bundle |
|
|
784
|
+
| **500** | Major GamePass (significant gameplay advantage), large currency pack |
|
|
785
|
+
| **1,000** | Top-tier GamePass, mega currency bundle |
|
|
786
|
+
| **2,500+** | Whale-tier only. Use sparingly |
|
|
787
|
+
|
|
788
|
+
### Pricing Tactics
|
|
789
|
+
|
|
790
|
+
**Anchoring:** Show the most expensive option first in the shop UI. When a player sees "Mega Pack: 1,000 Robux" first, the "Starter Pack: 100 Robux" feels like a bargain by comparison.
|
|
791
|
+
|
|
792
|
+
**Bundle Value:** Offer multi-item bundles at a per-unit discount:
|
|
793
|
+
- 100 Coins = 50 Robux (0.50 Robux/coin)
|
|
794
|
+
- 300 Coins = 100 Robux (0.33 Robux/coin) -- "Best Value" tag
|
|
795
|
+
- 1,000 Coins = 250 Robux (0.25 Robux/coin) -- "Most Popular" tag
|
|
796
|
+
|
|
797
|
+
**Minimum Price Floor:** Do not price anything below **25 Robux**. Roblox takes a 30% marketplace fee, and extremely low-priced items generate negligible revenue while still requiring full implementation and support effort.
|
|
798
|
+
|
|
799
|
+
**Odd Pricing:** 49 Robux feels cheaper than 50 Robux. 99 feels cheaper than 100. Roblox players respond to this the same way real-world consumers do.
|
|
800
|
+
|
|
801
|
+
**Limited-Time Offers:** Create urgency with rotating shop items or seasonal GamePasses. Fear of missing out (FOMO) drives conversions, but use ethically (see Section 8).
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## 13. DevEx Math
|
|
806
|
+
|
|
807
|
+
### Dual Exchange Rate System
|
|
808
|
+
|
|
809
|
+
As of September 5, 2025, Roblox operates a dual-rate DevEx system based on when Robux was earned:
|
|
810
|
+
|
|
811
|
+
| Rate | Value | Applies To |
|
|
812
|
+
|------|-------|------------|
|
|
813
|
+
| **New Rate** | $0.0038/R$ | Robux earned **on or after** 10 AM PT on September 5, 2025 |
|
|
814
|
+
| **Old Rate** | $0.0035/R$ | Robux earned **before** 10 AM PT on September 5, 2025 |
|
|
815
|
+
|
|
816
|
+
### Cash-Out Ordering Rules
|
|
817
|
+
|
|
818
|
+
- **Must clear Old Rate first:** You must cash out **all** Old Rate Robux before you can cash out any New Rate Robux.
|
|
819
|
+
- **Spending does not help:** Spending Robux on the platform (items, experiences, etc.) does **not** reduce your Old Rate balance. Spending is deducted from your total balance but does not count toward clearing Old Rate first.
|
|
820
|
+
- **Group funds:** If you receive payment from a Group that earned Robux before the cutoff, those Robux also cash out at the Old Rate. Your Old Rate balance may increase from Group payouts.
|
|
821
|
+
|
|
822
|
+
### Example Conversion
|
|
823
|
+
|
|
824
|
+
| Balance Type | Amount | Rate | USD Value |
|
|
825
|
+
|-------------|--------|------|-----------|
|
|
826
|
+
| Old Rate | 30,000 R$ | $0.0035 | $105 |
|
|
827
|
+
| New Rate | 30,000 R$ | $0.0038 | $114 |
|
|
828
|
+
| Mixed (clear Old Rate first) | 30,000 Old + 30,000 New | Dual | $105 + $114 = $219 |
|
|
829
|
+
|
|
830
|
+
### Minimum Cashout
|
|
831
|
+
|
|
832
|
+
- **30,000 Robux** minimum per cash-out request.
|
|
833
|
+
- Funds are reviewed on a per-request basis. First-time cashouts require creating a DevEx portal account via email invite.
|
|
834
|
+
- Eligibility requirements and service requirements are defined in the [DevEx Terms of Use](https://en.help.roblox.com/hc/en-us/articles/205499100-Developer-Exchange-DevEx-Program-Frequently-Asked-Questions).
|
|
835
|
+
|
|
836
|
+
> **Upcoming (June 2026):** US creators 18+ will get a higher rate of ~$0.0054/Robux (42% increase). Requires identity verification.
|
|
837
|
+
|
|
838
|
+
### Revenue Projection Formulas
|
|
839
|
+
|
|
840
|
+
```
|
|
841
|
+
Daily Revenue (Robux) = DAU x Conversion Rate x Average Purchase (Robux)
|
|
842
|
+
|
|
843
|
+
Monthly Revenue (Robux) = Daily Revenue x 30
|
|
844
|
+
|
|
845
|
+
Monthly Revenue (USD) = Monthly Revenue (Robux) x 0.0038
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**Example projections at different scales (New Rate):**
|
|
849
|
+
|
|
850
|
+
| DAU | Conversion Rate | Avg Purchase | Daily Robux | Monthly USD |
|
|
851
|
+
|---|---|---|---|---|
|
|
852
|
+
| 100 | 2% | 100 R$ | 200 | $23 |
|
|
853
|
+
| 1,000 | 2% | 100 R$ | 2,000 | $228 |
|
|
854
|
+
| 10,000 | 3% | 150 R$ | 45,000 | $5,130 |
|
|
855
|
+
| 100,000 | 3% | 150 R$ | 450,000 | $51,300 |
|
|
856
|
+
|
|
857
|
+
> **Important:** If your revenue includes Old Rate Robux, the USD value will be lower until the Old Rate balance is cleared. For mixed balances, calculate separately and sum.
|
|
858
|
+
|
|
859
|
+
**Typical conversion rates on Roblox:** 1-5% of DAU makes a purchase on any given day. Well-optimized games with strong shop design reach the higher end.
|
|
860
|
+
|
|
861
|
+
**Premium Payout addition:** Premium Payouts add roughly 10-30% on top of direct purchase revenue depending on your Premium player ratio and engagement quality.
|
|
862
|
+
|
|
863
|
+
### Break-Even Calculations
|
|
864
|
+
|
|
865
|
+
```
|
|
866
|
+
Hours spent developing = X
|
|
867
|
+
Hourly rate target = $Y/hr
|
|
868
|
+
Required total earnings = X * Y
|
|
869
|
+
Required Robux = (X * Y) / 0.0038
|
|
870
|
+
Required paying players = Required Robux / Average Purchase
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## 14. Roblox TOS Compliance (MANDATORY)
|
|
876
|
+
|
|
877
|
+
These are not suggestions. Violating them gets your game taken down.
|
|
878
|
+
|
|
879
|
+
### Odds Disclosure (enforced, games get removed)
|
|
880
|
+
|
|
881
|
+
**Any item sold with a randomized element MUST display the exact drop chance percentages in-game.** This applies to:
|
|
882
|
+
- Loot boxes / mystery boxes
|
|
883
|
+
- Random pet hatching
|
|
884
|
+
- Gacha pulls
|
|
885
|
+
- Any "chance" mechanic tied to a purchase
|
|
886
|
+
|
|
887
|
+
If a pet has a 0.1% drop rate, the player must see "0.1%" before they buy. Not "rare," not "legendary," not a color code. The exact number.
|
|
888
|
+
|
|
889
|
+
Games have been taken down for violating this. Roblox enforces it.
|
|
890
|
+
|
|
891
|
+
```luau
|
|
892
|
+
-- Example: display odds on a loot box GUI
|
|
893
|
+
local oddsLabel = script.Parent.OddsLabel
|
|
894
|
+
oddsLabel.Text = "Drop rates: Common 60% | Uncommon 25% | Rare 10% | Epic 4% | Legendary 1%"
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Presenting Products (Guidelines)
|
|
898
|
+
|
|
899
|
+
Roblox requires monetization products to be presented in a way that is **transparent, honest, and user-friendly**:
|
|
900
|
+
|
|
901
|
+
**Discounts must be genuine and fair:**
|
|
902
|
+
- A discount is not genuine if an item is always "on sale" for the same amount.
|
|
903
|
+
- A discount is not fair if it's only offered for a very short time, pressuring users.
|
|
904
|
+
|
|
905
|
+
**No misleading urgency:**
|
|
906
|
+
- Don't claim an item is almost out of stock or only available for a short time if it isn't true.
|
|
907
|
+
- Don't use a countdown timer that isn't accurate or automatically restarts.
|
|
908
|
+
|
|
909
|
+
**Language recommendations:**
|
|
910
|
+
| Avoid | Use Instead |
|
|
911
|
+
|-------|-------------|
|
|
912
|
+
| "GET IT NOW" | "View Item" |
|
|
913
|
+
| "LAST CHANCE, ACT NOW" | "See Price" |
|
|
914
|
+
| "BUY BEFORE IT'S GONE!" | "Open Shop" |
|
|
915
|
+
|
|
916
|
+
### Other TOS Rules That Affect Monetization
|
|
917
|
+
|
|
918
|
+
- **No gambling mechanics.** Do not implement anything that resembles gambling (betting Robux, coin flips, roulette). Roblox bans these.
|
|
919
|
+
- **No off-platform sales.** Do not direct players to buy Robux or items outside of Roblox's systems.
|
|
920
|
+
- **No misleading product descriptions.** GamePass and DevProduct descriptions must exactly match what the player receives.
|
|
921
|
+
- **No purchased advantages in experiences marked as "All Ages."** Stricter rules apply for experiences targeting younger audiences.
|
|
922
|
+
- **Refund policy.** If a player reports not receiving an item, investigate and honor legitimate claims. Roblox can reverse charges.
|
|
923
|
+
- **PolicyService integration required.** Use `PolicyService:GetPolicyInfoForPlayerAsync()` to restrict subscriptions, commerce products, paid random items, and immersive ads based on user eligibility.
|
|
924
|
+
|
|
925
|
+
> **Recommendation:** Download and read the full [Roblox Community Standards](https://en.help.roblox.com/hc/en-us/articles/203313410-Roblox-Community-Standards) and [Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use). Feed them to the AI as context when working on monetization features.
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## 15. Ethical Monetization
|
|
930
|
+
|
|
931
|
+
Roblox's audience skews young (a significant portion is under 16). This carries a responsibility to monetize fairly. Roblox also actively enforces policies against predatory practices.
|
|
932
|
+
|
|
933
|
+
### Do
|
|
934
|
+
|
|
935
|
+
- **Provide genuine value** for every purchase. The player should feel good about what they got.
|
|
936
|
+
- **Allow core gameplay for free.** Free players should enjoy the full game loop. Purchases should enhance, not gate.
|
|
937
|
+
- **Price transparently.** Show the Robux cost clearly before any purchase prompt.
|
|
938
|
+
- **Offer earnable alternatives.** If a cosmetic costs 100 Robux, also let players earn it after 10 hours of gameplay.
|
|
939
|
+
- **Respect declining.** If a player closes a purchase prompt, do not immediately re-prompt.
|
|
940
|
+
|
|
941
|
+
### Do Not
|
|
942
|
+
|
|
943
|
+
- **No pay-to-win in competitive modes.** If your game has PvP, purchased items should not provide a statistical advantage.
|
|
944
|
+
- **No hidden costs.** Never require a chain of purchases to unlock something ("buy A to unlock B to unlock C").
|
|
945
|
+
- **No artificial scarcity manipulation.** "Only 3 left!" when supply is unlimited is deceptive.
|
|
946
|
+
- **No pressure tactics on children.** Countdown timers, social pressure ("your friend bought this!"), and guilt messaging are inappropriate.
|
|
947
|
+
- **No paywalled progression.** Never block a player from advancing in the story or level because they have not purchased something.
|
|
948
|
+
- **No misleading descriptions.** GamePass and product descriptions must accurately reflect what the player receives.
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
## 16. Best Practices
|
|
953
|
+
|
|
954
|
+
### Server-Side Purchase Verification (Always)
|
|
955
|
+
|
|
956
|
+
Never grant items from the client. A client script can prompt a purchase, but the grant must always happen in a ServerScript via `ProcessReceipt` (for products) or `PromptGamePassPurchaseFinished` (for GamePasses, verified with `UserOwnsGamePassAsync`).
|
|
957
|
+
|
|
958
|
+
### Graceful Failure Handling
|
|
959
|
+
|
|
960
|
+
```luau
|
|
961
|
+
-- Wrap every MarketplaceService call in pcall
|
|
962
|
+
local success, result = pcall(function()
|
|
963
|
+
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, passId)
|
|
964
|
+
end)
|
|
965
|
+
|
|
966
|
+
if not success then
|
|
967
|
+
-- API is down or rate-limited. Fail gracefully.
|
|
968
|
+
warn(`[Purchase] API call failed: {result}`)
|
|
969
|
+
-- Do NOT assume they own it; do NOT assume they don't.
|
|
970
|
+
-- Cache the last known state and retry later.
|
|
971
|
+
end
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### Receipt Logging
|
|
975
|
+
|
|
976
|
+
Log every purchase for customer support and debugging:
|
|
977
|
+
|
|
978
|
+
```luau
|
|
979
|
+
-- Inside ProcessReceipt, after granting
|
|
980
|
+
print(`[Receipt] Player={receiptInfo.PlayerId} Product={receiptInfo.ProductId} PurchaseId={receiptInfo.PurchaseId} CurrencySpent={receiptInfo.CurrencySpent} PlaceId={receiptInfo.PlaceIdWherePurchased}`)
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
Keep a DataStore or external log of all purchases so you can:
|
|
984
|
+
- Investigate "I paid but didn't get my item" support tickets
|
|
985
|
+
- Track conversion metrics
|
|
986
|
+
- Identify unusual purchase patterns (potential fraud or exploits)
|
|
987
|
+
|
|
988
|
+
### Test Purchases in Studio
|
|
989
|
+
|
|
990
|
+
- In Roblox Studio, `ProcessReceipt` will fire with test data.
|
|
991
|
+
- `UserOwnsGamePassAsync` returns false in Studio for passes the Studio user does not own.
|
|
992
|
+
- Use Studio's "Test" tab to simulate purchases.
|
|
993
|
+
- Always test the full flow: prompt, purchase, grant, rejoin-and-re-grant, and the failure path.
|
|
994
|
+
|
|
995
|
+
### Natural Purchase Prompt Placement
|
|
996
|
+
|
|
997
|
+
**Good placements:**
|
|
998
|
+
- In a dedicated shop GUI the player opens voluntarily
|
|
999
|
+
- Contextually, when the player encounters a locked feature ("This area is VIP-only. Unlock VIP?")
|
|
1000
|
+
- After the player has played for several minutes and understands the game's value
|
|
1001
|
+
|
|
1002
|
+
**Bad placements:**
|
|
1003
|
+
- Immediately on join before the player has loaded
|
|
1004
|
+
- Every 30 seconds via popup
|
|
1005
|
+
- Blocking the screen during active gameplay
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
## 17. Anti-Patterns
|
|
1010
|
+
|
|
1011
|
+
### Client-Side Purchase Granting (Exploitable)
|
|
1012
|
+
|
|
1013
|
+
```luau
|
|
1014
|
+
-- BAD: Never do this
|
|
1015
|
+
-- LocalScript
|
|
1016
|
+
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, id, purchased)
|
|
1017
|
+
if purchased then
|
|
1018
|
+
player.Character.Humanoid.WalkSpeed = 50 -- exploiter can fire this event
|
|
1019
|
+
end
|
|
1020
|
+
end)
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
Exploiters can fire `RemoteEvent`s and manipulate client-side logic. Always grant on the server.
|
|
1024
|
+
|
|
1025
|
+
### Improper ProcessReceipt Handling
|
|
1026
|
+
|
|
1027
|
+
```luau
|
|
1028
|
+
-- BAD: Returns PurchaseGranted without actually granting
|
|
1029
|
+
MarketplaceService.ProcessReceipt = function(receiptInfo)
|
|
1030
|
+
-- "I'll grant it later"
|
|
1031
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted -- Player never gets their item
|
|
1032
|
+
end
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
```luau
|
|
1036
|
+
-- BAD: No error handling, can silently fail
|
|
1037
|
+
MarketplaceService.ProcessReceipt = function(receiptInfo)
|
|
1038
|
+
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
|
|
1039
|
+
player.leaderstats.Coins.Value += 100 -- crashes if player left or leaderstats missing
|
|
1040
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted
|
|
1041
|
+
end
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
```luau
|
|
1045
|
+
-- BAD: No idempotency check, causes duplicates on retry
|
|
1046
|
+
MarketplaceService.ProcessReceipt = function(receiptInfo)
|
|
1047
|
+
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
|
|
1048
|
+
if player then
|
|
1049
|
+
player.leaderstats.Coins.Value += 100
|
|
1050
|
+
end
|
|
1051
|
+
return Enum.ProductPurchaseDecision.PurchaseGranted -- Roblox won't retry, but if
|
|
1052
|
+
-- you returned NotProcessedYet when the player was absent and PurchaseGranted
|
|
1053
|
+
-- here, the player gets double coins if there's no receipt dedup.
|
|
1054
|
+
end
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Aggressive Popup Spam
|
|
1058
|
+
|
|
1059
|
+
Prompting purchases repeatedly annoys players and violates Roblox UX guidelines. A player who closes a prompt does not want to see it again immediately. Implement cooldowns:
|
|
1060
|
+
|
|
1061
|
+
```luau
|
|
1062
|
+
-- Minimum 60-second cooldown between prompts of the same type
|
|
1063
|
+
local lastPromptTime: { [number]: number } = {}
|
|
1064
|
+
|
|
1065
|
+
local function safePrompt(player: Player, productId: number)
|
|
1066
|
+
local key = player.UserId
|
|
1067
|
+
local now = os.time()
|
|
1068
|
+
|
|
1069
|
+
if lastPromptTime[key] and now - lastPromptTime[key] < 60 then
|
|
1070
|
+
return -- too soon, skip
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
lastPromptTime[key] = now
|
|
1074
|
+
MarketplaceService:PromptProductPurchase(player, productId)
|
|
1075
|
+
end
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### Misleading Descriptions
|
|
1079
|
+
|
|
1080
|
+
Do not describe a GamePass as "2x Everything" if it only doubles coins and not XP. Do not show a product giving 1,000 coins in the icon but actually grant 100. Roblox can remove misleading assets, and players will leave negative reviews.
|
|
1081
|
+
|
|
1082
|
+
### Hiding Costs
|
|
1083
|
+
|
|
1084
|
+
Never make the total cost of engagement unclear. If your game has a "Battle Pass" that requires buying 10 tiers at 50 Robux each, make the full 500 Robux cost visible upfront rather than drip-feeding 50 Robux prompts.
|