Monetization — Setup Checklist & What's Done
2026-07-20 · updated (from the 2026-07-18 snapshot) · Phase-2 store/RevenueCat config, turnkey · plus a complete record of the code shipped. Boxes tick off and persist in this browser; dashed code is click-to-copy.
Top half is the step-by-step App Store Connect + RevenueCat setup — the biggest remaining non-code block — with the exact IDs to create. Tick boxes as you go (saved locally). Bottom half is the exhaustive list of everything already built in code — the full 6-PR / 59-commit program (backend #514 + #515, admin #163, iOS #309 + #311 + #312) — so the two halves together are the full picture. Confirm the pending product sign-offs (prices, discount duration) before doing section D onward.
Identifiers reference (create exactly these)
Click any dashed value to copy. Names in bold are load-bearing — the backend matches them literally.
| Thing | Exact value | Note |
|---|---|---|
| Entitlement (Plus) | plus | Backend derives tier from this literal id. |
| Entitlement (Pro) | pro | Literal; Pro outranks Plus. |
| Products (6) | app.lissin.audio.plus.weekly app.lissin.audio.plus.monthly app.lissin.audio.plus.yearly app.lissin.audio.pro.weekly app.lissin.audio.pro.monthly app.lissin.audio.pro.yearly | Note app.lissin.audio.*, not com.*. |
| Offerings | default · save50 · save65 · winback | default = standard paywall (must be "Current"). The iOS rescue ladder requires exact save50/save65 offerings for visible discount stages; missing stages are safely skipped. winback = promo/win-back. |
| Dev webhook URL | https://api.dev.lissin.com/api/v1/billing/webhooks/revenuecat | Already configured & verified. |
| Prod webhook URL | https://api.lissin.com/api/v1/billing/webhooks/revenuecat | Set before go-live. |
| Public SDK key | appl_XpLTTWDJZdTEnqTAYxrGYTXUBCk | Already ships in the app (Beta+Release). |
A · App Store Connect — prerequisites (iOS/Billing)
B · Verify / finalize the 6 products (iOS/Billing)
Backend contract expects Plus < Pro and derives tier from entitlements, not prices — so prices are a store-side decision only.
C · The 3-day free trial (iOS/Billing)
Backend already projects it (period_type=TRIAL → trial phase + restricted plus_trial limits). You only need the App Store intro offer:
One introductory offer per subscription group, per user. A user who consumes the 3-day trial is no longer intro-eligible in that group — which directly affects the rescue offers below.
D · Rescue offers — 50% then 65% (iOS/Billing + Product)
For never-subscribed users, Apple promotional offers don't apply (they require a current/previous subscription). The PRD's recommended mechanism is introductory pricing on the standard products, or offer codes. Because of the one-intro-per-group rule, decide the mechanism with product first:
The backend records presented_offering_id/offer_code per event and the iOS rescue ladder drives standard→save50→save65 — persisting/resuming its stage, resetting on purchase, and committing a stage only after the exact offering loads. Dismissal routing was corrected in c6305160; missing rescue offerings are skipped, never replaced with standard prices.
E · Promotional & win-back offers (iOS/Billing)
F · RevenueCat dashboard (iOS/Billing)
G · RevenueCat Placements (iOS/Billing)
Create Placements matching the iOS placement registry + backend denial placements, so paywalls can change remotely without an app release:
onboarding
lockedEpisode
secondShow
generationAllowance
higherRecurringAllowance
documentAttach
remix
cadenceChange
proSources
transcript
speed
soundscape
exploreTimeLimit
planBilling
H · Webhooks (dev + prod) (iOS/Billing + Backend)
I · Sandbox end-to-end verification (QA)
J · Go-live gate (Product + Eng)
What's done — exhaustive record
Backend merged through #522 into dev (+ #523/#524 fixes); Admin through #168 into dev; iOS through #317 into develop. Everything remains observe-only / additive / flag-gated until rollout. The dismissal correction c6305160 is pushed separately and must still be merged before the offer ladder is release-ready.
Backend — PR #514 (17 commits), how each works
| Commit | What & how |
|---|---|
2c8591e | Versioned plan contract — plan-contract.ts holds Free/Plus/Pro + plus_trial as typed, JSON-serializable data (credits, minutes, caps, speed, transcript/soundscape/remix). Confirmed numbers test-locked. |
8edc016 | plan_versions table + seeder — the contract published to the DB (one active row per variant); runtime source of truth → dashboard-editable later. npm run seed:plan-versions. |
f4adc3f | Usage ledger schema — immutable usage_ledger (CREDIT/RESERVE/RELEASE/CONSUME/EXPIRE/ADJUST) + per-window usage_accounts (base + rolloverIn − consumed − reserved). |
8a6e08b | Window math — pure UTC-anchored weekly (ISO Mon) / rolling-3-day / billing-month / trial windows + previous-window for rollover. Fully unit-tested. |
f546ffc | Quota service — balances, reserve/consume/release (idempotent, rollover-aware), and a read-only peekBalance; unlimited meters short-circuit. |
17b250a | Access phase + trial fields on billing_access (accessPhase, planVersion, trial dates, productId, offerId, environment). |
c9caf70 | Expanded /billing/access — adds phase, planVersion, serverTime, per-meter balances (read-only peek), trial window. All additive. |
17fed30 | Trial projection — webhook maps period_type=TRIAL → trial phase + window; persists plan version, product, offer, real sandbox/prod environment; EXPIRATION → 'expired'. |
fda8643 | observe() — records real consumption to the ledger (idempotent), flags would-exceed, never denies. |
2de1f2e | Wire generation + upload (observe) — batch create → one-time, schedule-intent show → recurring, upload → files. Fire-and-forget, keyed by resource id. |
4804bf4 | Wire Explore playback + SSE (observe) — debit credited audible seconds per progress receipt (standalone content); generate/live + generate/stream observe once per stream. |
9fe7dc2 | Enforcement engine — enforce() pre-flight (capability / file-size / episode-duration / quota-peek) → structured BillingEnforcementException (HTTP 402) behind per-domain flags. |
333c46b | Wire enforcement into generation (batch/live/stream) + document upload. |
4e1e491 | Docs — BILLING_ENFORCE[_DOMAIN] flags in .env.example. |
The 14 arc-commits above landed first; 3 further commits (→ 17 total) addressed every code-review comment. Re-review green. All defaults remain OFF (observe-only). 3 migrations.
Data model added
plan_versions— versioned, dashboard-editable plan definitions (active row per variant).usage_accounts— per-user, per-meter, per-window rolling balance.usage_ledger— immutable entries; dedupe by (user, meter, idempotency key); reservation lifecycle linkage.billing_accessextended — accessPhase, planVersion, trialStartsAt/EndsAt, productId, offerId, environment.- 3 migrations authored (manual SQL +
migrate deploy); apply on dev-deploy or via docker locally.
Contracts established
Expanded GET /billing/access (additive, all optional)
phase, planVersion, serverTime, trial{startsAt,endsAt}|null,
balances.{recurringGeneration, oneTimeGeneration, explorePlayback(sec), fileUploads}
= { limit|null, remaining|null, used, reserved, rolloverBalance, resetAt|null, window }
Structured 402 denial (when a domain is enforced)
{ error:"billing_denied", code, message, requiredTier?, meter?, remaining?, limit?,
resetAt?, maxMinutes?, maxBytes?, placement? }
code ∈ CAPABILITY_REQUIRED | QUOTA_EXHAUSTED | DURATION_LIMIT | FILE_LIMIT | ACCESS_UNKNOWN
Enforcement flags (all default off → observe-only)
BILLING_ENFORCE · BILLING_ENFORCE_GENERATION · BILLING_ENFORCE_FILES · BILLING_ENFORCE_PLAYBACK · BILLING_ENFORCE_CAPABILITIES — per-domain overrides master (incl. explicit false).
Backend — PR #515 · grandfathering (4 commits)
- Grandfather cohorts — rule-based cohorts (e.g. legacy pre-launch users) resolved to a grant overlay so existing users keep access when gates flip.
- Hand-picked user IDs — an explicit allow-list alongside cohorts for one-off grants.
- Campaign audit — every grandfather run is recorded (who / when / scope) for review.
- Safe by construction — idempotent, bounded, and overlay-safe (never downgrades a paying user; layers on top of real entitlements).
- Verified: 29/29 billing + grandfather tests; all review comments fixed.
Admin — PR #163 (11 commits)
- Grants / revoke / access-snapshot — per-user billing grant + revoke, plus a read-only access snapshot showing projected tier / phase / balances.
- Cohort grandfathering UI — drive the #515 cohorts from the dashboard.
- Users-page multi-select — select N users → bulk "Grandfather selected (N)" action.
- Grandfathering audit page — browse the campaign audit trail.
- All of the above
billing.manage-gated;tsctype-check +next buildclean.
iOS — PR #309 (9 commits)
| Commit | What |
|---|---|
772daf4e | Decode the expanded /billing/access contract (optional fields, lenient); extend PlanLimits. |
d8d93c06 | Align the local fallback plan to the credit-based Phase-0 numbers + a plusTrial variant. |
7dd7e34b | Tests for expanded-contract decoding (with + without new fields). |
a5a174e3 | Decode the 402 billing_denied body → APIError.billingDenied + PaywallPlacement(billingDenial:) mapping (dormant until flags flip). |
The 4 arc-commits above landed first; 5 further commits (→ 9 total) addressed every review comment. Re-review green.
iOS — PR #311 · offer-stage rescue ladder (6 commits)
- Rescue ladder standard → 50% → 65%: each closed paywall drops the user to the next downsell (one exit offer per stage).
- Durable stage — the current stage persists and resumes across launches, and resets on purchase.
- Safe commit — a stage is committed only after its offering actually loads, so a missing offering never strands the user mid-ladder.
- RC offerings
default/save50/save65; an absent rescue offering is skipped safely. - Re-review COMPLETED / SUCCESS.
iOS — PR #312 · Phase-4 gates, trial & live balances (12 commits)
- Phase-4 gates — transcript, playback speed, soundscape, Explore time limit, and file size + count gates wired to the contract.
- Trial banner — surfaces the 3-day restricted Plus trial state.
- Usage nudges — approaching-limit nudges driven by the per-meter balances.
- Plan & Billing live balances — the settings screen shows real remaining / used per meter from
/billing/access. pro_sourcesintentionally not wired — no such feature exists yet.- Re-review COMPLETED / SUCCESS.
Verification & safety
- Backend
nest buildclean; 29/29 billing + grandfather tests pass across #514 and #515. - iOS
xcodebuildBUILD SUCCEEDED (Xcode 26.5) for the full stack; 356 tests across #309 + #311 + #312. - Admin
tsctype-check +next buildclean (#163). - Webhook idempotency (dedupe by provider event id) + out-of-order stale-event guard — unit-tested. Grant-as-overlay + downgrade guard + TRANSFER stand-down — unit-tested. Grandfather runs idempotent / bounded / overlay-safe.
- Every code-review comment fixed & pushed; iOS #311 & #312 re-reviews COMPLETED / SUCCESS. Everything observe-only / additive / flag-gated; none of the 6 PRs merged yet.
Remaining — user / non-code (not shipped in these PRs)
- Merge the 6 PRs — iOS in order
#309 → #311 → #312; backend one at a time (no concurrency guard on prod migrations). - RevenueCat / App Store catalog — create the
default+save50+save65offerings, the 3-day trial intro offer, the placements, and the prod webhook (the top half of this doc). - Product price / discount sign-offs — standard prices + rescue discount duration.
- Flip enforcement — turn on
BILLING_ENFORCE_*per domain → QA → grandfather cohort → gradual % ramp.
Still deferred (code, separate future work)
pro_sourcesgate (no such feature yet); backend offer-state / nudge-decision API.- Explore-playback signed-URL denial; scheduler per-episode recurring metering; atomic reserve/consume wiring.
Sources: lo/lissin-monetization-plan/ (PRD docs 01–08) · code from PRs #514 + #515 (backend→dev), #163 (admin→dev), and #309 + #311 + #312 (iOS→develop). Companion reports in lo/: monetization-implementation-final-report.html, monetization-requirements-status-report.html.