mcpField
R
roblox-monetization

roblox-monetization

github.com/flatbedjDevelopment055 installsUpdated 6w ago
Repository

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

  1. 01Handle ProcessReceipt safely
  2. 02Save rewards and receipts atomically
  3. 03Roll back grants when saves fail
  4. 04Grant Gamepass perks from live ownership
  5. 05Check purchase-field save/load parity

Use cases

  1. 01Implement retry-safe Developer Product purchases
  2. 02Prevent lost rewards after save failures
  3. 03Recheck permanent perks when players join
  4. 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-monetization

After installing, type/roblox-monetizationin Claude Code to confirm it loaded.

Details

From the project README

Roblox 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 as inventory[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.
  • ProcessReceipt is a WRITE-ONLY callback — you can't read or invoke it to test (get isn'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] = true is set before save() 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 on PromptGamePassPurchaseFinished. Persist a state.<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.mega flag 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 robux number 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.

Comments

Markdown supported