game-iap
Skill1 starsChanged 52 days agoMITView on GitHub
Implement in-app purchases in a game or app — Google Play Billing, Apple StoreKit 2, one-time unlocks, consumables, subscriptions, entitlement caching for offline play, restore, refund handling, server-side receipt validation, sandbox testing, and the store-policy rules that get IAP builds rejected. Use when adding, debugging, or reviewing purchases, entitlements, paywalls, "remove ads" products, or premium unlocks.
In this repository4 other files it gives its agents.
- app-authSkill
- game-adsSkill
- store-listingSkill
- store-releaseSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 65
Keep products in a data file (JSON), not in code, with: id, display name key, description key, type, fallback price, and what it unlocks.
- Distinctive: line 79
Never display a hard-coded price.
- Distinctive: line 120
Do not build a link-out flow globally on the strength of a US headline.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Game IAP
Purchases are the part of a game most likely to lose real money to a bug, and the part least covered by tests because the store is remote and asynchronous. This skill gives the architecture that makes IAP testable, the platform APIs, and the policy rules that cause rejection.
Decide first
Answer these three before writing code. They determine everything else.
| Question | Consequence |
| Consumable, non-consumable, or subscription? | Non-consumable (level pack, remove-ads) → entitlement, must survive reinstall via restore. Consumable (gem pack) → must be consumed after granting or the user can never rebuy. Subscription → renewal state, grace periods, a server is effectively mandatory. |
| Does the game have a server? | No server → entitlements live in local save and the store is the only source of truth. Yes → validate receipts server-side and keep entitlement server-side; the client becomes a cache. |
| Must purchased content work offline? | Yes → you must cache entitlements locally and define what happens when the store is unreachable (see Offline and refunds). This is the single most common source of angry reviews. |
Never gate content on a network call at launch. A player who paid and boarded a plane must still have their content.
Core architecture
One interface, three implementations. This is the whole trick — it makes IAP unit-testable without a store, and it is what lets you ship a desktop or web build that simply sells nothing.
StoreBackend (interface, async via callbacks/signals)
├── PlayBillingBackend — Android
├── StoreKitBackend — iOS
├── FakeStoreBackend — tests, debug builds, screenshots
└── NullBackend — platforms where you do not sell (desktop, web)The interface needs exactly five outbound events and four inbound calls:
events: purchase_finished(sku, success, error)
purchases_synced(owned_skus[]) # the store's complete list
sync_failed(error) # offline — do NOT clear entitlements
prices_loaded({sku: localized_price})
availability_changed(available)
calls: start() # connect, then immediately sync + load prices
purchase(sku)
restore() # re-query owned products
is_available()purchases_synced carrying the complete list (not a delta) is what makes refunds work: when
the store answers, it replaces local state rather than merging into it.
Full worked implementation, including the reconciliation edge cases:
references/reference-implementation-godot.md
Product catalog as data
Keep products in a data file (JSON), not in code, with: id, display name key, description key,
type, fallback price, and what it unlocks. Then:
is_covered(sku)answers "does the player already have this?" including bundles — asupporter_bundlemust coverfull_unlock, which must covergrade_2..grade_5. Without a coverage rule you will sell someone content they already own.- The store, the paywall UI and the tests all read the same list, so a typo shows up once.
Product IDs are permanent. Google Play and Apple both refuse to reuse an ID after it exists, even deleted. Decide the naming scheme before creating the first one. Lowercase, underscores or dots, no version numbers in the ID.
Prices always come from the store
Never display a hard-coded price. Currency, tax-inclusive display and regional pricing are the store's job, and showing the wrong one is a policy violation in several markets.
Fallback rule when the store has not answered yet: show the local list price only for the currency you actually know (e.g. your home market), and show no price at all for everyone else — a wrong currency is worse than a bare "Buy" button.
Offline and refunds
Four states, and code must handle all four:
| State | Behaviour |
| Store reachable, sync succeeds | Store list replaces local entitlements. Refunded purchases disappear here — this is the only place a refund can be honoured. |
| Store unreachable | Keep cached entitlements untouched. Never downgrade a paying player because of a dropped connection. |
| Purchase completes while a sync is in flight | The sync result may predate the purchase. Track SKUs granted since the sync started and re-add them to the synced list, or the player loses what they just bought. |
| Purchase pending (slow card, cash payment, family approval) | Grant nothing yet. Show "pending". Google returns PENDING; grant only on PURCHASED. |
The third row is the bug nobody writes a test for. Write the test.
Platform notes
- Android —
references/android-play-billing.md. Play Billing Library 8+ is mandatory for new uploads and updates since 31 Aug 2026; latest is 9.1.0. v8 renamed "in-app items" to "one-time products" and removedquerySkuDetailsAsync,queryPurchaseHistoryand the no-argenablePendingPurchases. - iOS —
references/ios-storekit.md. StoreKit 2 (Product,Transaction,Transaction.updates) is the default; it verifies signatures for you. You must run aTransaction.updateslistener for the whole app lifetime or you will drop purchases made outside your flow. - Server validation —
references/server-receipt-validation.md. Required the moment a purchase grants something a cheater would want. Play: Developer API + Real-time developer notifications (RTDN). Apple: App Store Server API + Server Notifications V2.
Policy rules that cause rejection
- Digital content must use the platform's IAP. Both stores. Taking Stripe/PayPal/bank transfer for in-game content gets the app removed. (Physical goods and real-world services are the exception, and must not use IAP.)
- US App Store only, since May 2025: apps may include buttons and links to external purchase pages with no entitlement required. Outside the US storefront the old restriction still applies. Do not build a link-out flow globally on the strength of a US headline.
- A restore path is mandatory on iOS for non-consumables. A visible "Restore purchases" button. Rejections under 3.1.1 for a missing one are routine.
- Kids' apps: purchase entry points must sit behind a parent gate, and the gate must not be
solvable by a young child (no simple "tap the biggest number"). See
app-authandstore-listingfor the Families/Kids declarations that follow from selling to children. - Do not promise content you gate later. Changing a bought unlock into a subscription is a refund event and a review-bomb event.
Testing
Ranked by what actually catches bugs:
- FakeStoreBackend unit tests — every row of the offline/refund table above, plus double-purchase, purchase-while-owned, and cancelled flows. These run in CI with no store.
- Android closed testing / license testers — real Play dialogs, no charge. License testers are set at the account level in Play Console, not per build. Purchases still have to be refunded/consumed between runs for consumables.
- iOS sandbox / StoreKit configuration file — the
.storekitfile lets you test in the simulator without App Store Connect at all; do both, because the file does not catch App Store Connect misconfiguration.
Checklist before you call IAP done: references/pre-launch-checklist.md
Sources
Specs verified 2026-09-24:
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

