Files
vyndr/specs/tier-redesign-spec.md
builtbykev 4a4a3428d8 spec: tier redesign (Option 2, settled-free / live-paid) — design + build order
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-07-31 07:06:33 -04:00

9.5 KiB

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/countlive: {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 == nullLIVE ⇒ Analyst+. outcome != nullSETTLED ⇒ 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.