# BUILD 2 — REVIEW ZERO (report; mechanism NOT built) 2026-07-31. Nothing built, no Stripe object created or changed, no price logic touched. ## WHY THIS STOPPED **I have no Stripe credentials in this environment** (`.env` holds ODDS/SUPABASE/INTERNAL keys — **no `STRIPE_SECRET_KEY`**). The order's standing floor requires *"founder/standing/grandfather/race all verified server-side"*. **I cannot verify any of them**, cannot create the standing price objects the rollover needs, and cannot run the concurrent-checkout race test. This is a payment path where the failure modes are **permanent and customer-facing**: a race bug mis-prices a subscriber *forever* (slot 101 on a founder rate, or slot 99 on standing), and a grandfather bug overcharges a founder *every month*. Shipping that unverified is the one place "probably right" is not good enough. **So: findings, then stop.** ## WHAT I DID ESTABLISH (VERIFIED) **0.1 — Stripe IS live and FOUNDER price objects DO exist.** Proven indirectly but soundly: `GET /api/founders/count` returns **`{available:true, claimed:0, total:100}`**, and `routes/founders.js` returns **`{available:false}`** whenever `countFounderSeats()` is null — which it is when `!STRIPE_SECRET_KEY || founderPrices.length === 0`. **`available:true` therefore proves both the secret key and at least one founder price ID are configured in prod**, and that `claimed:0` is a REAL count, not a fallback. (That route is honest by construction — *"any failure → hide, never fabricate"* — which is why it can be used as a probe at all.) **STANDING objects: CANNOT DETERMINE.** Env is not readable from here, and `getPriceId` **falls back silently**: a missing standing price yields the `PRICE_UNCONFIGURED` sentinel, which does not fail until Stripe rejects the session. **So a missing standing object would not surface until the first post-cap checkout 400s in front of a paying customer.** Verifying this is a 30-second check in the Stripe dashboard and is **prerequisite #1**. **0.3 — THE COUNTER TODAY IS NOT A GATE, AND CANNOT BECOME ONE WITHOUT NEW WORK.** - It is a **read**, not a claim: `countFounderSeats` lists Stripe subscriptions and returns a number. - It is **cached 300s** (`CACHE_KEY 'founders:count'`), so it is stale by construction. - **Founder pricing is gated by a CODE + EXPIRY, not by the count** (`VALID_FOUNDER_CODES`, `FOUNDER_EXPIRY 2026-12-31`) — so today **anyone holding `FOUNDER2026` gets the founder rate at any seat number**, and the cap is decorative. - **Two simultaneous checkouts at slot 99 would both read 99 and both get founder.** There is no claim, no lock, no unique constraint anywhere in the path. **0.2 — the entitlement the gate reads** is `config/tiers.js` capability `reasoning_visible` (free false / analyst+ true), consumed by `snapshotGating.entitledToItemizedGrades` via `resolveTierFromRequest`, which reads `users.tier`. **So a successful subscription must set `users.tier`** for Build-1's gate to open. Whether the Stripe webhook currently writes that on `checkout.session.completed` is **CANNOT DETERMINE without the webhook secret** — and it is **prerequisite #2**, because a paid sub that does not flip `users.tier` sells access that never opens. ## WHAT I WOULD BUILD, ONCE UNBLOCKED (design is settled, so this is fast) 1. **An atomic slot claim, DB-backed** — a `founder_slots` table with a **unique constraint on `(tier, slot_number)`**, claimed inside the checkout-session request *before* the Stripe call. The unique index — not a count read — is what makes the race impossible: two concurrent claims for slot 100 mean one INSERT wins and the other is rejected to standing. **The cached count must be removed from the decision path entirely** and kept only for display. 2. **Price selection from the claim**, not from a code: claim succeeded → founder object; claim rejected/cap full → standing object. **Retire the code+expiry bypass**, or it silently defeats the cap. 3. **Grandfathering** is already native (a sub created against a founder price stays on it) — the build rule is simply: **never call Stripe's price-update/migration on a founder subscription.** 4. **Founder-follows-upgrade**: on tier change, attempt an atomic claim on the *target* tier's founder slots; success → founder object, failure → standing. Tie to continuous subscription by **releasing the slot on cancellation** (that is what makes "break it → standing" true rather than aspirational). 5. **Honest display**: real uncached count at render; **if the count cannot be served, show the offer with no number** — the route already does exactly this, so follow its precedent. 6. **Beta framing** with no proven-edge claim, pointing at `/record` (shipped) as the honest building record. ## PREREQUISITES (all need Kev — none are code) 1. **Confirm/create the two STANDING price objects** in Stripe ($24.99 analyst, $59.99 desk) and set `STRIPE_PRICE_ANALYST` / `STRIPE_PRICE_DESK`. 2. **Confirm the webhook sets `users.tier`** on `checkout.session.completed` (else the gate never opens). 3. **A Stripe test-mode key** available to the build/verification environment, so the race, grandfather and end-to-end unlock can actually be exercised rather than asserted. ## TAGS VERIFIED: Stripe live + founder price objects exist (via the honest counter probe); the counter is a cached read with no claim; founder pricing is code-gated not count-gated; the gate reads `users.tier` via `reasoning_visible`. **CANNOT DETERMINE: whether standing price objects exist; whether the webhook writes `users.tier`.** **BLOCKED: the entire mechanism — no Stripe credentials, so nothing on the payment path can be verified, and it must not ship unverified.**