No build, no migration, no Stripe object touched. Awaiting Kev on Q1-Q3. TWO EXPECTATIONS IN THE ORDER ARE WRONG: 1. G5 — users.founder_status is LIVE, not dead. Written by the webhook (stripeService.js:163), read and served by routes/stripe.js:95 as is_founder, and present in middleware/auth.js:24 PROFILE_COLUMNS so it loads on EVERY authenticated request. The guardrail says don't write it unless G5 proves it live — G5 proves it live, so A5 must NOT drop it. 2. THE TWO FOUNDER FLAGS ALREADY DISAGREE IN PROD: user_profiles.founder_pricing is true on 1 of 3 profiles while users.founder_status is true on 0 of 3. The webhook writes both from the same isFounder, so this is a dual-write that has already drifted. The build must pick one canonical flag and derive or retire the other; two independently-writable founder flags is how a founder loses their rate on one code path. GREPS: G1 founder_pricing has exactly one writer (the webhook mirror) and four readers (partners MRR attribution, the profile API, the profile badge). G2 the promo-code bypass is the ONLY founder gate today — getPriceId(tier, founderCode) against VALID_FOUNDER_CODES, stamped into metadata.is_founder, which the webhook then trusts, so a code alone mints a founder at any seat number. G3 the webhook DOES set tier + subscription_status=active + founder_pricing (closing an earlier CANNOT DETERMINE: a paid sub does flip the Build-1 gate) but stores NO stripe_subscription_id, confirming A1. G4 nexapay has ZERO code references and the column is empty, so A5's drop is evidence-supported as its own migration. G6 price selection is getPriceId -> line_items. DB VERIFIED: user_profiles has nexapay_customer_id and NO stripe_customer_id / stripe_subscription_id (A1 needed); users already carries stripe_customer_id; founder_pricing_seats is a VIEW; 3 profiles, 1 flagged founder. CANNOT DETERMINE: the four Stripe price IDs — no STRIPE_SECRET_KEY or STRIPE_PRICE_* in this environment, so I could not independently re-verify that the IDs in the order are what prod will charge. Since A3 would hardcode them, a typo becomes a permanent mis-charge; recommend reading them from env (already the pattern) with a boot assertion that all four resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
5.7 KiB
BUILD 2 — REVIEW ZERO REPORT (no build; awaiting Kev on Q1-Q3)
2026-07-31. Nothing built, no migration applied, no Stripe object touched.
🔴 TWO ORDER EXPECTATIONS ARE WRONG — read before deciding Q1-Q3
G5 — users.founder_status is LIVE, NOT DEAD. Do not drop it.
| where | what |
|---|---|
src/services/stripeService.js:163 |
WRITTEN by the webhook: founder_status: isFounder |
src/routes/stripe.js:95 |
READ and served: is_founder: req.user.founder_status |
src/middleware/auth.js:24 |
in PROFILE_COLUMNS — loaded on every authenticated request |
src/middleware/auth.js:72 |
defaulted to false on the fallback profile |
The guardrail says "Don't write users.founder_status unless G5 proves it live." G5 proves it
live. A5's "drop on evidence" must NOT run for this column.
🔴 AND THE TWO FOUNDER FLAGS ALREADY DISAGREE IN PROD
user_profiles.founder_pricing = trueon 1 of 3 profilesusers.founder_status = trueon 0 of 3
The webhook writes BOTH from the same isFounder — so they are a dual-write that has already
drifted. Whatever is decided on Q3, the build must pick ONE canonical flag (the order says
user_profiles.founder_pricing) and make the other a derived read or explicitly retire it. Leaving
two independently-writable founder flags is how a founder loses their rate on one code path.
THE GREPS
G1 — user_profiles.founder_pricing
- WRITE:
stripeService.js:173(webhook mirror) — the only writer. - READ:
routes/partners.js:68,92(MRR attribution picks founder vs standard price);web/src/app/api/user/profile/route.ts:16;web/src/app/profile/page.tsx:17,121(the badge). routes/founders.js:6,9documents that the counter deliberately does not trust this flag (it counts Stripe subs instead) — "a tier/founder_pricing field a profile can set".
G2 — THE PROMO-CODE BYPASS IS THE ONLY FOUNDER GATE TODAY.
createCheckoutSession(userId, email, tier, founderCode) (stripeService.js:68) →
getPriceId(tier, founderCode) → isFounderCodeValid() against VALID_FOUNDER_CODES
(FOUNDER2026, VYNDR, BETONBLK, EARLYBIRD) + FOUNDER_EXPIRY 2026-12-31. The result is stamped
into metadata: { user_id, tier, is_founder: String(isFounder) } (:119), and the webhook
trusts that metadata (:155). So today a code alone mints a founder at any seat number, and
carries itself into the DB flag. This is the bypass B1 must delete.
G3 — the webhook DOES set the entitlement (stripeService.js:151-176). It writes users
(tier, stripe_customer_id, founder_status) and mirrors to user_profiles
(tier, subscription_status: 'active', founder_pricing).
This closes an earlier CANNOT DETERMINE: a paid sub DOES flip the Build-1 gate.
But it stores NO stripe_subscription_id anywhere — confirming A1 is required, since
finalize_founder_slot and grandfather reconciliation both key off it.
G4 — nexapay_customer_id: ZERO code references in src/ or web/src/. The column exists on
user_profiles and is empty (0/3). A5's drop is evidence-supported — as its own migration.
G6 — price selection: getPriceId returns the founder PRICE_MAP entry when the code is valid,
else standing (falling back to the PRICE_UNCONFIGURED sentinel if unset); used at
line_items: [{ price: priceId, quantity: 1 }] (:115).
DB FACTS (VERIFIED against zmdnczhtdxcddsxzttub)
user_profilescolumns:id, email, tier, scan_count, scan_reset_date, subscription_start, subscription_end, subscription_status, cancel_at_period_end, founder_pricing, age_verified, **nexapay_customer_id**, created_at, updated_at, mfa_setup_prompted, grace_period_until, partner_ref. Confirmed: NOstripe_customer_id, NOstripe_subscription_id→ A1 needed. (NoteusersDOES already carrystripe_customer_id—middleware/auth.js:24.)founder_pricing_seats= VIEW ✓ decorative, as the order states.- 3 profiles; 1 with
founder_pricing=true.
CANNOT DETERMINE
The four Stripe price IDs. PRICE_MAP is env-driven and there is no STRIPE_SECRET_KEY and
no STRIPE_PRICE_* in this environment, so I cannot confirm the four IDs in the order are the
ones prod will actually charge. The order says they were verified live this session — I am flagging
that I could not independently re-verify them, and A3 hardcodes them, so a typo becomes a
permanent mis-charge. Recommend the build read them from env (already the pattern) and assert at
boot that all four resolve, rather than hardcoding in SQL.
THE THREE DECISIONS — WAITING ON KEV (no silent defaults)
Q1 POOL — global 100 vs 100-per-tier. Bearing on the build: it changes the UNIQUE index
(UNIQUE(slot_number) vs UNIQUE(tier, slot_number)) and whether founder-follows-upgrade needs a
second claim at all (global = the slot travels with the user; per-tier = a Desk slot must be claimed
separately and may be full).
Q2 REOPEN — reopen on cancel vs 100 lifetime seats. Bearing: decides whether
release_expired_slots also handles cancellation, and whether the public counter can ever go down.
Q3 TEST RECORD — the 1 founder_pricing=true desk profile with 0 Stripe subs. Note it also
has users.founder_status = false, i.e. it is already inconsistent. My read: it is a test
artifact, not a subscriber. Bearing: seed slot #1 vs start clean and clear the flag.
TAGS
VERIFIED: G1-G6 with file:line; user_profiles/users schema; the view; the flag divergence
(1 vs 0); the webhook does set the entitlement but stores no subscription id.
CANNOT DETERMINE: the four Stripe price IDs (no key/env here).
BLOCKED: all of Phase A/B — awaiting Q1, Q2, Q3.