{"id":"moxxum/monetize","name":"monetize","scope":"moxxum","platform":"roblox","description":"ProcessReceipt router with purchase-ID idempotency ledger, GamePass cache, prompt helpers","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":["The package archive does not include its license text; the license is declared in its manifest metadata."],"licenseVerified":false,"dependencies":{},"integrity":"f057d13fabc473d38de4c9db0dc76ba40bd30facb70756751779c1732adacb82","likes":0,"downloads":0,"install":"forest install moxxum/monetize","url":"https://forest.dev/p/roblox/moxxum/monetize","files":"https://api.forest.dev/ai/package/roblox/moxxum/monetize/files","readme":"# monetize\n\nPurchases without the classic double-grant bug. A `ProcessReceipt` router whose\nidempotency comes from a **purchase-ID ledger stored in the player's datakit\nprofile** — a receipt already in the ledger is acknowledged as granted without\nre-running the grant, so DataStore retries and rejoin-replays can never grant twice.\nPlus a GamePass ownership cache (filled on purchase/first query), and prompt helpers.\n\nServer realm: `Monetize = \"moxxum/monetize@0.1.0\"` under `[server-dependencies]`.\nMonetize owns `MarketplaceService.ProcessReceipt`; nothing else may set it.\n\n## API\n\n```lua\nMonetize.Init({\n\tGetProfile = DataKit.Get,          -- required; profile needs :Get/:Set/:IsActive\n\tLedgerPath = \"PurchaseLedger\",     -- default; add it to your datakit template or let it self-create\n\tLedgerSize = 50,                   -- purchase IDs retained\n\tOnPurchasePrompted = Analytics.PurchasePrompted,   -- optional hooks\n\tOnPurchaseCompleted = Analytics.PurchaseCompleted,\n})\nMonetize.RegisterProduct(productId, grantFn)  -- grantFn(player, receiptInfo); may yield\nMonetize.RegisterPass(passId, grantFn?)       -- grant runs on purchase during session\nMonetize.OwnsPass(player, passId)             -- cached; fails closed on API error\nMonetize.PromptProduct(player, productId)\nMonetize.PromptPass(player, passId)\n```\n\nReceipt flow: player absent or profile inactive → `NotProcessedYet` (Roblox retries).\nGrant errors → `NotProcessedYet`. Grant succeeds → purchase ID recorded in the ledger,\n**then** `PurchaseGranted`.\n\n## Example\n\n```lua\nlocal DataKit = require(ServerPackages.DataKit)\nlocal Monetize = require(ServerPackages.Monetize)\n\nMonetize.Init({ GetProfile = DataKit.Get })\n\nMonetize.RegisterProduct(GameConfig.Products.SmallCoinPack.Id, function(player, receipt)\n\tlocal profile = DataKit.Get(player) -- guaranteed active inside a grant\n\tprofile:Set(\"Coins\", profile:Get(\"Coins\") + 100)\nend)\n\n-- Pass-gated perk: check at point of use, no grant needed\nif Monetize.OwnsPass(player, GameConfig.Passes.Vip.Id) then ... end\n```\n\n## Test\n\n`rojo build test.project.json -o test.rbxl`, open in Studio, Play Solo, expect\n`[monetize-test] ALL PASS` (receipt idempotency, unknown products, ledger cap).\n","readmeTruncated":false}