Files
vyndr/specs/build2-review-zero.md
builtbykev 14f3ce95b9 report: Build 2 Review Zero — payment mechanism BLOCKED, no Stripe credentials
Nothing built. No Stripe object created or changed, no price logic touched.

STOPPED because there is no STRIPE_SECRET_KEY in this environment (.env holds
only ODDS/SUPABASE/INTERNAL keys). The order's standing floor requires
founder/standing/grandfather/race all verified server-side; none of that is
verifiable here, the standing price objects cannot be created, and the
concurrent-checkout race cannot be exercised. On a payment path the failure modes
are permanent and customer-facing — a race bug mis-prices a subscriber forever,
a grandfather bug overcharges one every month — so it must not ship unverified.

VERIFIED ANYWAY:
- Stripe IS live and FOUNDER price objects DO exist. /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. So available:true proves the
  secret key and at least one founder price ID are configured in prod, and
  claimed:0 is a real count rather than a fallback.
- THE COUNTER IS NOT A GATE. It is a cached (300s) READ, not a claim; founder
  pricing is gated by CODE + EXPIRY, not by the count, so 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 lock or unique constraint anywhere in the path.
- The gate reads users.tier via config/tiers.js reasoning_visible, so a
  successful subscription must set users.tier for Build 1's gate to open.

CANNOT DETERMINE: whether the STANDING price objects exist (env unreadable, and
getPriceId falls back SILENTLY to a PRICE_UNCONFIGURED sentinel, so a missing
standing object would not surface until the first post-cap checkout 400s in front
of a paying customer); whether the webhook writes users.tier on
checkout.session.completed.

DESIGN IS SETTLED for when it unblocks: a founder_slots table with a unique
constraint on (tier, slot_number) claimed before the Stripe call — the unique
index, not a count read, is what makes the race impossible; price selection from
the claim rather than a code, with the code+expiry bypass retired; grandfathering
by simply never calling Stripe price-migration on a founder sub;
founder-follows-upgrade by claiming on the target tier and releasing the slot on
cancellation; honest display that shows no number when the count is unavailable
(the existing route already sets that precedent).

PREREQUISITES, all needing Kev and none of them code: confirm/create the two
standing price objects and set STRIPE_PRICE_ANALYST / STRIPE_PRICE_DESK; confirm
the webhook sets users.tier; provide a Stripe test-mode key so the race,
grandfather and end-to-end unlock can be exercised rather than asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-07-31 08:30:32 -04:00

5.7 KiB

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.