# VYNDR — CANONICAL STATE FILE
Read-only ground-truth audit. Written 2026-07-26 by Claude Code (repo + deployed DB access).
Every item tagged **VERIFIED** (file/line or measured number), **CANNOT DETERMINE** (with reason),
or **BLOCKED** (with what unblocks it). Where a prior claim conflicts with code/data, the code/data wins.
Nothing was built, changed, deployed, or migrated by this audit.

---

## REVIEW ZERO — PREMISE

- **0.1 What I can read** — VERIFIED. The **repo** (full source), the **deployed Supabase DB**
  (read-only via MCP, project `zmdnczhtdxcddsxzttub`), and the **deployed API** (`api.vyndr.app`).
- **0.2 Key reachability** — VERIFIED. `PROPLINE_API_KEY_*` are **NOT on the box** (`.env` absent);
  they were pasted in-session earlier this conversation and are usable for probes. `ODDS_API_KEY`
  is on the box but **exhausted (0/500)**. `ODDSPAPI_KEY` not on box. DB-dependent items were run
  against prod directly, so nothing here is BLOCKED on PropLine keys.

---

## PHASE 0 — THE ROI RECONCILIATION (headline)

- **0.1 ROI computed in code?** — VERIFIED. **Not for the model ledger.** ROI exists only for
  **user bet-tracking**: `performanceService.js:42` (`roi: stats.roi`) and `betService.js:172-174`
  (`profit = payout - amount`). There is **no ROI computation over `ledger_entries`** (the model's
  public record). That is why the model's ROI has been invisible.

- **0.2 Price-at-grade persisted?** — VERIFIED. **YES** — `ledger_entries.locked_odds` (text,
  American), written by `ledgerService.recordPipelineGrades`. Populated on essentially all settled
  rows (5 nulls on B). **ROI IS computable for accrued history.**

- **0.3 11-point index persisted?** — VERIFIED (nuanced). **NOT in `ledger_entries`** — only the
  collapsed letter (`grade`); `_grade_11` is explicitly `delete`d at `gradeSlateService.js:97`
  before the ledger write. **BUT it IS retained in `model_snapshots.grade_11`** (migration
  `025:51`, `retentionService.js:117`) — 2,888 of 4,600 snapshot rows carry it. So sub-tier is
  **lost on the settled-outcome table but recoverable** by joining `model_snapshots` to
  `ledger_entries` on (player, stat, line, date). STATE.md's "grade_11 is stored" and this audit's
  "deleted before ledger write" are BOTH correct — different tables.

- **0.4 Price distribution by grade (American odds, settled)** — VERIFIED:
  | sport | grade | n(decided) | median | q1 | q3 |
  |---|---|---|---|---|---|
  | MLB | B | 285 | −270 | −650 | −155 |
  | MLB | C | 176 | −169 | −500 | +109 |
  | WNBA | B | 208 | −124 | −145 | −110 |
  | WNBA | C | 160 | −120 | −134 | −108 |
  Blended B median −160, C median −130. Overall range −10000 … +600. MLB grades skew to **deep
  favorites**; WNBA grades cluster tight around −120.

- **0.5 Flat-stake unit ROI by grade (1u/row at `locked_odds`, decided rows hit/miss, voids
  excluded)** — VERIFIED:
  | sport | grade | n | hit% | **ROI%** |
  |---|---|---|---|---|
  | MLB | B | 285 | 70.2 | **−1.02** |
  | MLB | C | 176 | 60.8 | **+4.57** |
  | WNBA | B | 208 | 52.4 | **−4.79** |
  | WNBA | C | 160 | 51.9 | **−5.26** |
  | A / D / F | (all) | 1 / 2 / 4 | 0 / 0 / 0 | −100 each (negligible n) |
  Including voids as net-0 (my first pass) gives blended B −2.31%, C −0.10%.
  **CONCLUSION — the premise's binary is a false dichotomy; the truth decomposes:**
  - MLB **B** = high-hit (70%) **break-even favorites** — the premise's "favorites at fair prices,
    zero edge" hypothesis is CONFIRMED here.
  - WNBA **B/C** = **losing** (~52% hit at ~−120 where break-even is ~54.5%).
  - MLB **C** = **genuinely +EV (+4.57% on 176 decided)** — a real edge the blended "no ROI" masked.
  So: ROI is not uniformly zero-edge. It is **not computed in code**, and when computed here it shows
  **one profitable segment (MLB-C) hidden under WNBA losses and break-even MLB-B**.

- **0.6 64/56 population** — VERIFIED. The displayed `/api/ledger/accuracy` figures are
  `hits/(hits+misses)`, **excluding voids** (88 void rows, 8.8%) and unsettled (67). Distinct
  `outcome` values are **{hit, miss, void, null}** — **no `push`** (pushes structurally impossible:
  half-number lines). The ROI table above uses the SAME decided (hit/miss) population, so hit-rate
  and ROI are comparable. Blended B hit is 63.1% on `hits/(hit+miss)` but 55.9% on all-settled
  (the 64 void B rows are the gap).

- **0.7 Hit rate in UI/marketing?** — VERIFIED. Displayed on **≥10 surfaces**: `app/page.tsx`
  (landing), `GradeCard`, `TopSignals`, `vyndr/TierRecord`, `vyndr/ModelRecord`, `vyndr/AccuracyBadge`,
  `game/[id]/page`, `u/[handle]/portrait`, `ledger/page`, `scan/page`. **ROI / edge is displayed
  NOWHERE.** Honesty gap: users see "63% / 70% HIT" with no indication MLB-B is −1% and WNBA is −5%.

- **0.8 CLV computed / closing persisted?** — VERIFIED (with a broken-ness caveat). `closing_odds`
  on **962/996** rows, `clv` on **842/996** — so CLV IS computed and closing IS persisted. BUT
  `closingCapture.js:7-8` documents it as **effectively broken**: `closing_line == locked_line on
  92% of rows` (only ~56 rows show real movement), because `captureClosing` overwrites the field
  with the current feed on every snapshot and most props leave the feed near their lock. CLV exists
  but is **largely degenerate (≈0)**.

---

## PHASE 1 — THE GRADED LINE

- **1.9 Selector location** — VERIFIED. Two stages: (a) `gradeSlateService.dedupeProps`
  (`gradeSlateService.js:34`, called `:144`); (b) the `snapshotService` dedup at
  `snapshotService.js:425-434`.
- **1.10 Reads over which set?** — VERIFIED. **One provider at a time** — PropLine-normalized rows
  (primary) via `oddsService` → `recordDownstream` → `gradeAndCacheSlate`. Falls back to
  odds-api / oddspapi only if PropLine fails (`oddsService` fallback chain). Not all providers merged.
- **1.11 dedupeProps exists?** — VERIFIED. **YES, it EXISTS** (`gradeSlateService.js:34`). Logic:
  Set on key `` `${player}::${stat_type}::${line}` `` — keeps the **FIRST** row per (player, stat,
  line), discards later rows at the same (player, stat, line) (i.e. other books at the same line),
  caps at `limit`. Written **Session 32** (`f0c8b4f`, "Grades pipeline + NFL/NHL wiring"). This
  settles the asserted/un-asserted question: **it is real, not imagined.**
- **1.12 snapshotService 411-434** — VERIFIED. Confirmed: dedup by `` `${nameKey}|${stat_type}` ``,
  keeps the row with the **highest `confidence`**. `confidence` derives from the **grade letter**
  (band-midpoint from `grade_thresholds.json`, `confidence_basis:'grade_band'`) — so "highest
  confidence" == "highest grade letter". It carries no information beyond the letter.
- **1.13 consensus vs first-book** — VERIFIED. **NO consensus rule exists in code.** The graded line
  is chosen by **first-book-at-each-line (dedupeProps) then highest-grade-across-lines
  (snapshotService)**. The "book-agnostic consensus rule" description is a prior order's *proposal*,
  never built. The "first book" description is CLOSER to correct. **Code wins: no consensus.**
- **1.14 5 books at 3 lines → which line?** — VERIFIED (walked). `normalizeProps` emits one row per
  book → `dedupeProps` keeps the first book at each of the 3 distinct lines (3 survivors) →
  `gradeAndCacheSlate` grades both sides of each → `snapshotService` keeps the **highest-grade** of
  the 3. **The graded line = whichever of the 3 lines grades highest** (a best-grade-for-us
  selection, active now that the feed is 27% MLB / 82% WNBA multi-book).
- **1.15 Consumers assuming one book/prop** — VERIFIED (partial list): the grade path
  (`dedupeProps` collapses book multiplicity), `analyzeViaEngine1` (`book_odds`/`fair_prob` use the
  single surviving row's odds), the snapshot GameCard overlay, `detectBestBook` (no-ops <2 books).
- **1.16 Test pinning graded line vs book-set change?** — VERIFIED: **NONE.** `dedupeProps` and
  `snapshotService` have unit tests for dedup mechanics, but **no test pins graded-line stability
  against a change in the book set** (a multi-book different-line scenario).
- **1.17 Ledger flag for pre/post book-set change?** — VERIFIED. **No dedicated flag.** `model_version`
  stamps the model era and challenger-version columns exist, but **nothing distinguishes grades by
  book-set basis.** A silent book-set change would not be visible on the ledger.

---

## PHASE 2 — THE FEED

- **2.18 Books-per-prop TODAY (2026-07-26, live `/api/odds`)** — VERIFIED, and it **CORRECTS the
  prior "137/140 single-book, DK129/FD14" measurement — the feed has broadened:**
  - **MLB**: n=322 → `{1 book: 234 (73%), 2: 51, 3: 26, 4: 10, 5: 1}`. Books: **draftkings 273,
    betmgm 105, betrivers 49, pinnacle 30, fanduel 2.** So 73% single-book (mostly DK), 27%
    multi-book, **5 books now present** (not DK-only).
  - **WNBA**: n=163 → `{1 book: 29 (18%), 2 books: 134 (82%)}`. Books: **fanduel 149, draftkings 148.**
    **WNBA is graded off a genuine 2-book feed (DK + FD)** — better multi-book coverage than MLB.
- **2.19 ALLOWED_BOOKS** — VERIFIED. `oddsNormalizer.js:9` = 11 books (draftkings, fanduel, betmgm,
  caesars, fanatics, bet365, hardrockbet, pointsbet, betrivers, pinnacle, thescore), applied at
  `:110`/`:198`/`:235`. **Drops nothing today** (every book in the live feed is on the list).
- **2.20 PropLine request** — VERIFIED. `proplineAdapter.js:128` `buildUrl = ${BASE}/${sportKey}/odds`;
  `:151-152` params = `{ apiKey, markets }`. **No `regions`/`bookmakers` param is sent.** The feed
  broadening (2.18) happens on PropLine's DEFAULT response, not a param we added.
- **2.21 PropLine docs / multi-book param** — CANNOT DETERMINE (not re-tested this order). Prior
  research (WebSearch): PropLine advertises "13 books + 5 exchanges; every payload includes a
  bookmakers array" and is The-Odds-API-compatible (which uses `regions`/`bookmakers`). Whether a
  param unlocks the full set on our tier is **unconfirmed** — would need a keyed test with the param.

---

## PHASE 3 — THE CHALLENGER LEDGER

- **3.22 Row counts** — VERIFIED:
  | challenger | total | settled | first row | note |
  |---|---|---|---|---|
  | arch-v1 | 164 | 128 | 2026-07-21 | 94 with non-zero delta |
  | contact-v1 | 122 | 86 | 2026-07-23 | 107 non-null `p_win_contact` (15 abstained) |
  | proj-v1 + proj-v1.1 | 76 + 46 = 122 | 86 | 2026-07-23 | 119 with `proj_point` |
  **All MLB** (challengers are statcast-gated → MLB only; the 407 WNBA rows carry none). Public
  ledger total = **996 rows** (929 settled, 67 unsettled). **The earlier "ZERO settled p_win /
  measurement not begun" is now STALE — 86-128 settled per challenger.**
- **3.23 Population rate** — VERIFIED. 872 rows in the last 14 days across **12 active days** (2 days
  had no rows). Challengers populate a fraction of rows (arch 164/996) — the rest are pre-deploy or
  WNBA.
- **3.24 Idempotent-lock lag** — VERIFIED, **still structural.** The ledger upsert is
  `ignoreDuplicates:true`, so a challenger's fields land ONLY on rows first written AFTER that
  challenger's code deployed (arch 07-21, contact/proj 07-23); re-running a snapshot never
  backfills challenger fields onto an already-locked row. Any challenger added later inherits the
  same gap. **Settlement itself is healthy** (`stale_unsettled = 0`).
- **3.25 Silent-failure / abstain path** — VERIFIED: **none producing fake accrual.** contact-v1's
  nulls are honest abstentions (thin/absent Statcast); proj-v1 projects 119/122; arch-v1's
  non-moved rows are byte-identical-to-champion (no distinctive axis). No caught-throw-writes-null
  path masquerading as accrual.
- **3.26 Void/DNP rate** — VERIFIED. **88 void (8.8%)** + 67 unsettled of 996. Matches the ~9% premise.
- **3.27 model_snapshots / archetype / opp_rank** — VERIFIED (partial). `model_snapshots` is LIVE:
  **4,600 rows, latest 2026-07-26 22:01**, `grade_11` on 2,888. Archetype + `opp_rank_stat` were
  verified live per-sport (MLB + WNBA) in prior orders; **CANNOT re-confirm per-sport freshness here
  without additional queries** (not run to keep this pass bounded).

---

## PHASE 4 — WHAT IS ACTUALLY WIRED

- **4.28 SportsGameOdds wired?** — VERIFIED. **NOTHING.** No SGO reference in `src/`, `web/src/`,
  or `.env`. Not wired, configured, committed, or deployed. (The audit that qualified it as a source
  was report-only.)
- **4.29 Design surfaces live vs designed** — VERIFIED:
  - **S2 (book comparison / crown / disagreement):** `BookComparison.tsx` EXISTS in
    `web/src/components/` but is **NOT imported/routed anywhere (dead component)**. `BookChip` +
    `BookWordmark` exist and are used. `MovementStrip`, `CrownBadge` → **NOT FOUND** (never built).
    (Corrects an earlier order that said "BookComparison doesn't exist" — it exists, just unrouted.)
  - **THE WIRE:** = the **daily newsletter/content format** (see 4.30). Newsletter engine built
    (`newsletterService`); send is unscheduled (internal endpoint only).
  - **S3 article media:** **NOT FOUND** (no article-media generator; `mediaEngine` only has wire/
    share text templates).
  - **S-2 Offseason hub / SeasonBoard:** **NOT FOUND** (never built).
  - **System / Intelligence:** design files present in `specs/design-reference/`; live components partial.
- **4.30 What is THE WIRE?** — VERIFIED. It is **VYNDR's daily newsletter / content voice**, not a UI
  surface: `newsletterService.js:219-220` (`THE WIRE — {date}`) + `mediaEngine.js:6,121`
  (`MORNING WIRE`, `SIGNAL` deterministic templates). It's the editorial format for the daily report.
- **4.31 detectBestBook + LineSparkline** — VERIFIED. `slateAdapter.detectBestBook` returns a book
  only when ≥2 books post the SAME line at differing prices (no-ops on 1 book, never marks a lone
  price "best"). `StatStrip.LineSparkline` (`StatStrip.tsx:130`) renders only at **≥3 history
  points**, returns `null` below. Both degrade honest-absent on single-book/shallow data.
- **4.32 Line history / closing overwrite** — VERIFIED. `history` (`{t,line}`, line-deduped, cap 24)
  is shallow — most props sit at 1-2 flat points (line moves are rare; books move odds, not the
  half-point line). Two distinct closing mechanisms: the **`closing_captures` TABLE is INSERTED /
  appended** (`closingCapture.js:234`), but the **`ledger_entries.closing_line/closing_odds` is
  OVERWRITTEN every snapshot** (`ledgerService.js:18-21` doc; overwrite is why CLV is degenerate — 0.8).
- **4.33 Migration drift** — VERIFIED, **still present.** Repo `supabase/migrations/` has
  **001-022, 025, 030, 031, 032**. **MISSING: 023, 024, 026, 027, 028, 029** — applied to prod but
  untracked (they carry the challenger columns `p_win_challenger`/`challenger_*`, `model_version`,
  `env_*`, `archetype_vector` — confirmed live in prod). The repo does NOT reflect prod schema; check
  `information_schema.columns` via MCP, not the repo, before schema work.
- **4.34 WNBA grading live?** — VERIFIED. **YES.** 407 WNBA ledger rows, **376 settled**, graded off
  a 2-book (DK+FD) feed. WNBA carries no challenger rows (statcast-gated MLB-only).

---

## PHASE 5 — DOCTRINE AND THE BOARD

- **5.35 CLAUDE.md (read in full)** — VERIFIED. Standing rules & findings:
  - **NO CODE WITHOUT A SPEC**; 5 quality gates; WSL2 heredoc rule (python3 for >10-line files);
    update BUILD-STATE.md / BLOCKERS.md.
  - **Data Semantics Rule:** VYNDR never generates lines/odds — market values are REAL captured book
    numbers; only model_value/grade/edge are model output. `Number(null)===0` is the recurring
    fabrication bug; use strict null guards.
  - **Grade internals:** grade is an additive integer index (`engine1`, NEUTRAL_INDEX 3), moved by
    flat ±1.0/±0.5 deltas; A needs sum ≥+4.5, D ≤−1.51. `confidence` is NOT a probability (grade-band
    midpoint). `p_win` is the real signal. **NEVER rescale thresholds to mint A's (permanent founder
    ruling).** **A-RATED marketing on hold** until a prod fingerprint shows real A grades.
  - **Three stat_type whitelists must stay in sync** (analyze.js, scan.js, validation.py).
  - **Snapshot pipeline** is the product model (scheduled grade → lock to line → read from cache);
    on-demand "Read" retired. SNAP_TTL 24h.
  - **DO-NOT-WIRE / DO-NOT-TOUCH:** Tank01 player props = empty (do not wire); ParlayAPI host dead;
    `gameLogService.getGameLogs` returns null for MLB (a trap — use `featureCache.getStatRows`);
    `mlbGrader.js` is dead code; the legacy `--grade-a` token alias block kept until consumers migrate.
  - **Three separate MLB stat maps on purpose** (featureCache / outcomeService / liveTrackingService) —
    do not merge. Settlement is MLB-only (WNBA/NBA/soccer never settle via that path — but WNBA IS
    grading + settling per 4.34, so confirm the settle path).
- **5.36 Running task list / open items** — VERIFIED. Primary: **`specs/STATE.md`** (1,886 lines,
  "STATE OF THE WORLD," CURRENT STATUS + OPEN ITEMS block, last dated 2026-07-22). Also
  `BUILD-STATE.md`, `BLOCKERS.md`, `DECISIONS.md`, `AUTONOMY.md`, `PROMISE-AUDIT.md`, `ROADMAP.md`.
  **Open threads I have been tracking across recent orders that a strategist chat may not have:**
  (a) challenger measurement now HAS settled rows (arch 128 / contact 86 / proj 86) — promotion is a
  future per-prop-type ledger decision; (b) the design-migration arc (Landing hero migrated; scanner
  S6/S7 blue-channel reskin shipped; S2/THE WIRE/Offseason/article-media are GAP/unbuilt);
  (c) multi-book: SGO qualified (report-only), nothing wired; (d) **ROI is uncomputed in code and
  decomposes to MLB-C +4.57% / MLB-B −1% / WNBA −5%** (this file).
- **5.37 Spec / roadmap files** — VERIFIED (in `specs/`): `STATE.md` (running state),
  `VYNDR-NORTH-STAR.md`, `DESIGN-SPEC.md`, `ROW-GRAMMAR.md` (row grammar law), `LIVE-TRACKING.md`,
  `VOICE.md`, `model-train.md`, `phase-0-kill-the-lies.md`, `phase-1-truth-infrastructure.md`,
  `propline-audit.md`, `feature-1-1…4-1` + `a1-s3/s7/s9/s10` feature specs, `combat-intelligence.md`,
  `design-reference/` (the design bundle). Root docs: `CLAUDE.md`, `DECISIONS.md`, `ARCHITECTURE.md`,
  `AUTONOMY.md`, `BACKEND_HANDOFF.md`, `BUILD-STATE.md`, `BLOCKERS.md`, `PROMISE-AUDIT.md`.
- **5.38 Standing decisions a new session might contradict** — VERIFIED: `DECISIONS.md`
  (DECISION-001+ architecture log); CLAUDE.md's permanent rulings (never mint A's; A-RATED hold;
  data-semantics; do-not-wire list); STATE.md open items (A does not emit in prod; EV overconfident/
  unvalidated — hero ranks on ev_pct and picks the most overconfident read; grade_11 stored in
  model_snapshots). `AUTONOMY.md` = the zero-touch loop trace.

---

## CLAIMS I WAS ASKED ABOUT THAT TURNED OUT TO BE FALSE OR UNSUPPORTED

1. **"the ledger reports … 597 settled with 'no ROI'"** — the population is now **929 settled / 996
   total** (597 was an earlier snapshot); and **ROI IS computable** (`locked_odds` persisted) — it is
   simply **not computed in the ledger code**. "No ROI" = no computation, not incomputable.
2. **"Either ROI is not computed, or the model is selecting heavy favorites at fair prices"** — the
   binary is false; **both are partially true and it decomposes by sport/grade**: MLB-B = high-hit
   break-even favorites (the hypothesis), WNBA = losing, **MLB-C = genuinely +4.57% EV**. Not
   uniformly zero-edge.
3. **"the graded line is chosen by a book-agnostic consensus rule"** — **no consensus rule exists in
   code.** It is first-book-at-line (`dedupeProps`) + highest-grade-across-lines (`snapshotService`).
4. **Prior "137/140 single-book, draftkings 129 / fanduel 14" (~98% single-book)** — **corrected**:
   today MLB is 73% single-book with **5 books present** (DK/BetMGM/BetRivers/Pinnacle/FanDuel), and
   **WNBA is 82% two-book (DK+FD)**. The feed broadened.
5. **"the 11-point index is lost / unrecoverable"** — **it is retained in `model_snapshots.grade_11`**
   (2,888 rows); lost only from `ledger_entries`. Recoverable via join.
6. **"ZERO settled p_win yet / challenger measurement not begun"** (from prior challenger orders) —
   **stale**: arch-v1 128, contact-v1 86, proj-v1 86 rows are now settled.
7. **"BookComparison doesn't exist, only BookChip"** (an earlier order) — **BookComparison.tsx
   EXISTS**, it is just unrouted/dead.
8. **CLV framed as "held / not computed"** — **CLV IS computed** (842 rows) and closing IS persisted
   (962), but it is **largely degenerate** (closing==locked on 92%) due to the overwrite — a
   different problem than "not computed."
9. **WNBA implicitly treated as not-really-grading** (settlement described as MLB-only in CLAUDE.md) —
   **WNBA IS grading AND settling** (376 settled). The doctrine note about MLB-only settlement is
   contradicted by the data; the WNBA settle path should be confirmed.

---
*End of canonical state. Regenerate the measured numbers before citing them in a later session —
they move as the ledger accrues. Structural facts (file/line, schema, wiring) are stable until code changes.*
