🦄 Unicorn.Land ▸ docs/superpowers/specs/2026-07-05-oannes-codex-paywall-design.md
updated 2026-07-05

Oannes Codex Paywall — Design

Date: 2026-07-05 Status: Approved (brainstorming → spec) Project: Oannes Content Engine — the Codex (oannescode-site/codex/)

Goal

Turn the Codex (oannescode.com/codex/, shipped 2026-07-05) from a free searchable archive into the first real paid product from the claim graph — the top near-term revenue lever identified in the 2026-07-04 money-model research. Plain search alone isn’t worth paying for; the paid tier needs genuinely new value, not just an unlocked toggle.

Non-goals / explicitly deferred

The core constraint

The existing Codex ships one public claims.json with everything in it, including corroboration links. A paywall can’t be a UI toggle on top of that — anyone can read the full file from dev tools regardless of what the UI shows. Free and paid must be genuinely different payloads: the free static file is deliberately smaller, and the paid payload is served only after a server-side subscription check. There is no client-side enforcement step in this design; every gated capability is a server call.

Architecture

Data flow

  1. Visitor hits /codex/, free claims.json loads exactly as today (full text/claimant/ topics/date for all 4,121 claims, corroboration counts only — see Feature Matrix).
  2. Visitor clicks “Unlock the Codex” → POST /api/checkout creates a Stripe Checkout Session (mode=subscription, price=STRIPE_PRICE_ID) and the client redirects to the returned Stripe URL.
  3. Stripe redirects back to /codex/?session_id=... on success. Client calls GET /api/checkout/verify?session_id=..., which retrieves the session from Stripe, confirms payment_status=paid, upserts the KV subscriber record, sets the signed cookie, and the client reloads the premium view.
  4. Independently, POST /api/stripe-webhook (signature-verified) keeps the KV record in sync on customer.subscription.updated / .deleted / invoice.payment_failed — this is the source of truth for access, not just the one-time verify call, so a cancelled subscription actually loses access at the next check.
  5. On every page load, if the signed cookie is present and unexpired, the client calls GET /api/premium. The Function re-validates the cookie signature, looks up the email in KV, and only returns the full payload if status === "active". Any failure → 401 → client silently falls back to the free-only UI (no error state shown to a lapsed subscriber, just fewer unlocked features).
  6. GET /api/dossiers/:topic and GET /api/export follow the identical gate-check pattern.
  7. POST /api/billing-portal creates a Stripe Billing Portal session for a valid cookie holder and returns the redirect URL.

Feature matrix

Free (static claims.json) Paid (/api/premium, /api/dossiers, /api/export)
Claim text, claimant, topics, date All 4,121 All 4,121
Search Capped at top ~15 results/query, “Subscribe to see all N” prompt beyond that Unlimited
Advanced filters (date range, sort-by-corroboration-count, claimant-only)
Corroboration/contradiction links Count only (“🔗 3 related claims”) Full linked claim text
Topic Dossiers (AI-synthesized cross-claim narrative per topic)
CSV/JSON export
New-claim timing Weekly, on the existing static regeneration cadence As soon as ingested (see below)
Cancel/manage n/a Stripe Billing Portal (self-serve)

New pipeline pieces (vault _scripts/)

Error handling

Testing

Rollout

  1. Joshua: create the Stripe Product + recurring Price, provide Price ID + Secret Key + Publishable Key + (after webhook endpoint exists) Webhook Signing Secret.
  2. Claude: build the Pages Functions, KV namespace, cookie signing, and wire the Codex UI’s paywall CTAs — in Stripe test mode first.
  3. End-to-end test-mode verification (checkout → webhook → premium unlock → cancel → access revoked) before flipping to live keys.
  4. generate_dossiers.py first run (likely the most expensive single step — ~362 topics × Claude call).
  5. Switch to live Stripe keys, ship.