# Self-Serve Pro/Max Purchase — Implementation Plan **Status:** Phases 1–4 BUILT + INSTALLED + LIVE on immense-voyage.local (relay 0.2.121, app 0.2.153) as of 2026-06-08. Bitcoin rail end-to-end test pending; card rail awaits operator Zaprite config. Phase 5 (SMTP expiry reminders) remains. Follows the core decoupling (the relay owns tiers, keyed by user-id) — this lets users buy their own Pro/Max instead of the operator granting them by hand. **Phase 4 (cards via Zaprite) — DONE.** Symmetric with the Bitcoin rail (one-time prepaid checkout, NOT Zaprite recurring): relay `zaprite-client.js` (POST /v1/orders + GET /v1/orders/:id), `POST /relay/tier-zaprite-order` (operator-keyed), `POST /relay/zaprite/webhook` (re-fetch-to-verify — no signature needed; both rails land at extendUserTier). Config: `relay_zaprite_{api_key,base_url,currency}` + `relay_tier_prices_fiat_cents_json` (default $21/$42), set via the new "Set Zaprite Connection" StartOS action. `/api/billing/buy` gained `method:"card"`; tier-plans reports `card_available` + fiat prices; UI shows "Pay by card · $21" only when Zaprite is configured. Operator TODO: run "Set Zaprite Connection" (paste API key) + register the webhook at `https:///relay/zaprite/webhook` in Zaprite. ## Decisions (from Grant) - **Prepaid periods, NOT auto-recurring.** A user pays for a fixed period (default **30 days**) of Pro/Max. Near expiry they get an email reminder and pay again to extend. At expiry, the tier drops to Core. No stored payment method, no card-vault / dunning. - **Two payment rails:** - **Bitcoin / Lightning via BTCPay** — the *preferred* path. Relay already has `btcpay-client.js` (createInvoice + webhook HMAC) used for credit purchases; extend it for tier purchases. - **Cards via Zaprite** — secondary. Grant has a Zaprite org + API + webhooks. New integration. - **UI:** the main pill button is **"Pay with Bitcoin"** (opens a BTCPay invoice). Directly below it, a smaller **"Pay by card"** link (opens a Zaprite checkout). - **Expiry reminders via email.** Set up SMTP (Amazon SES per the Start9 recommendation) and send an automated "your Pro/Max expires in N days" email. Recaps already has an SMTP transport (magic-link emails). - **Prices:** from the relay's `relay_tier_prices_usd_json` (today Pro $5 / Max $15 per period), USD-denominated, paid in sats for BTCPay. ## Phases (each shippable on its own) ### Phase 1 — Relay: prepaid tier model + expiry enforcement (foundation) Rail-agnostic. Everything else depends on it. - `setUserTier({userId, tier, periodDays})` → set tier, set `subscription_expires_at`. **Extend from the current expiry** if the user is already active (so paying early adds time, doesn't reset it). - **Enforce expiry:** when the relay resolves a user's tier (identityTier / the metered-route gate), treat `subscription_expires_at < now` as Core. Add a lazy check + a periodic sweep so expired users actually drop. - Keep the operator-grant path (`/relay/user-tier`) working — it's the comp tool. A manual grant can set no-expiry (operator comp) vs a purchase sets a dated period. ### Phase 2 — Bitcoin/Lightning purchase (BTCPay) - Relay: `POST /relay/tier-invoice` (operator-key authed) — body `{user_id, tier, period_days}` → `createInvoice` with metadata `{kind:"tier_subscription", userId, tier, periodDays}` → returns the checkout URL + invoice id. - Relay webhook: on a settled `tier_subscription` invoice → `setUserTier` (extend by periodDays). (Mirrors the existing credit-purchase webhook branch.) - Recaps: a `pending_purchases`-style record + the settle→poll→cache-tier loop (reuse the credit-purchase machinery). On settle, refresh `/api/license-status` so the badge flips to Pro/Max. ### Phase 3 — Purchase UI - Tier picker (Pro / Max, price, "30 days") with the **"Pay with Bitcoin"** pill + **"Pay by card"** link. Reuse / replace the existing buy modal. - Bitcoin → opens the BTCPay checkout (Phase 2). Card → opens Zaprite (Phase 4). ### Phase 4 — Cards via Zaprite - Relay (or Recaps): create a Zaprite checkout for the tier (Grant's org + API), metadata carrying `{userId, tier, periodDays}`. - Zaprite webhook → verify signature → `setUserTier` (extend). Same landing point as the BTCPay webhook. - Wire the "Pay by card" link to it. ### Phase 5 — Expiry reminder emails - SMTP via SES (Grant sets up SES + StartOS System SMTP; Recaps' transport already exists). - Periodic job: find users with `subscription_expires_at` in ~N days, email a "renew" notice with a link back to the purchase UI. Idempotent (don't double-send). ## Notes / open defaults (sensible unless Grant says otherwise) - Period = 30 days. Grace = none beyond the advance email (downgrade on expiry). Extend-from-current-expiry on early renewal. - Relay stays **`make install` only** (private — never registry-deploy). - The operator-key path authenticates Recaps→relay for invoice creation, the same as the tier-grant flow.