audit-monetization-iap · git:20260923.68c095e · 2026-09-23 · sha256 084f247dd38379ec
audit-monetization-iap git:20260923.68c095eA
Immutable. This exact content never changes and is served at /api/v1/blob/084f247dd38379ec.
--- name: audit-monetization-iap description: > Read-only audit of mobile IAP and subscriptions (StoreKit 2, Play Billing, RevenueCat): receipt validation, restore, lifecycle sync, grace periods, entitlements. Use when "audit our IAP" or "restore purchases broken". Web → audit-payment-system. license: MIT effort: high --- # audit-monetization-iap — Store billing & entitlements **Degree of freedom: MIXED** — Phases 0–1 `[HIGH freedom]`; Phase 2 sandbox `[LOW freedom — run exactly]`, sandbox only: a real purchase charges a real card and cannot be undone from here. Read-only. Entitlements must match what users paid for: no unlock-without-pay, no paid-but-locked, no double-charge, no lost purchase on reinstall. **The two revenue killers are client-trusted entitlements (piracy) and broken restore (refunds + 1-star reviews).** > **Present findings. Do not patch store/server code until approved.** > Never test against real billing. ## This skill vs neighbors | Skill | Owns | |---|---| | **audit-monetization-iap** (this) | App Store / Play IAP, restore, store notifications | | `audit-payment-system` | Web money-movement, Stripe, ledgers, PCI | | `plan-mobile-readiness` | Submission paperwork (IAP via official billing is a checkbox) | | `plan-aso` | Store listing conversion, not receipt validation | | `plan-privacy-compliance` | Purchase-data in privacy labels | Do **not** fire for "Stripe webhook / double charge on the website" → `audit-payment-system`. ## How to reason 1. **Observe** — quote where "is Pro?" is decided and whether a receipt/token is verified server-side 2. **Interpret** — can a client lie, or can a paid user lose access? 3. **Classify** — client-trust / missing restore / lifecycle hole / correct 4. **Severity** — client-trusted entitlement or broken restore = Critical ## Worked example > **Observe:** `isPro` reads `RevenueCat.getCustomerInfo()` on-device only; > no App Store Server Notification handler. > **Interpret:** a patched client can unlock Pro; refunds never revoke. > **Classify:** client-trust + missing lifecycle sync. > **Severity:** Critical. > **Finding:** entitlements | client-only | Critical | `lib/pro.ts:18` | > verify on server + ASN V2 / RTDN --- ## Phase 0 — Detect the billing setup [HIGH freedom] - Layer: raw StoreKit 2 / Play Billing, or RevenueCat / Adapty / Glassfy - Entitlement check: client-only vs server-verified - Backend store of subscription state vs trust-the-SDK-at-runtime - Products: consumable, non-consumable, auto-renewing sub, trial / intro --- ## Phase 1 — Correctness [HIGH freedom] **Receipt / token validation** — Server-side (App Store Server API / Play Developer API). Client-trusted "purchase succeeded" is Critical. StoreKit 2 signed transactions still need verification, not just decode. **Source of truth** — One place answers "does this user have Pro?" Backend must hear App Store Server Notifications V2 / Play RTDN for cancel, refund, billing failure — not only check at purchase time. **Restore** — Discoverable Restore (Apple requires it). Recovers after reinstall / new device / re-login. **Lifecycle** — trial, active, grace (billing retry), on hold, paused (Android), expired, refunded, up/downgrade. Missing grace locks paying users out. Missing revoke leaves refunded users in. **Trials** — One trial per user/family; no farm. UI must not promise a trial the store will refuse. **Cross-platform** — iOS + Android (+ web): one purchase unlocks via backend, or the user is charged twice. **Prices** — From the store (localized), never hardcoded. **Edges** — Interrupted purchase resumes; sandbox vs prod receipt mix-up; clock-gamed trials. --- ## Phase 2 — Sandbox only [LOW freedom — never real billing] Exercise: fresh purchase, restore on clean install, cancel → access ends at period end, sandbox refund → revoke, trial → convert, interrupted purchase. Confirm backend entitlement flips. --- ## Definition of Done - [ ] Layer + products inventoried - [ ] Every entitlement traced to server verify, or client-trust flagged Critical - [ ] Store notifications wired or gap logged - [ ] Restore present and sandbox-verified - [ ] Lifecycle grid complete - [ ] Cross-platform double-charge checked - [ ] Prices from store - [ ] Nothing patched ## Self-critique before reporting [LOW freedom — do not skip] 1. **Evidenced** — file:line for the entitlement SoT, not "RevenueCat probably verifies" 2. **No real charges** — sandbox only 3. **Severity justified** — Critical = unlock-without-pay or paid-but-locked 4. **Right owner** — web Stripe → `audit-payment-system` 5. **Restore probed** — "button exists" is not restore-verified ## Output format 1. **Setup** — layer, products, entitlement SoT 2. **Findings** — issue | severity | `file:line` | fix shape 3. **Lifecycle grid** — state × handled? 4. **Sandbox log** — scenario | expected | actual 5. **Fix plan** — server verify + restore first ## Related - `audit-payment-system` — web / Stripe / ledger - `plan-mobile-readiness` — store review gates - `plan-aso` — listing, not billing correctness - `plan-privacy-compliance` — purchase data disclosure