About roblox-monetization
A Roblox monetization implementation skill for developers handling Developer Products and Gamepasses. It emphasizes idempotent receipts, atomic persistence, and rollback to prevent lost or duplicated purchases.
Key features
- 01Handle ProcessReceipt safely
- 02Save rewards and receipts atomically
- 03Roll back grants when saves fail
- 04Grant Gamepass perks from live ownership
- 05Check purchase-field save/load parity
Use cases
- 01Implement retry-safe Developer Product purchases
- 02Prevent lost rewards after save failures
- 03Recheck permanent perks when players join
- 04Verify purchase data save/load consistency
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ flatbedj/ roblox-skills --skill roblox-monetizationAfter installing, type/roblox-monetizationin Claude Code to confirm it loaded.
Details
From the project READMERoblox monetization: safe ProcessReceipt + Gamepasses
Real Robux is unforgiving. A subtle bug here means players get charged and receive nothing (or get infinite free stuff). These are the patterns that keep it correct.
Developer Products: MarketplaceService.ProcessReceipt
The callback runs when a Robux DevProduct purchase completes. It MUST be idempotent (Roblox re-delivers until you return PurchaseGranted) and atomic (grant + dedupe recorded in the SAME durable save).
Commit-and-rollback shape (use for every branch):
local key = tostring(receipt.PurchaseId)
if state.receipts[key] then return Enum.ProductPurchaseDecision.PurchaseGranted end -- already granted
-- snapshot what you'll change, apply the grant, THEN record the receipt:
state.cash += amount
state.receipts[key] = true
if not PlayerData.save(player) then -- durable save failed → undo everything
state.cash -= amount
state.receipts[key] = nil
return Enum.ProductPurchaseDecision.NotProcessedYet -- Roblox will re-deliver later
end
PlayerData.sync(player)
return Enum.ProductPurchaseDecision.PurchaseGranted
Rules that prevent the classic bugs:
- Only grant FUNGIBLE things inside ProcessReceipt (currency, or a durable token count the client redeems). Do NOT roll a random non-fungible reward (a specific card) inside ProcessReceipt — a save failure + rollback + re-delivery causes data loss and false "big win" events. If you must grant a specific item, grant it + snapshot the exact delta so rollback fully reverses it (decrement the copy, delete the entry if it hits 0).
- Record the PurchaseId in the SAME
save()that persists the reward, and roll BOTH back together on save failure.save()must return a success bool. - NEVER use a config TABLE as a table key. A frequent BLOCKER bug: a config entry like
{ id = "x", count = 2 }used directly asinventory[def] = .... DataStore can't serialize table keys → the save throws → the whole grant rolls back → NotProcessedYet re-delivers FOREVER → the player is charged and never gets anything. Always extract the string:local id, n = def.id, def.count. ProcessReceiptis a WRITE-ONLY callback — you can't read or invoke it to test (getisn't available). Verify the grant logic by static reading + pure checks (e.g. confirm the eligible pool is non-empty), not by calling it.- Minor known window:
receipts[key] = trueis set beforesave()yields; a concurrent re-invocation could see the flag mid-save. Standard, low-probability; note it, don't over-engineer.
Gamepasses (permanent perks)
- Derive ownership from LIVE ownership, not just a saved flag: on join,
MarketplaceService:UserOwnsGamePassAsync(userId, passId); grant instantly onPromptGamePassPurchaseFinished. Persist astate.<pass>bool as a cache but re-verify on join. - An "all-access / mega" bundle that grants EVERY pass (now and future) is best done with a persistent
state.megaflag that re-applies every pass flag on each join — so passes added later unlock automatically.
Config & dashboard reality
productId == 0/gamePassId == 0= inert placeholder: show a "not set up yet" note instead of prompting, so the shop is safe before ids exist.- A config
robuxnumber is DISPLAY ONLY — the real charge is whatever price is set on the DevProduct/Gamepass in the Creator Dashboard. Set the real price there; the in-game label is just cosmetic. - Regional pricing means hardcoded in-game price labels DRIFT from what non-base-region players are charged (the purchase PROMPT is always correct). For accuracy, fetch live prices via
GetProductInfo/GetGamePassProductInfo. - Launch checklist the dev must do (you can't): create each DevProduct/Gamepass in the dashboard, paste real ids into config, and save/publish the place.
Persistence of purchase fields
Every purchase field (receipts, currency, pass flags, tokens, invitedBy) needs full save/load parity — a field loaded but not serialized (or vice-versa) means re-claimable one-time gifts or lost purchases (here the leak costs real money, so it's worth an explicit check). The general save/load/reset checklist is in roblox-studio-workflow.