audit-monetization-iap — Store billing & entitlements
Degree of freedom: MIXED — Phases 0–1 [HIGH freedom]; Phase 2 sandbox
[LOW freedom — run exactly]. Never test against real billing.
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
- Observe — quote where "is Pro?" is decided and whether a receipt/token is verified server-side
- Interpret — can a client lie, or can a paid user lose access?
- Classify — client-trust / missing restore / lifecycle hole / correct
- Severity — client-trusted entitlement or broken restore = Critical
Worked example
Observe:
isProreadsRevenueCat.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. Never real billing.
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]
- Evidenced — file:line for the entitlement SoT, not "RevenueCat probably verifies"
- No real charges — sandbox only
- Severity justified — Critical = unlock-without-pay or paid-but-locked
- Right owner — web Stripe →
audit-payment-system - Restore probed — "button exists" is not restore-verified
Output format
- Setup — layer, products, entitlement SoT
- Findings — issue | severity |
file:line| fix shape - Lifecycle grid — state × handled?
- Sandbox log — scenario | expected | actual
- Fix plan — server verify + restore first
Related
audit-payment-system— web / Stripe / ledgerplan-mobile-readiness— store review gatesplan-aso— listing, not billing correctnessplan-privacy-compliance— purchase data disclosure