roblox-opencode 1.0.0 → 1.0.1

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.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +863 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,1084 +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.
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.