---
path: /billing
surface: customer
domain: account
auth: signed-in
source: apps/oshun/web/src/app/billing/page.tsx
status: walked
last_walked: >
  2026-06-29 billing-index direct-route addendum — Playwright real-dev-infra
  coverage for signed-in /billing render, anonymous proxy redirect, rail hrefs,
  44 px tap targets, no horizontal overflow at 390 px, standalone display-mode
  launch, and real service-worker offline replay of the cached static billing
  document. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §202;
  spec: apps/oshun/web/e2e/billing-index.spec.ts. 2026-06-29 billing-rail
  telemetry addendum — the same real-dev-infra spec now asserts the static
  billing card, rail list, crypto/profile/AAA rail links, and billing steward
  link expose no telemetry hook attributes and dispatch no
  `oshun-analytics:event` when activated. Evidence:
  WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §216; spec:
  apps/oshun/web/e2e/billing-index.spec.ts.
---

# Billing

## Purpose

The top-level account-billing landing surface — a calm directory that routes the
user to the rail that fits where they came from. Today only the Aje crypto rail
is wired (`/billing/crypto`); card and bank rails are explicitly "follow in
subsequent releases" per the in-page copy. Users arrive from the
`Manage billing` affordance in `/profile`.

## Entry points

- Cross-domain link from `/profile` billing row ("Manage billing" affordance)
- Direct URL / bookmark — yes; auth required (`proxy.ts` redirects no-session to
  `/welcome`; `/billing` is not in `PUBLIC_PATHS`/`PUBLIC_PREFIXES`)
- Alias hops: `/settings` and `/account` rewrite to `/profile`, which links here
- Upgrade flow that lands a user on a payment decision (`/aaa-upgrade`)
- Email link from invoice issuance (which more typically deep-links straight to
  `/billing/crypto?invoice=…`)

## Layout regions

`page.tsx` is a static server component (no data fetch, no `LWebShell` — it uses
raw Lilith CSS variables `--l-bg`, `--l-paper`, `--l-ink`, `--l-accent`,
`--l-rule`, `--l-muted` directly). A single `<main data-billing-index>` centers
one bordered card.

- **Header**: none (no shell header; bare `<main>` on `--l-bg`)
- **Hero / above-the-fold**: the card — eyebrow `Account · billing`, serif H1
  "Your subscription, kept calm.", and a muted intro paragraph
- **Main**: a `<ul>` of three rail/destination links
- **Footer / sticky bottom**: a muted help line with a `mailto:` steward link

## States

- [x] **Loading** — N/A because the page is static (no `await`, no data fetch);
      renders instantly
- [x] **Empty** — N/A because content is hardcoded copy, not data-driven
- [x] **Populated (short)** — N/A; the three links are fixed
- [x] **Populated (long)** — N/A; fixed link list
- [x] **Error (recoverable)** — N/A; no query to fail
- [ ] **Error (unrecoverable)** — no segment `error.tsx` exists under
      `apps/oshun/web/src/app/billing`; a static render crash would fall through
      to the nearest parent/global boundary
- [x] **Offline** — static HTML; SW-cached shell renders the links offline; the
      `mailto:` and outbound links still resolve when connectivity returns
- [x] **Gated / anonymous** — `proxy.ts` redirects no-session users to
      `/welcome` before this renders; verified by `billing-index`
- [x] **Standalone PWA** — bare `<main>` uses `100svh` and safe-area padding;
      the 390 px standalone launch is verified for display-mode, containment, 44
      px tap targets, and no horizontal overflow

## Interactions

- [x] **Pay with crypto (USDC, USDT, ETH, BTC) →** (link)
  - Function: navigates to `/billing/crypto` (the only wired rail)
  - Keyboard: in document order, first focusable; Enter activates
  - Screen reader: announces "Pay with crypto (USDC, USDT, ETH, BTC), link"
  - Touch target: verified ≥ 44 px tap height at a 390 px phone viewport
  - Mobile (≤ 640 px): full-width card, links stack
  - Offline behavior: the billing index itself replays from the real service
    worker cache after an authenticated online visit; the dynamic crypto target
    waits for connectivity
  - Telemetry: explicitly absent; no hook attributes and no
    `oshun-analytics:event` dispatch on activation
- [x] **Manage your account profile →** (link)
  - Function: navigates to `/profile`
  - Screen reader: announces "Manage your account profile, link"
  - Telemetry: explicitly absent; no hook attributes and no
    `oshun-analytics:event` dispatch on activation
- [x] **Upgrade to the AAA Studio plan →** (link)
  - Function: navigates to `/aaa-upgrade`
  - Screen reader: announces "Upgrade to the AAA Studio plan, link"
  - Telemetry: explicitly absent; no hook attributes and no
    `oshun-analytics:event` dispatch on activation
- [x] **billing@oshun.app** (mailto link)
  - Function: opens the user's mail client to the billing steward
  - Screen reader: announces the address as a link
  - Offline behavior: hands off to OS mail client; composition works offline
  - Telemetry: explicitly absent; no hook attributes and no
    `oshun-analytics:event` dispatch on activation

## E2E coverage

- [`apps/oshun/web/e2e/billing-index.spec.ts`](../../../apps/oshun/web/e2e/billing-index.spec.ts)
  — signed-in direct-route render, static directory copy, crypto/profile/AAA
  rail hrefs, billing steward `mailto:`, 44 px tap-target floor at 390 px, no
  horizontal overflow, standalone display-mode launch, real service-worker
  cached-document offline replay with no generic offline-fallback card, explicit
  absence of rail/card/steward telemetry hooks and click-dispatched analytics
  events, and anonymous redirect-before-render.

## Data & contracts

- **Reads**: none — fully static
- **Writes**: none
- **Realtime**: none
- **Caching**: static (no `dynamic`, no fetch); `/billing` is an exact shell
  navigation in `public/sw.js`, so an authenticated online navigation writes the
  document into the static SW cache and offline navigation replays the cached
  billing page rather than the generic offline fallback
- **Auth/role check**: `proxy.ts` — protected by default (not public);
  no-session → `/welcome`
- **Metadata**: `title: 'Billing'`,
  `description: 'Plan, payment method, and invoice history for your OSHUN account.'`,
  `alternates.canonical: '/billing'`

Note the description promises "Plan, payment method, and invoice history," but
the page itself renders none of those — it is a router to `/billing/crypto`. See
gaps.

## Cross-references

- Feature spec: [`V1/features.md#account`](../../../V1/features.md#account)
- Architecture:
  [`V1/ARCHITECTURE.md#account`](../../../V1/ARCHITECTURE.md#account)
- Sibling routes:
  - [`billing-crypto.md`](./billing-crypto.md) — the actual paywall
  - [`profile.md`](./profile.md) — the account hub that links here
- Journeys:
  [`../../journeys/sign-up-and-pay-crypto.md`](../../journeys/sign-up-and-pay-crypto.md)
- Upgrade flow: [`../13-system/aaa-upgrade.md`](../13-system/aaa-upgrade.md)
- Component sources: `apps/oshun/web/src/app/billing/page.tsx` (no child
  components — inline JSX with raw Lilith CSS vars)

## Open questions / known gaps

- [ ] The page metadata promises plan / payment-method / invoice-history views,
      but none exist here — it is purely a link directory. Confirm whether those
      are intended for this route or always delegated to `/billing/crypto`.
- [x] No telemetry on the rail links — current V1 behavior is explicitly no
      `billing_rail_selected`-style event or hook on the static rail directory.
- [ ] Card-rail and bank-rail surfaces are unbuilt ("follow in subsequent
      releases") — confirm they are out of V1 scope.
- [ ] Add a segment `error.tsx` for `/billing/*` if product wants a local
      unrecoverable render-crash boundary; none exists today.
