From 4a4a3428d8d3f9ea7e2227ca65fe9a3d7c0fdd94 Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 31 Jul 2026 07:06:33 -0400 Subject: [PATCH] =?UTF-8?q?spec:=20tier=20redesign=20(Option=202,=20settle?= =?UTF-8?q?d-free=20/=20live-paid)=20=E2=80=94=20design=20+=20build=20orde?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report-first. Nothing built; no tier, price, gate or Stripe object changed. REVIEW ZERO findings that shape the design: 0.2 The ladder is HALF-EXPRESSIBLE already — PRICE_MAP separates founder from standing objects, so lifetime grandfathering is native (a sub created against a founder price stays on it). BUT founder access is gated by CODE + EXPIRY (FOUNDER2026/VYNDR/BETONBLK/EARLYBIRD, expiry 2026-12-31), NOT by seat count: anyone with a code gets founder pricing at any seat number. A real Stripe-derived counter exists (/api/founders/count, live 0 of 100) but only DISPLAYS — and it is cached 300s, so it cannot enforce "slot 100 and 101 differ permanently". Making the counter the gate, transactionally and uncached at checkout-session creation, is a real build. 0.3 The paid->free flip point already exists ON THE SERVED PAYLOAD: settlement writes ledger_entries.outcome + settled_at, and /api/snapshot already merges per-grade results — live WNBA returns 25 grades, 5 carrying outcome {result:'hit', actual:1}. So the gate discriminator (outcome != null) is present on the exact object to be gated; no new pipeline needed. 0.4 THE MIGRATION IS NOT WHAT THE ORDER ASSUMES: the users table holds 3 users, all free, created Jun 12-19, and ZERO paid. There is no warm mass base — the "founder launch to existing users" is a courtesy note to 3 people, and the launch's real audience is people who have not signed up yet. DESIGN: free = full data aggregator + the COMPLETE settled record (letter, reasoning, edge, outcome — browsable and filterable), which is the proof hook. Analyst = tonight's live grades + reasoning + edge, unlimited. Desk = + alt ladder, Kelly, portfolio, engine2. Reasoning/grade/edge are ONE paid unit while live and become free together at resolution — which also converts today's unenforced board-reasoning leak into a deliberate rule. GATE: outcome == null => live => Analyst+; outcome != null => settled => free. Filter whole grades server-side (not field-strips) so a live grade cannot leak partially; never infer resolution from time or game status, only from a written outcome; fail closed to LIVE so a settle failure withholds rather than exposes; void/unrecoverable are terminal and therefore free. BUILD ORDER: (1) the settled/live gate, (2) the free settled-record surface — noted as arguably shipping WITH (1), since gating live grades without it leaves free users no graded content at all, (3) Stripe ladder + transactional counter + grandfather rule + retire the code gate, (4) the founder note to the 3, (5) pricing visuals (already designed in the package). CANNOT DETERMINE: whether the four Stripe price objects exist in the dashboard (env not readable here) — flagged as a prerequisite for build 3. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc --- specs/tier-redesign-spec.md | 158 ++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 specs/tier-redesign-spec.md diff --git a/specs/tier-redesign-spec.md b/specs/tier-redesign-spec.md new file mode 100644 index 0000000..dc36f0f --- /dev/null +++ b/specs/tier-redesign-spec.md @@ -0,0 +1,158 @@ +# SPEC — TIER REDESIGN (OPTION 2: settled-free / live-paid) +Report-first, 2026-07-31. **Nothing built, no tier/price/gate/Stripe object changed.** +Every downstream build is its own gated order. + +--- + +## REVIEW ZERO — CURRENT REALITY (VERIFIED) + +**0.1 Today's line** (confirmed in `specs/tier-structure-pull.md`): free/anon receives the +**entire uncapped live board** (`routes/snapshot.js:106-107` applies only `stripModelPrice`, no +count cap), **including full `reasoning` + kill-condition reasons** — which violates +`config/tiers.js free: { reasoning_visible: false }`, enforced on the scan path but not the board. +Edge fields (`p_win`/`ev_pct`/`model_odds`/`value`/`takeable`) **are** correctly stripped. + +**0.2 Stripe — the ladder is HALF-EXPRESSIBLE TODAY, and the gate is the wrong thing.** +`stripeService.PRICE_MAP` already separates founder from standing objects: +`STRIPE_PRICE_ANALYST` · `STRIPE_PRICE_ANALYST_FOUNDER` · `STRIPE_PRICE_DESK` · +`STRIPE_PRICE_DESK_FOUNDER` (+ `africa`). **So grandfathering is natively expressible** — a sub +created against a founder price object stays on it forever unless someone migrates it, which is +exactly the ratified "lifetime founder". + +**🔴 BUT founder access is gated by CODE + EXPIRY DATE, NOT by a seat count.** +`VALID_FOUNDER_CODES = FOUNDER2026, VYNDR, BETONBLK, EARLYBIRD`, `FOUNDER_EXPIRY = 2026-12-31`. +Anyone holding a code gets the founder price **regardless of how many seats are gone**. +A **real** counter exists but does **not** gate anything: `GET /api/founders/count` → +**live: `{available:true, claimed:0, total:100}`**, Stripe-derived from actual subscriptions on +the founder price (`routes/founders.js` — deliberately NOT a settable profile field), **cached +300s**. **A 5-minute cached read cannot enforce "slot 100 and 101 differ permanently" — that +cache alone permits 101+.** Converting the counter from *display* to *gate*, transactionally, is +a real build (Phase 3). + +**0.3 The paid→free flip point — VERIFIED, AND IT ALREADY EXISTS ON THE SERVED PAYLOAD.** +Settlement writes `ledger_entries.outcome` + `settled_at` (`ledgerService`), and the board +**already merges the per-grade result**: live `/api/snapshot/wnba` returns **25 grades, 5 of them +carrying `outcome: {result:'hit', actual:1}`**. So the discriminator the gate needs +— **`outcome != null` ⇒ settled ⇒ free; `outcome == null` ⇒ live ⇒ paid** — is present on the +exact object that must be gated. **No new pipeline is required.** + +**0.4 The existing base — THE MIGRATION IS NOT WHAT THE ORDER ASSUMES.** +`users` table: **3 users, all `free`, created 2026-06-12 → 06-19. ZERO paid.** +There is no warm mass base to migrate. The "founder launch to existing users" is a **courtesy +note to 3 people** — which removes the migration risk almost entirely and means **the launch's +real audience is people who have not signed up yet.** + +--- + +## PHASE 1 — THE TIER TABLE + +| | **FREE** | **ANALYST** ($14.99 founder → $24.99) | **DESK** ($44.99 founder → $59.99) | +|---|---|---|---| +| Data aggregator (schedule, per-book lines, player stats, streaks, hot lists, team hubs) | **✅ full, uncapped** | ✅ | ✅ | +| **Settled/resolved graded record** (letter + reasoning + edge + outcome) | **✅ COMPLETE, browsable, filterable** | ✅ | ✅ | +| Public model record / accuracy / CLV aggregate | ✅ | ✅ | ✅ | +| **Tonight's LIVE grades** (letter) | **❌** | ✅ | ✅ | +| Live **reasoning** (the grade's argument) | ❌ | ✅ | ✅ | +| Live **edge** (`p_win`, `ev_pct`, `model_odds`, `value`, `takeable`) | ❌ | ✅ | ✅ | +| Scans/day | **0 live** (settled browsing unlimited) | ∞ | ∞ | +| Alt-line ladder · Kelly · portfolio · engine2 | ❌ | ❌ | ✅ | + +**2. FREE IS A COMPLETE PRODUCT, NOT A DEMO.** The settled record is the *hook*: every graded +read the model ever made, with its **full reasoning and its edge numbers**, next to **what +actually happened**. Filterable by sport/stat/grade/date/outcome. That is a stronger proof +surface than most competitors' paid product, and it is the thing that sells the live tier. + +**3. REASONING + GRADE + EDGE ARE ONE PAID UNIT while live** — the reasoning *is* the grade's +argument; splitting them sells a claim without its defence. **All three become free together at +resolution.** This also *resolves the 0.1 contradiction*: board reasoning stops being an +unenforced leak and becomes deliberate — free after settlement, gated before it. + +--- + +## PHASE 2 — THE SETTLED/LIVE GATE (load-bearing) + +**4. The rule, in one line:** `outcome == null` ⇒ **LIVE ⇒ Analyst+**. `outcome != null` ⇒ +**SETTLED ⇒ everyone.** Resolution is written by the existing settle pass; nothing new to build +in the pipeline. + +**5. Airtightness — gate on the SERVER, by presence of a real outcome:** +- The snapshot route already resolves tier (`resolveTierFromRequest`, **fails closed to `free`**). + Extend the existing `gate()` composition: for unentitled tiers, **drop every grade whose + `outcome` is null** — a filter, not a field-strip, so a live grade cannot leak in a partial form. +- **Never infer resolution from time, game status, or `gradedAt`** — only from a written outcome. + A game can be final while the settle pass has not yet run; treating "probably over" as settled + is exactly how a live edge leaks. +- **Fail-closed both ways:** unknown/missing outcome ⇒ treated as LIVE (paid). A settle failure + therefore withholds content rather than exposing it, matching the `resolveTierFromRequest` + precedent. +- `void` / `unrecoverable` outcomes are **terminal ⇒ settled ⇒ free** (they are results, not + pending edges). + +**6. Snapshot-path change (the one structural edit):** free/anon board = **settled grades only**; +Analyst+ = full board. The free board is therefore *yesterday's proven card wall*, which is +honest and marketable. Counts must be honest: a free user sees "N settled reads" and an explicit +"tonight's N live reads are Analyst" — **never a blurred/locked row implying we hid something +they nearly had**, and never a fabricated count. + +--- + +## PHASE 3 — PRICE LADDER + LIFETIME FOUNDER + +**7. Stripe objects (4, two already env-named):** `analyst_founder $14.99` · `analyst $24.99` · +`desk_founder $44.99` · `desk $59.99`. **Grandfathering is native**: a subscription created +against a founder price stays on it forever. **The build rule: NEVER call Stripe's price-update +/ migration on a founder subscription.** Rollover changes only which object *new* checkouts use. + +**8. The counter must BECOME the gate, transactionally.** Today it is a 300s-cached display +number and the price is gated by a code — so today **anyone with a code can be seat 500 at +founder pricing.** Required change: at **checkout-session creation**, re-derive the founder count +**uncached** and pick the price object then; if the cap is reached, the session is created against +the **standing** object. Recommended hardening: a DB unique-slot claim (or a Stripe metadata +seat number) so two simultaneous checkouts cannot both take slot 100 — **the cached read must not +be in the decision path.** Retire or repurpose the code+expiry gate, otherwise the codes silently +bypass the cap. + +**9. Honest display:** `"$14.99/mo — Founder rate, X of 100 left, then $24.99."` X from the +**uncached** count. When 0 remain, the card shows the standing price with no phantom scarcity. +**Live today: 0 of 100 claimed** — so the meter is currently truthful and the ladder starts clean. + +**10.** Standing is **launch-standing, revisable UPWARD** as the settled record proves out. +Founder subs never move. Analyst at $24.99 is deliberately underpriced at scale. + +--- + +## PHASE 4 — MIGRATION (redesigned around 0.4) + +**With 3 free users and 0 paid, this is not a mass migration.** Design accordingly: +1. **Direct, personal note to the 3** — live grades are becoming an Analyst feature; here is a + founder seat (or a comped period) as the early-user thank-you. Honest, not a silent takeaway. +2. **A grace window is optional at this size** — 7 days of continued live access is a courtesy, + not a risk-management necessity. +3. **The real launch audience is prospective, not existing.** The founder offer's job is + acquisition, not retention. **Do not design an elaborate migration for 3 people** — spend the + effort on the settled-record proof surface, which is what converts a cold visitor. + +--- + +## PHASE 5 — SEQUENCED BUILD ORDER + +1. **The settled/live gate** (snapshot filter + honest free-board copy). Structural core; depends + on nothing new — the `outcome` marker is already on the payload. **Ships first: it is what + makes the product sellable.** +2. **The free settled-record surface** (browsable/filterable proof). Depends on 1's semantics. + *This is the free product; without it, gating live grades leaves free users with no graded + content at all.* Arguably ships WITH 1, not after. +3. **Stripe ladder + transactional counter + grandfather rule.** Independent of 1-2; needs the 4 + price objects created in Stripe and the code-gate retired. +4. **The founder launch / note to the 3.** Needs 1-3 live. +5. **Pricing-surface visuals** — already designed (Landing / System / Mobile pricing artboards in + `specs/design-reference/`); the visual target exists, no new design needed. + +--- + +## TAGS +VERIFIED: current gate behaviour; Stripe price-object structure + code-based founder gate + real +but cached counter (live 0/100); resolution marker present on the served payload (5/25 WNBA +grades carry `outcome`); user base = 3 free / 0 paid. CANNOT DETERMINE: whether the 4 Stripe +price objects exist in the Stripe dashboard (env values not readable from here) — **flagged as a +prerequisite for build 3**. BLOCKED: none.