agentleFS
thomaslimitedthomaslimited/game-shipping-skills/skills/game-iap/SKILL.md

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.

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
  1. Very specific:

    Keep products in a data file (JSON), not in code, with: id, display name key, description key, type, fallback price, and what it unlocks.

    line 65
  2. Distinctive:

    Never display a hard-coded price.

    line 79
  3. Distinctive:

    Do not build a link-out flow globally on the strength of a US headline.

    line 120

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

QuestionConsequence
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 — a supporter_bundle must cover full_unlock, which must cover grade_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:

StateBehaviour
Store reachable, sync succeedsStore list replaces local entitlements. Refunded purchases disappear here — this is the only place a refund can be honoured.
Store unreachableKeep cached entitlements untouched. Never downgrade a paying player because of a dropped connection.
Purchase completes while a sync is in flightThe 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 removed querySkuDetailsAsync, queryPurchaseHistory and the no-arg enablePendingPurchases.
  • iOS — references/ios-storekit.md. StoreKit 2 (Product, Transaction, Transaction.updates) is the default; it verifies signatures for you. You must run a Transaction.updates listener 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-auth and store-listing for 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:

  1. 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.
  2. 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.
  3. iOS sandbox / StoreKit configuration file — the .storekit file 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:

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.