# VYNDR β€” STATE OF THE WORLD ### As of `7ea0af2` (main, DEPLOYED + fingerprinted live), 2026-07-20. This file opens every future session. **Start with the CURRENT STATUS + OPEN ITEMS block below.** --- # 🧭 CURRENT STATUS + OPEN ITEMS β€” orientation block (2026-07-20, ~03:20 UTC) *Written to orient a fresh session. Ground truth, not optimism. Everything below was verified live unless explicitly marked UNVERIFIED.* ## What shipped tonight (all deployed + fingerprinted) | # | Shipped | Proof | |---|---|---| | 1 | **Probability layer revived** β€” it was DEAD in prod | `p_win`/`ev_pct`/`model_odds`/`value` on **32/32** live grades (was 0/8) | | 2 | **Value engine Arc 1** β€” de-vig, EV, value triplet, hero v2 | live: book βˆ’140 Β· fair βˆ’125 Β· model βˆ’109, `value:false` correctly | | 3 | **Grade-range work** β€” L20 symmetry, `refreshTeamStats` wired, consistency CV guard, `confidence_basis`, dead `mlbGrader.js` removed | suite 279/3325; D emitted on real WNBA data locally | | 4 | **Backup durable ON-BOX** | dump read back: **643 rows in dump == 643 live**, 18 MB | | 5 | **`model_snapshots` retention LIVE** (migration 025) | **100 rows** captured over 2 cycles, incl. **36 refusals** | | 6 | ESPN team-stats parser fixed (`buckets is not iterable`, 0/15 β†’ 15/15) | live refresh | ### ⚠️ Two honest qualifiers on the above - **A STILL DOES NOT EMIT IN PRODUCTION.** The 11-step grade is now *stored* (`grade_11`) and A/D are *arithmetically* reachable + locked by tests, but the Β±1.0 opponent factor is still dead because `opp_rank_stat` is underivable from ESPN (see open item 6). Live boards remain **B/C only**. **β†’ The "A-RATED" marketing hold STANDS** (`AccuracyBadge` correctly falls through to "MODEL Β· X% HIT"; `TopSignals` self-hides). Do not promote A-rated. - **EV IS OVERCONFIDENT AND UNVALIDATED.** First captured values include **+62.1% / +61% / +56.9%** β€” real markets don't offer that. Cause: `p_win` clamps at `PROB_CEIL` 0.95 off ~10 games. **Hero v2 ranks on `ev_pct`, so it picks the MOST overconfident read.** Do not surface EV as truth until calibration runs on the harness. ## 🟒 OFF-BOX BACKUP β€” CLOSED. FULL ROUND TRIP PROVEN (2026-07-20 05:25 UTC) **Root cause (Kev):** the vyndr-backup key was only in Hetzner's PROJECT ssh-key store, never in the Storage Box's own `.ssh/authorized_keys`. Installed there β†’ the box now offers `publickey,password` (it previously offered an EMPTY auth list, which is what "SSH not enabled on this box" looks like). **Proven end-to-end:** - `POST /api/internal/backup/run` β†’ `ok:true`, **`offbox_ok:true`**, exit 0, 31s. - Output confirms both env questions: **`ssh key: base64-decoded`** (the key is correct base64, not a mangled raw paste) and destination **`u635423@...:vyndr-backups/`** β€” no leading-slash bug. - **File VERIFIED on the box, not inferred from exit 0:** `GET /api/internal/backup/offbox` (rsync `--list-only` through the pinned host key) returned `vyndr-20260720-051158.dump`, **833,917 bytes, 05:12:28 UTC** β€” byte-identical size to the local dump, timestamp from that run. **Hardening shipped with it:** - **Host key STATICALLY PINNED.** `ssh-keyscan` matched the out-of-band fingerprint `SHA256:XqONwb1S0zuj5A1CDxpOSuD2hnAArV1A3wKY7Z3sdgM`; `scripts/storagebox_known_hosts` ships to the container and the script uses `StrictHostKeyChecking=yes` + `UserKnownHostsFile=`. `accept-new` (trust-on-first-use) is GONE; a missing pin file REFUSES the push. Tests assert the weakenings never return. - **Remote dir guaranteed** β€” `--mkpath`, else remote `mkdir -p`. Without it a push into a missing parent can write the dump AS the directory name: one file, overwritten nightly, reading as "backups exist" while retaining exactly one. - **A failed required push now PAGES** at urgent (was "low/deferred") and the run reports `offbox_ok:false`. Exit code still tracks ON-BOX durability so a good on-box dump never raises a false total-failure alarm. ### βœ… ROUND TRIP CLOSED β€” pulled back off-box and rebuilt into a live DB (05:25 UTC) The full insurance chain is now PROVEN, not assumed: dump β†’ validated β†’ pushed off-box β†’ verified on the box β†’ **pulled back down from the box β†’ restored into a live Postgres**. - **Pulled FROM the Storage Box** (not the local copy) with the in-session key through the pinned host key (`StrictHostKeyChecking=yes`, never bypassed): `vyndr-20260720-051158.dump`, 833,917 bytes β€” byte-identical to what was pushed. - **Restored into scratch Postgres 17** (docker): 715 archive objects, **42 public tables**, `ledger_entries` with all **27 columns** and real rows (spot-checked: Kelsey Mitchell points o22.5 B βˆ’105 hit). - **ASSERTION PASSED β€” `ledger_entries` restored = 645, live = 645** (target was β‰₯ 645). `model_snapshots` also restored 100/100, so the new retention store is covered by backups from day one. - Scratch DB torn down; the pulled copy deleted; **both dumps still on the box** (nothing rotated out, cron untouched). **πŸ”΄ OPERATIONAL GOTCHA FOUND DURING THE RESTORE β€” record this in any DR runbook:** the dump is written by **pg_dump 17** (Supabase is PG 17.6), so **pg_restore 16 CANNOT read it** β€” it fails with `unsupported version (1.16) in file header`. The first restore attempt failed for exactly this reason. **Restore with PG 17+ tooling** (`postgres:17-alpine` works). Restoring into vanilla Postgres also logs `errors ignored on restore: 12` β€” Supabase-specific roles/extensions that do not exist locally; harmless, the data restores completely. ## Open items β€” with status | Item | Status | Note | |---|---|---| | **Settlement: 0 pushes / 470 settled** | πŸ”΄ OPEN, unstarted | Implausible β€” hits/TB land on the number regularly. Exact-number push almost certainly mis-scored as hit or miss. Corrupts every accuracy/ROI number. | | **~28 props/day never settle** | πŸ”΄ OPEN, unstarted | Jul 17 MLB 86 graded/57 settled; Jul 18 103/75. Cause undiagnosed. | | **Model-version contamination** | 🟠 PERMANENT, mitigate | `ledger_entries` mixes pre/post-2026-07-19-fix grades with no marker; eras cannot be separated retroactively. **Any backtest/accuracy claim off existing ledger history MUST treat the fix boundary as a hard cutoff.** `model_snapshots` stamps `model_version`+`code_sha` so it can't recur. | | **A-grade unreachable in prod** | πŸ”΄ OPEN | `opp_rank_stat` null; ESPN team endpoint has no defensive metric at all. Marketing hold stands. | | **EV overconfident** | 🟠 OPEN | Needs calibration before it drives any surface. Hero v2 already ranks on it. | | **`edge_pct` broken scale (U-deg pt 2)** | πŸ”΄ OPEN | 51.5% of ledger rows exceed the sane cap; worst 620. 13 frontend surfaces render it; **nothing renders `ev_pct`**; it's the free-tier hook; it's written to the append-only `edge` column every cron. | | **CLV broken (C4)** | πŸ”΄ OPEN | `closing_line == locked_line` on ~95% of rows. BEAT CLOSE suppressed. **CLV ledger stays PRIVATE until backtest-proven.** | | **Consistency CV floor** | 🟠 STOPGAP | `CONSISTENCY_MIN_MEAN=4` leaves a Β±1.0 dead for MLB low-count stats. Real fix = index-of-dispersion classifier; needs a backtest first. | ### πŸ”‘ THREE KEYS TO ROTATE (all currently valid) 1. **Storage Box password** β€” Kev plans a fresh reset; do it before/with the SSH fix. 2. **`VYNDR_INTERNAL_KEY`** β€” was pasted in plaintext into a chat transcript (2026-07-19) and is in the local `.env`. It gates snapshot triggers, backup runs, settle passes and the newsletter send. **Rotate: Coolify env + local `.env` together.** 3. **GitHub PAT in the `origin` remote** β€” plaintext in `.git/config`, leaked since ~Session 31. Deploys run off `gitea` so it's inert, but it is a live credential. Revoke on GitHub; the remote can simply be deleted. ## Next queued work (in order) 1. **Backtest harness** β€” reads `model_snapshots`. **Needs ~2 weeks of history**; as of now it holds ONE night. Its honest first output is *"cannot validate yet"* and that is the expected, correct result. Build the instrument now, let data accumulate. 2. **Settlement-correctness audit** β€” the two bugs above. Highest-value correctness work: everything downstream trusts hit/miss. 3. **A-grade / opponent-strength sourcing** β€” MLB is **solved and verified**: `statsapi` `teams/stats?group=pitching` returns all 30 teams free with `era/whip/avg/slg/ops/HR9`, and it's *stat-specific*, which beats what we were fetching. NBA/WNBA need `stats.nba.com` or derived allowed-points. Implement behind the **source-adapter pattern** (health check that PAGES on empty β€” tonight's ESPN crash was invisible for exactly that reason). 4. **Metrics engine** β€” archetype-baselined proprietary metrics (`specs/model-train.md` Β§2C). **Gated on the harness**: every metric family ships only if it backtests better than without it. **Governing docs:** `specs/model-train.md` (value engine + phases + north star) Β· `specs/snapshot-retention.md` (Phase 2 report + schema) Β· `specs/audit-data/grade-collapse.md` Β· `specs/audit-data/gate-simulation.md`. --- ## 🟒 RETENTION CLOCK TICKS ON ITS OWN (2026-07-20 06:05 UTC) Induced the REAL cron entrypoint rather than waiting for 14:00 UTC. - **Cron capture needed NO wiring β€” verified in code.** `snapshotScheduler`'s tick calls `runAll` = `snapshotService.runAllSnapshots` β†’ `runSnapshot` per sport β†’ `gradeAndCacheSlate` β†’ `onGraded` β†’ retention. **The scheduled and manual paths are the same function.** Nothing had been captured only because no slot had fired since retention deployed (~02:55; slots 14/19/22/1/3). - **PROVEN by induction:** `POST /api/internal/snapshot/all` (= `runAllSnapshots`, the exact function the tick calls) wrote cycles at **06:03:23 mlb** and **06:05:09 wnba**, 50 rows each, `cycle_hour_utc=6`, features 50/50. *(The HTTP request gateway-timed-out at ~2 min; the job completed server-side β€” verified in the DB, not from the response.)* - **`archetype` FIXED β€” 50/50 on MLB** (was 0/100). Retention still COLLECTS at grade time (only moment features exist) but now PERSISTS after enrichment via `retentionService.mergeEnrichment`, which fills ONLY archetype/team/opponent and never rewrites grade-time features or model output (test-locked). - **πŸ”Ά `team`/`opponent` STILL NULL β€” upstream, not the merge.** The ENRICHED grades themselves carry `team:null` (verified live: 0/25 with team, 25/25 with archetype). `resolvePlayerStats` doesn't return `stats.team` for MLB, and enrichment never sets `opponent` at all. Fixing that is an upstream change, deliberately not made under this order. - **WNBA archetype is null** β€” honest absence: WNBA stats don't resolve (Python service offline), so there is nothing to classify from. - **ZERO-WRITE ALARM ARMED** β€” `opsWatch.retentionZeroWriteAlarm` pages at missed-snapshot severity when a slot GRADED props but retention wrote fewer rows than the slate. `runSnapshot` now returns `retentionRows` so the tick can evaluate it. Retention is best-effort by design, so a broken write is silent by construction; this is the counterweight. Verified quiet on a healthy run (gradeCount 25 β†’ retentionRows 50). - **INTRADAY DEFERRED (Phase 4).** `intradayRefreshService` calls `analyzeViaEngine1` DIRECTLY β€” it never goes through `gradeAndCacheSlate`, so there is no `onGraded` hook to reuse. Capturing it needs its own ctx + snapshot_id + persist path and would ~5Γ— row volume. Not small; not done. - Outcome stamping still NOT implemented (depends on the settlement fix). ## 🟒 RETENTION LIVE β€” model_snapshots capturing (2026-07-20, migration 025 applied) Phase 2 priority zero. **History now compounds from tonight.** Spec: `specs/snapshot-retention.md`. - **Why:** the ONLY model history was `ledger_entries` (640 rows / 6 game days) and it stores NO model inputs. All ~30 other warehouse tables are empty. We could score our own grades but could not replay a different model β€” the only question a backtest answers. - **`model_snapshots`** (migration 025, APPLIED): append-only, ONE ROW PER GRADED PROP PER SIDE PER CYCLE. Unique on `(snapshot_id, player_key, stat, line, side)` so a retried cycle can't duplicate. RLS on, service-role writes only. - **VERIFIED CAPTURING (2026-07-20 03:02 UTC, 2 triggered cycles): 100 rows.** MLB 50 (14 graded / **36 refused**), WNBA 50 (50 graded). `features`, `grade_11`, `p_win`, `ev_pct` populated on 100% of graded rows; 1 model_version. - **Refusals are captured β€” the ledger drops them entirely.** First-ever refusal breakdown: `juiced_no_edge` 18, `rare_event_over_below_line` 13, `insufficient_data` 5. **The MLB gate refused 36 of 50 sides (72%)** β€” whether that costs edge is now measurable for the first time. - Contract: retention is best-effort + injectable; `persist()` returns errors and never throws. **A retention failure can never break a snapshot.** - πŸ”Ά **EV LOOKS OVERCONFIDENT β€” do not surface it as truth yet.** First captured EVs include +62.1%, +61%, +56.9% β€” implausible for real markets. Cause is the quantile estimator clamping `p_win` at 0.95 (`PROB_CEIL`) off ~10 games. **Hero v2 already ranks by highest `ev_pct`, so it will select the MOST overconfident read.** Calibration (C-cal) must run on the harness before EV drives anything user-facing. ## πŸ”Ά SETTLEMENT-CORRECTNESS AUDIT β€” two concrete findings (open, scheduled) Logged from the Phase 2 history report; do NOT fix inside a retention build. 1. **ZERO pushes across 470 settled rows.** Implausible β€” hits/TB land exactly on the number regularly. The exact-number push case is almost certainly mis-scored (silently counted as hit or miss). 2. **~28 props/day never settle** (Jul 17 MLB 86 graded/57 settled; Jul 18 103/75). Cause undiagnosed. Both are correctness bugs in the scoring that accuracy, ROI, CLV and every public record depend on. ## πŸ”΄ MODEL-VERSION CONTAMINATION in ledger_entries (permanent, mitigate by cutoff) `ledger_entries` mixes pre- and post-2026-07-19-fix grades with **no version marker**, and the eras cannot be separated retroactively. **Any backtest or accuracy claim computed off existing ledger history must treat the fix boundary as a hard cutoff.** `model_snapshots` stamps `model_version` + `code_sha` on every row so this can never recur. ## 🟒 BACKUP β€” DURABLE ON-BOX, VERIFIED BY READ-BACK (2026-07-20, deployed) The highest-severity open item is closed **on-box**. Off-box is a tracked follow-up. - **Runs as CODE, not a cron somebody must install** (`src/backupScheduler.js`, armed in `server.js`). Arming is opt-OUT: armed whenever `SUPABASE_DB_URL` exists, `BACKUP_CRON=0` kills it, nightly `BACKUP_HOUR_UTC:BACKUP_MINUTE_UTC` (default 03:10 UTC). The S62 design was opt-in and nobody ever opted in β€” that is why the DB went unbacked for weeks. Deploy == installed now. - **DURABLE:** `BACKUP_DIR=/app/backups` is a Coolify persistent volume, so dumps survive redeploys. The container-ephemeral risk is closed. - **VERIFIED END-TO-END (2026-07-20 02:31 UTC):** `POST /api/internal/backup/run` β†’ exit 0 in 31s β†’ `vyndr-20260720-023136.dump`, **821,549 bytes**, 698 archive objects, ledger_entries present. `GET /api/internal/backup/verify` read the rows back OUT of the archive with `pg_restore`: **643 rows in the dump vs 643 live β€” exact match**, DB 18 MB. - The earlier figure of 604 was a moving target, not a discrepancy: the ledger is append-only and 39 rows were written between the census and the dump (2 manual regrades + the 01:00 UTC cron; newest lock 01:01:45 UTC). 604 + 39 = 643 reconciles exactly. - **Permission bug found and fixed by the real run:** the first attempt failed with `pg_dump: Permission denied` β€” the container runs as non-root `vyndr` (uid 100/gid 101) and the mounted volume was root-owned. The Dockerfile now creates + chowns `/app/backups`; the verify endpoint reports `uid`/`gid`/ `backup_dir_writable` so a mounted-but-unwritable volume is loud, not silent. - **πŸ”Ά OFF-BOX IS DEFERRED, NOT DONE.** Storage Box SSH auth is not working yet, so the rsync is gated behind `BACKUP_OFFBOX=1` (+ `BACKUP_REMOTE` + `BACKUP_SSH_KEY`) and currently logs "DEFERRED". A failing push is a LOW priority notice, never a failure β€” the durable on-box dump succeeded, and crying wolf trains us to ignore backup alerts. **Remaining risk: a single Hetzner-side loss takes the dumps with it.** To finish: install the generated public key on the Storage Box, set `BACKUP_SSH_KEY` in Coolify (port 23 β€” 22 is SFTP-only), flip `BACKUP_OFFBOX=1`. - **Not yet done:** a full restore into a scratch Postgres SERVER. The read-back proves the archive CONTAINS the data; a server restore would additionally prove schema/constraint restorability. It needs the dump off the container, which is blocked on the same Storage Box auth. ## MODEL TRAIN β€” ARC 2 PREP: DEAD PROBABILITY LAYER REVIVED (2026-07-19, on main, DEPLOYED + fingerprinted) Re-sequenced ahead of the gate work after the audit found the ranking signal did not exist. Reports: `specs/audit-data/gate-simulation.md` (G-b/C-cal) + `specs/audit-data/grade-collapse.md` (the big one). - **p_win/ev_pct/kelly/model_odds/value were absent on 100% of live grades** β€” `gameLogService` returns null for MLB by construction + the Python service is offline, so `meta.gameLogs` was `[]` everywhere. EV, the Model Train's whole ranking signal, had never been computed on a live prop; hero v2 always fell through to its fallback; quarter-Kelly (sold on /pricing) never ran. FIX: `featureCache.getStatRows`. **Fingerprinted live: p_win 0.523, ev_pct -10.4, model_odds -109 β€” the value triplet is whole.** - **Grade range restored ON MERIT** (rescaling permanently rejected): `refreshTeamStats` wired into `runSnapshot` (it had ZERO callers), L20 made symmetric (no negative branch existed), `game_count_in_7d` derived. D now emits (WNBA 1/25, earned at p_win 0.365). **A-emission awaits the 01 UTC snapshot** β€” `opp_rank_stat` needs that run. MARKETING HOLD on "A-RATED" copy until then. - **Calibration guard:** consistency CV is NBA-tuned; `cv ~ 1/sqrt(mean)` made every MLB stat `boom_bust`. Floored at `CONSISTENCY_MIN_MEAN=4`. - Ledger truth (576 rows): only B/C ever emitted, 9 confidence values, ceiling 63; past -400 was -7.7% ROI (the -400 floor was the whole win); flex -161..-250 is our BEST band (+2.2%) so its EV gate ships DISABLED. Suite 276/3286, build 0. ## MODEL TRAIN β€” ARC 1 (ENGINE) SHIPPED (2026-07-19, on main `7a925f4`, pushed, NOT yet deploy-fingerprinted) Spec written retroactively: `specs/model-train.md` (arc list + what's open). Goal of the train: make "real opportunities at takeable prices" the ENGINE, not a display filter. Arc 1 = steps 1–6, backend only. **No frontend consumes the new fields yet** β€” the triplet/VALUE marker/refusal copy are Design's Session-2 surfaces. - **De-vig** (`src/utils/devig.js`, NEW): two-way multiplicative de-vig β†’ fair prob + fair American price per side + `overround`. One side unpriced β†’ fair is NULL, never faked. Also `americanToImpliedProb` / `americanToDecimal` / `impliedProbToAmerican`. - **EV** (`devig.evPct`): `ev_pct = modelProb Γ— decimal βˆ’ 1`, at the graded side's ACTUAL price. This is the ranking signal now, replacing raw |model βˆ’ consensus|. - **Takeable / value gates** (`src/config/valueEngine.js`, NEW): `TAKEABLE_ODDS_CEILING` **βˆ’160**, `TAKEABLE_ODDS_MAX` **+200**, `VALUE_EV_THRESHOLD` **2** (%) β€” all env-tunable. `takeable` = price in band; `value` = takeable AND ev β‰₯ threshold. Strict null guards (`Number(null)===0` would have made a missing price "takeable"). - **Hero v2** (`heroPropService`): the hero is the highest `ev_pct` among **takeable** A/B reads β€” a huge model-vs-line gap on a βˆ’900 line is trivia, not an opportunity. Falls back to most-recent-real graded read on an empty slate (unchanged). - **Value triplet** on every graded read: `book_odds` Β· `fair_odds` Β· `model_odds` (+ `fair_prob`, `overround`, `devig_method`). Wired inside `analyzeViaEngine1`'s existing p_win/Kelly block; rides the `...result` spread through snapshot / hero / scan. Documented in `BACKEND_HANDOFF.md`. - **Test baseline: 276 suites / 3306 tests green** (was 274/3289), `next build` exit 0. New suites: `devig.test.js`, `valueEngine.test.js` (+ `heroPropService.test.js` rewritten). - **Knobs NOT in this arc** (asked about, confirmed absent from the codebase): `EDGE_FLEX_WALL`, `HARD_JUICE_WALL`, `LADDER_ODDS_MAX`, `MIN_RUNG_PROBABILITY`. The only price wall on disk is the flat `JUICE_ODDS_FLOOR` (βˆ’400) from the rare-event work below β€” it is **not** edge-aware, and there is no βˆ’250 wall. ## RARE-EVENT UNDER SUPPRESSION (2026-07-19, on main) Betting-logic audit: the CONSENSUS-vs-MODEL board flooded with "doubles u0.5 Β· MODEL 0.2 Β· +edge" fake reads (juiced rare-event unders). Report: the doubles projection is REAL per-player (MLB_LOG_FIELD doublesβ†’doubles; values varied 0.03/0.16/0.2/0.22) β€” NOT a flat fallback; the issue is purely structural. - **Config-driven** (`src/config/rareEventMarkets.js`): RARE_EVENT_STATS = doubles/triples/home_runs/stolen_bases, RARE_EVENT_LINE_MAX = 0.5. - **Grade layer** (`analyzeViaEngine1`): rare-event UNDER at ≀0.5 β†’ always refused (grade null + suppressed); rare-event OVER at ≀0.5 β†’ refused unless projection > line (a 0.2-over-0.5 carries the SAME |edge| as the under, so it'd just take its board rank β€” refusing it is what actually clears the market). OVER with a genuine projection > line still grades. - **Board layer** (`marketBreadth.collectBreadth`): drops null-model rows β€” a suppressed/ungraded prop can't rank a "MODEL β€”" placeholder onto the board. - Suite 274/3289 green. Fingerprint: next MLB snapshot should have no rare-event u0.5 grades. ## BACKUP + FOUNDER CHECKOUT (2026-07-18, on main) - **Task A β€” DB backup (SHIPPED; Kev cron+fingerprint on box)** `c2c43cd`: Dockerfile now has pg_dump/pg_restore/rsync; `backup-db.sh` validates every dump via `pg_restore --list` (must contain ledger_entries). Runs IN the API container (SUPABASE_DB_URL is there; WSL2 can't reach Supabase). Runbook = host cron `docker exec sh /app/scripts/backup-db.sh`. **Mechanism fingerprint PASSED locally** (137 rows β†’ dump β†’ validate β†’ restore β†’ 137 rows). Kev: install the host cron + run the prod restore fingerprint. - **Task B β€” founder checkout SEAT-GATED (SHIPPED)** `ccb9668`: `resolveCheckoutPrice` attaches the founder price while seats remain (< FOUNDER_SEATS_TOTAL, same `countFounderSeats()` truth as the meter), flips to standard at 100; meter shows SOLD OUT. Fixes "Claim a Founder Desk" charging $44.99 vs advertised $34.99. **payment_failed grace 48hβ†’14d** (spans Stripe retries; revoke only on real cancel). Tested seat 0/99/100/null. Needs `STRIPE_PRICE_*_FOUNDER` set in prod for founder pricing to activate. ## SECURITY + PLUMBING FOLLOW-UP (2026-07-18, on main) β€” code shipped, migrations authored Chrome ops session findings (Supabase advisor + Stripe). Code items shipped + fingerprinted; DB migrations authored for Kev to apply (no DB access from the box). - **Item 0 (SHIPPED, fingerprinted)** `3b12c6c`: founder count = real active Stripe subs (`stripeService.countFounderSeats`), not DB tier fields. Phantom 1 (a comped profile) gone β†’ prod now `available:false` (founder price IDs unset in prod env β€” set `STRIPE_PRICE_*_FOUNDER` to show the real 0). - **Items 1,3,4,5 (AUTHORED β€” Kev applies in Supabase)** `78c1929`: `migrations/023_security_hardening.sql` (founder_pricing_seats β†’ security_invoker; waitlist anon-INSERT-only; function search_path) + `migrations/024_anon_revoke_discoverability.sql` (revoke anon SELECT). Fingerprint = re-run Security Advisor. - **Item 2 (SHIPPED β€” Kev sets env + cron)** `5f5c004`: `scripts/backup-db.sh` + `docs/BACKUP-RUNBOOK.md`. Needs `SUPABASE_DB_URL` (direct :5432 URI) in Coolify + the cron on the Hetzner box. Fingerprint = restore a dump + count ledger_entries. - **Item 6 (SHIPPED β€” Kev activates portal in Stripe)** `889e862`: portal proxy + "Manage billing" button. Dunning verified: cancel-on-exhaustion β†’ subscription.deleted β†’ 48h grace β†’ `gracePeriod.checkGracePeriod` revokes to free. Nuance: 48h grace on first payment_failed < Stripe's 2-week retry window (self-correcting). - **Item 7 (SHIPPED)** `ae3cff9`: ESPN BET β†’ theScore Bet (PENN) in books.js + ALLOWED_BOOKS; design HANDOFF flagged for the BookChip swap. ## TRUTH-EVERYWHERE PART 2 β€” COMPLETE (2026-07-18, on main, all 8 items fingerprinted) Standing law: NO FABRICATED DATA ANYWHERE (UI, marketing, demos, examples). If it renders a number, it's from the DB or it doesn't render. Live data IS the better demo. - **Item 1 β€” Verb law** (`66d52a9`): one verb, READ (never SCAN), swept every user-visible surface (FAB/nav/tier/ticker/upgrade). `tests/unit/verbLaw.test.js` enforces. Ticker "slate read" text flips at the next snapshot cron. - **Item 2 β€” Founder counter** (`41fc2b9`): real active-paying-founder count from Stripe-synced user_profiles, cached; HIDES when unavailable. Live: **1/100** (was fabricated 47/100). - **Item 3/4 β€” Reports**: full fabricated-data inventory + pricing claim audit (LIVE/PARTIAL). Alt-ladder/Kelly/correlation are LIVE in the engine; scratch-cascade + real-time-feed are PARTIAL. - **Item 5 β€” Daily hero prop** (`9b9aab4`): live RULE = largest |modelβˆ’consensus| gap, A/B only. Shows book line vs VYNDR model + real timestamp. Emptyβ†’most-recent-real; nothingβ†’hidden. NO static Jokic fallback. `heroPropService` + `/api/hero-prop`. Live: Erica Wheeler 9.5 vs 14.2. - **Item 6 β€” Desk showcase** (`cb3237c`): real alt-line ladder from the snapshot (edge guarded on small-line artifact); Ο† = real same-team correlation for two real legs or hidden; Kelly tile REMOVED (no odds β†’ can't size honestly). `deskShowcaseService` + `/api/desk-showcase`. Live: Vandersloot ladder. - **Item 7 β€” Accuracy β†’ TRUTH** (`89a2977`): public 30D accuracy (`/api/accuracy` + `/api/ledger/accuracy`) now reads the CLEAN Postgres ledger aggregate (getModelAggregate/getAccuracyView, model_value>0), not the Redis outcome log. Number moved 67%β†’61%. **BEAT CLOSE suppressed** at source (`clvCaptureReliable()`) β€” see C4. - **Item 8 β€” Blog** (`3b7a1f5`,`a8e383e`): wired the 5 REAL articles (content lives in `web/content/articles` β€” the runtime content root, NOT repo-root content/) with honest dates + markdown render + OG. Backdated 2026-03-22 orphan deleted + 301. Live: 5 articles serve. ### πŸ”΄ C4 β€” CLV CAPTURE BROKEN (open, backend) `captureClosing` re-records the LOCKED line as "closing" (closing_line == locked_line on the whole sample) β†’ CLV is flat β†’ BEAT CLOSE = a measured-wrong 0%. Suppressed on all public surfaces until fixed. Full write-up + fix plan: `specs/audit-data/clv-capture-broken.md`. Flip `CLV_CAPTURE_RELIABLE=1` after C4 verifies real closes. ## PHONE AUDIT PUNCH LIST β€” COMPLETE (2026-07-17, on main, deployed, fingerprinted) Kev's 35-screenshot real-390px audit (signed-out + Desk signed-in). Worked top-down, fingerprinted the batch (compare "on current form" literal live in served chunk off `71e35e9`). Test baseline: 268 suites / 3239. - **P0-1** blue-B/amber-C banished β†’ token-derived grade badges (earlier batch). - **P0-2** image pipeline (logos/headshots) `loading=eager` (earlier batch). - **P0-3** player grouping/ladders across leaders/explore/ledger/breadth (`lib/playerGrouping.js`). - **P0-4** mobile app bar collapsed to one line (ticker+heartbeat hidden <768, `MobileSyncClock`). - **P1-5** 390px overflow containment (breadth rows stack, pitcher line wraps). - **P1-6** (`ff0d3b1`) ONE freshness source: Slate "UPDATED" now reads the pipeline `refreshed_at` (the app-bar clock's field), not the client poll time β€” no more "0s ago" vs "SYNC 46:03" contradiction. - **P1-7** (`d755b43`) flat edge board: an impossible `|edge|>40` is treated ABSENT at both data (can't rank a fake +140% over a real +8.4%) and display layers. Chips render now (P0-2). `EDGE_BOARD_SANE_MAX`. - **P1-8** (`00537eb`) Compare verdict truth: no phantom "edge tonight" for July NBA(0) β†’ "on current form"; cites what actually wins (scoring/boards/playmaking, not usage). - **P2-9** (`77e8937`) leaderboard stat labels `web/src/lib/statAbbrev.js` (SB/ER/TB) + **FLAGGED** the backend grade degradation (below). - **P2-10** (`71e35e9`) ledger read-card density (padding/margins tightened; archetype showDesc kept β€” it's inline, zero height cost). ### πŸ”΄πŸ”΄ BACKEND FINDING β€” MLB GRADE PIPELINE DEGRADED (needs backend work, NOT frontend) Diagnosed against live `/api/snapshot/mlb` (2026-07-17). Full write-up + do-not list: `specs/audit-data/mlb-grade-degradation.md`. Frontend guards (sane-edge cap, statAbbrev) are damage control, NOT the fix. - **projection == 0 for 9/25** grades β€” the MLB feature path feeds 0 instead of refusing (S58 `insufficient_data`). This also breaks `computeEdge` β†’ the broken edge_pct. - **edge_pct on a broken scale** {20,60,100,140} β€” not a market %. Real prop edge is single-digit. - **grade↔confidence mismatch on 25/25** β€” B shown at conf 55 (= the B- band per `grade_thresholds.json`); systematic one-sub-tier gap. The surfaced `confidence` is not the probability that derived the letter (likely the data-sufficiency penalty applied to display-only). This is the audit's "B at 45%" β€” NOT a frontend labeling or threshold bug; DO NOT re-letter/re-threshold on the frontend. ## DESIGN COMPLETION TRAIN β€” SUBSTANTIALLY SHIPPED (2026-07-16, on main, deployed) Β· ONE MASTER CHROME AUDIT PENDING Goal: the live product matches Design's mockup at EVERY width. **HONESTY LAW: I cannot see rendered output (WSL2↔Chrome unreachable) β€” every visual change is "built to Design's spec, VISUALLY UNVERIFIED." Kev runs ONE master Chrome audit at the END (390px + desktop) against the running unverified list below; fixes propagate through tokens (surfaces inherit, don't fork).** Design package (3 revs) authoritative at `specs/design-reference/` (HANDOFF.md = entry point; `vyndr-system.html` desktop, `vyndr-mobile.html` 19 screens, `vyndr-landing.html`, `assets/glyphs/` 83 SVGs+MANIFEST). SHIPPED + deployed: - **Palette alignment** (`e0a26a8`,`c3d469e`,`ebaab00`): tokens β†’ Design's EXACT ramp. **Grade colors corrected: B blueβ†’WHITE #F0F0F0, C amberβ†’GREY #B8BCC8, D #FF4757** (the old blue/amber violated DESIGN-SPEC's own "B neutral-bright, C muted"). Surfaces #0E0E14/#14141E, text #F0F0F0/#B8BCC8/#707080, borders #1E1E2A/#2A2A38. vyndrTokens + tests aligned. ShareCard/OG billboards grade-B white. - **Glyph system** (`77d8fd6`,`771d8b0`): 44 archetypes render Design's REAL marks (83-mark set: 74 display + 9 classifier-legacy), front `lib/archetypes.js` + back `archetypeService.js` color-synced. ZERO generic placeholders left. Combat keeps its char-glyph namespace (separate test). - **Entity layer VERIFIED already-complete** (DS0/Wave 2A/2B β€” the audit was stale): TeamLogo (real ESPN CDN, monogram fallback) on game cards/team hub; PlayerAvatar headshots MLB/NBA/WNBA + NFL/NHL resolver ready + soccer-monogram (blocked) + combat-no-photos; books via bookInfo/BookChip everywhere (fixed BookComparison holdout `1558710`); line-shopping surfaces (detectBestBook/MarketBreadth) self-deepen. No `$1M` copy. - **Rev 3 matchup chips** (`f24f941`,`baf977f`,`bf8bf16`): `TeamChip` primitive (real TeamLogo at 10-12px + abbr, inherits row opacity) on StatStrip board rows, grade reveal, pitcher-vs. Game-card headers already had TeamLogo. - **Mobile: M1a header collapse** (`a10227e`) Β· **M1b app-bar clock** (`7c7ab24`, Hybrid: wall clock rests, STALE reacts off refreshed_at) Β· **grade-reveal mobile** (`baf977f`, 74px hero) Β· **flat EDGE BOARD** (`8a24ac9` β€” Design's core mobile screen: NEW flat edge-ranked IA, `slateAdapter.flattenToEdgeBoard`+`MobileEdgeBoard`, shown <768px, cards β‰₯768px; threaded edge_pct through the pipeline) Β· **M4 structural locks** (`6bc9093`). - **Key finding:** the 19 mobile screens were LARGELY already built (pages built to the SAME DS0-DS5/Β§7 contracts Design's mobile follows) β†’ they already match structurally + stack on mobile. The genuinely-new work was the flat board (done) + narrow chip/hero polish. REMAINING (all VISUAL, for the master audit): per-screen mobile polish of the ~17 already-built screens (blind, mostly Design-aligned) Β· M2 billboard crop-ratio verification (OG routes exist, colors fixed). Test baseline: 266 suites / 3214. ### πŸ”΄ MASTER-AUDIT UNVERIFIED LIST (every visual change this train β€” Kev's one end-of-session audit) 1. **Palette** (biggest): grade B-white/C-grey/red-#FF4757 + full surface/text/border ramp β€” every surface inherits. 2. **Archetype marks + colors** β€” 44 archetypes render Design's real 83-mark set. 3. **Flat EDGE BOARD** at 390px β€” the core mobile screen (rows/opacity-ramp/chips/hero-edge%). 4. **Grade-reveal mobile** (74px hero + team chip) Β· **app-bar clock** (Hybrid) Β· **M1a header collapse**. 5. **TeamChip** on board rows / grade reveal / pitcher-vs (real logo at 10-12px). 6. **OG billboards** (ShareCard/player/u) grade-B white + text #F0F0F0. ## SHIPPED β€” HEARTBEAT + CADENCE + QUOTA-GUARD (2026-07-15, on main, deployed) Three ships, each green + deployed. Standing authority is now PERMANENT full autonomy (three-gate floor: tests green, web build exit 0, post-deploy fingerprint of NEW code). - **`7712f0a` Heartbeat honesty (fingerprint-verified live):** the "frozen snapshot / STALE 8h" was a MEASUREMENT bug, not a dead pipeline. `updated_at` = grade-LOCK time (advances 5Γ—/day by design; grades never change in-game); the SYNC badge measured the 20-min `expected_interval_s` against it β†’ structurally STALE. `refreshed_at` (the real 20-min intraday heartbeat) was written to Redis but NEVER serialized. FIX: full snapshot seeds `refreshed_at`, `/api/snapshot/summary` + GET `/:sport` expose it, `LiveLayer` badge measures from it. Fingerprint PROVED the pipeline was alive all along (`refreshed_at` advancing 20:20β†’20:40β†’21:00β†’22:00; the 22:00 full slot fired β†’ scheduler healthy, re-armed fine after the 19:37 restart). - **`4cd933d` + `2d413cf` Per-sport cadence (Job 1):** `src/config/sportCadence.js` (config-over-constants). REALITY-CORRECTED: mlb/nba/**wnba** all keep the full grid `14/19/22/1/3` (PropLine 9k/day = cheap; WNBA games span afternoonβ†’late-evening, so 14 UTC/10am ET catches afternoon-game props β€” the earlier "drop it as waste" was wrong, corrected by observing 2 finished afternoon WNBA games on Jul 15). **Soccer alone is trimmed** to `14/19` + NO intraday β€” the genuine win, protecting the scarce odds-api key. Scheduler fires at HOURS_UTC and grades only `sportsForHour(h)`; only `intradaySports()` refresh. - **`2d413cf` Quota guard (the real finding):** odds-api quota was EXHAUSTED 0/500 mid-cycle, unpaged. Root cause: `futuresService` (the only regular burner) used raw axios, BYPASSING the gateway β†’ never hit `recordCall` (the ONLY place the WARN/BLOCK pager fires) β†’ the counter reached 100% via silent header-sync. FIX: futures now routes through `gateway.fetch('odds-api', …)` (counted + blocked); a RESERVE floor (`ODDS_API_RESERVE`=50) refuses DISCRETIONARY calls (futures/soccer) while `remaining ≀ reserve` so they can NEVER starve MLB's essential prop-backup; `quotaTracker.syncFromHeaders` (authoritative) now ALSO fires the once-per-period alert; `POST /api/internal/quota/test-alert` test-fires the pager end-to-end. **Fingerprint caveat:** backend-internal, no unauthenticated public surface (`/api/internal/*` 401s for any path β€” auth runs before route-match); verify via authenticated test-alert (200=new/404=old) with the internal key. - **odds-api 0/500 β†’ BOTH soccer props AND futures are blocked on QUOTA, not code.** Kev's ruling: HOLD THE LINE (don't pay; zero-out-of-pocket). Soccer stays honest-empty; cadence is ready for the reset. Env: keep `API_FOOTBALL_KEY` (code reads it), delete inert `API_FOOTBALL_KEY2` (Kev handling). See memory [[odds-api-quota-soccer-blocked]]. Test baseline: 264 suites / 3195. ## SHIPPED β€” TRUTH-EVERYWHERE + OFFSEASON HUB TRAIN (on main, deployed) Governed by `specs/truth-everywhere-train.md`. **The reframe that drove it: only MLB GRADED in prod (NBA/WNBA refused their whole slate β€” offline Python feature source, no fallback; soccer errored pre-grade). So grading, not settlement, was the first domino.** Standing deploy authority granted (green + fingerprint β†’ merge + deploy, no ask). Waves DEPLOYED: - **Wave 0 β€” NBA/WNBA GRADE (free ESPN gamelogs):** `espnStatsAdapter.getPlayerGameLog` feeds `featureCache` when the Python source is null β†’ l5/l20 β†’ props grade. **Verified live** (A'ja Wilsonβ†’B, Ionescuβ†’C off real ESPN form). Endpoint `site.web.api.espn.com/apis/common/v3/sports/basketball/{nba|wnba}/athletes/{id}/gamelog`. - **Resolver-hardening:** roster-index primary resolver (206 WNBA / 544 NBA) + `?season=` gamelog retry β€” the real fix for dual-league (WNBA+NCAA) players who got an empty gamelog (Collier/Brionna Jones now resolve). Broad coverage. - **Wave 1 β€” NBA/WNBA SETTLE:** `defaultGetPlayerStats` routes nba/wnba to the ESPN gamelog in BOTH `outcomeService` + `ledgerService`; separate `NBA_BOX_KEY`/`NBA_COMBO` settle maps; final-honesty guard (never settle in-progress); `opsWatch` finals-gated zero-settle alarm + per-sport boot announce. accuracy:{sport}+by_tier light up automatically. **PROOF is next-day** (grades lock tonight β†’ settle tomorrow β†’ `accuracy:wnba`). - **Wave 2 β€” NEVER-DARK HUB:** ESPN `/news` wire (`newsService` + `/api/news/:sport`) β€” **verified live (real headlines)**; `NewsWire`+`FuturesBoard` on `/explore` (offseason-aware, self-hide on empty, futures "TRACKED Β· NOT GRADED"). **KNOWN GAP: `/api/futures/:sport` returns `markets:[]` for ALL sports in prod** β€” code path looks correct (odds-api `/sports/{FUTURES_KEYS}/odds?markets=outrights`); needs a live odds-api probe (ODDS_API_KEY) to finish. Self-hides honestly meanwhile. `FUTURES_ENABLED=0` kill-switch; 12h TTL; 1 credit/refresh. - **Also rode along:** `espnAthleteIndex` cross-sport headshot capture (headshot-coverage) β€” NBA/WNBA/NFL/NHL real headshots when in-season. DEFERRED (locked decisions): **soccer** β€” grade+settle path, needs `API_FOOTBALL_KEY` (VALIDATED, Free 100/day, active to 2027 β€” set it in Coolify env `API_FOOTBALL_KEY`; code dormant until wired) + ESPN-soccer-free-first attempt; **MMA moneyline settle** (combat not yet in the grade/lock loop); **NFL/NHL** (dormant, in-season later); **futures debug** (odds-api probe). Test baseline now **263 suites / 3183**. ## SHIPPED β€” WIRING & DATA TRAIN (on main `3b1aa9f`, deployed 2026-07-13) Governed by `specs/wiring-data-train.md` + `specs/combat-intelligence.md` + the global visual reference `specs/design-reference/vyndr-system.html` (build toward it; live wordmark kept). **All 6 waves shipped + deployed** (fingerprint-verified: /parlay, /fight, /u/vyndr live; house record 88-38/70% real). Real assets verified live (MLB/ESPN headshot CDNs, ESPN-MMA feed, Baseball Savant CSV). - **Wave 1 β€” trust bugs:** billing renewal honest render (`billingDisplay.classifyRenewal` β€” no far-future placeholder); James Wood nameKey-collision fixed (`mlbStatsAdapter` teamHint disambiguation + streaks join-invariant); DeskShowcase "$1M terminal" β†’ deadpan copy. - **Wave 2 β€” sport-agnostic entity layer:** real player headshots threaded from ingestion (MLB MLBAM + NBA/WNBA ESPN athlete ids that were fetched-and-discarded) across slate/scan/hotlist/search/grade card; soccer = honest monogram (no free id); 8 self-authored SVG book wordmarks (`web/public/books/*.svg`, swappable for official art) + all 10 book keys resolve; team-logo abbr aliases. Storage: id on the `enriched` grade at `snapshotService` (zero new I/O). - **Wave 3 β€” record by grade tier (Addition 2):** ONE shared `TierRecord` (`lib/tierRecord.js` + component) on dashboard + /u + ledger; per-tier W-L always, hit-% only at nβ‰₯20 per tier (gate stays in `getModelAggregate`). - **Wave 4 β€” missing surfaces:** Outlook mode (grid never blank β†’ yesterday receipts / tomorrow schedule); Market-Breadth median-consensus-vs-model strip (self-hides <2 books); Parlay Lab `/parlay` (slate-independent leg source); live Grade-Shift timeline (`GradeShift`). - **Wave 5 β€” /u house-mode + arsenal:** house handle `vyndr` surfaces the real `user_id=NULL` public model record + per-tier calibration + 1080Γ—1350 portrait/OG (user-handle privacy 404s stay byte-identical); Baseball Savant pitcher-arsenal (`savantAdapter`, free CSV, verified) β†’ `PitcherArsenal` card, self-hides on absent. - **Wave 6 β€” combat v1 (MMA):** ESPN-MMA fight cards + tale-of-the-tape + style-blend archetypes (sport-scoped `COMBAT_ARCHETYPES` β€” FINISHER color collided w/ soccer + tripped the Ξ”E gate, so kept separate) + odds-api ML/round-totals + style-edge verdict. NOT in the snapshot/settle loop; method/round/props + fighter photos + matchup-GRADE engine + ufcstats scraping all DEFERRED, flagged data-limited in-UI. New routes: `/fight/[id]`, `/parlay`, `/u/[handle]/portrait`. POST-MERGE TODO: NBA/WNBA headshot coverage + combat depth need prod runtime verification; soccer headshots blocked on `API_FOOTBALL_KEY`; combat settlement + matchup-grade engine are the next combat sub-wave. ## SHIPPED β€” DESIGN TRAIN v2 (merged to main `e9c0a59`, deployed & fingerprint-verified live) Governed by `specs/DESIGN-SPEC.md` v2 (the raised standard: entities render as themselves, color contract, sub-200ms, screenshot-first billboards). **All six sessions DS0–DS5 shipped: merged to main, pushed to gitea, Coolify auto-deploy LANDED β€” verified live via `/pricing` DeskShowcase ("A $1M terminal") + `/u` route serving the unified EmptyState. 238 suites / 2885 tests green, `next build` exit 0.** Post-ship TODO: re-run the Chrome design audit against prod v2 to confirm all 22 audit findings closed (env here can't screenshot β€” WSL2↔Windows-Chrome localhost unreachable). - **DS0 Entity Layer** (`24af247`): `teamMeta.js` (real logos+colors for 30 MLB / 30 NBA / 13 WNBA / 48 WC nations via ESPN CDNs), `TeamLogo` / `PlayerAvatar` (team-colored monogram fallback, no gray silhouette) / `BookWordmark`, swapped into GameCard/StatStrip/StreaksPanel/TeamHub. - **DS1 Speed + Trust Bugs** (`1c681df`): React #418 hydration fix (mounted-flag guard), layout-matched `Skeleton`s replace text-wall loaders, scanβ†’ledger persistence via `session.access_token`. **DS1 follow-up** (`cf91c04`): closed the sb-token trust-bug CLASS β€” `lib/authToken.js` `currentAccessToken()` reads the REAL Supabase session (not the OAuth-only `sb-token` key) across profile/slip/dashboard/settings/tracker. - **DS3 Color Contract** (`49a3323`): `lib/colorContract.js` β€” green = one meaning (edge/A-tier/CTA), edge/CLV by sign, glow A/A+ only, archetype greens deduped. Enforced by `colorContract.test.js` (fails on violation). - **DS4 Billboards** (`45bafbc`): STREAKS row (length as mono hero), grade reveal (sign-colored edge), CLV reframe (flat says so), `/u` public profile + OG. - **DS2 Dashboard Slate Rebuild** (`fe294a5`): never-empty hero (falls back to yesterday's A-tier settled receipts), one bold hero per card, pending-filler collapse, ranked tonight-grades. `slateAdapter.js` engine + 21 tests. - **DS5 Pricing + Motion + States** (`a18a3f3`): Desk $44.99 as the hero tier (single green CTA, `DeskShowcase`), ticker β†’ punctuated stillness (β‰₯4.2s hold, one idle motion), unified `EmptyState` (404 grammar) at TeamHub/game/ledger, archetype glyphs propagated to streaks/ledger. NOTE: WSL2↔Windows-Chrome localhost is unreachable in this env, so visual proof was SSR-render + source-assertion tests + live prod fingerprint curls, not pixel screenshots. The `design/v2-train` + per-DS worktree branches can be pruned. Stale `origin` GitHub remote still carries the long-dead leaked PAT β€” deploy runs off `gitea`, so it's inert, but rotate it when convenient. ## WHERE THINGS STAND **Tests: 2757 / 229 suites, all green. Web build exit 0.** Baseline at the A1 board's start was 2398. Governing docs: `specs/VYNDR-NORTH-STAR.md` (v1.1) + `specs/VOICE.md` (v1.1) + `specs/ROW-GRAMMAR.md` β€” read them before building anything. **The record is live and writing itself.** Day one (Jul 11): 24β†’25 MLB rows, real locks (line/odds/book), closing capture every snapshot + every 20-min intraday refresh, settlement each morning slot with signed CLV. No percentage renders anywhere under nβ‰₯20 β€” enforced in `ledgerService.getModelAggregate`, the single gate. WNBA/soccer rows pend honestly until Phase 4.5. ## THE THREE A1 TRAINS (all merged to main, all pushed) | Train | Range | Carried | |---|---|---| | #1 | `4d2b27d β†’ 219167e` | S0 verify Β· S1 promise audit (`PROMISE-AUDIT.md`: alt ladder + quarter-Kelly BUILT, analyst unlimited, "40+" claims made honest) Β· S2 compliance (/responsible-gambling rebuilt, /terms /privacy drafts, /methodology, 5 seed articles in `content/articles/`) Β· S3 affiliate (BOOK IT deep links organic-until-config-flip, best-price dots, ?ref= partner attribution) Β· S4 media engine (`mediaEngine` + executable VOICE lint, `/desk` arsenal + DATA BRIEF, Ghost DRAFTS-only publisher) Β· S5 viability (statsapi lineups β†’ CONFIRMED/NOT-IN kills grades visibly; ESPN injury wire; Yesterday/Today/Tomorrow nav + Settle panel) Β· S7 newsletter (Listmonk capture + THE VYNDR REPORT assembly, env-gated) Β· S8 ops (settle alarm, 3-slot failure pager, quota/disk/mem, 9AM pulse β€” test alert delivered to the real ntfy channel) Β· S9 slip reader (tesseract.js OCR + per-book parsers, /slip page; synthetic-image acceptance passed) Β· S10 public profiles (/u/{handle}, private-by-default, no-existence-leak) Β· migrations 021/022 | | #2 | `219167e β†’ 14dc9cf` | S6 display (ROW-GRAMMAR.md + slot-order fixes, line sparklines from intraday history, last-10 ●●○ dots, CLV distribution on MODEL tab, ⌘K SearchModal + mobile search, landing LCP fixes) | | #3 | `14dc9cf β†’ ec5ee3d` | S11 live tracking (statsapi/ESPN live box lines β†’ ON PACE / NEEDS N / HIT βœ“ in the outcome slot, live games float up, "TRACKING β€” READ LOCKED PRE-GAME"; real-live acceptance: Harper 3/1.5 TB β–Ό8th β†’ HIT βœ“). Grades never change in-game. | **Deploy status:** ALL LIVE as of `17fb981` (Jul 12 17:43 UTC). Fingerprints confirmed in prod: `/api/live/mlb` serves JSON with `hasLive:true` (S11 live tracking working β€” a real game was tracked at confirm time), `/api/health` healthy, `/api/snapshot/summary` β†’ `expected_interval_s:1200` (env var now set). **P0 postmortem (`17fb981`):** the API silently served a 14h-old image (`4d2b27d`) from Jul 11 ~20:00 to Jul 12 17:43. Root cause: the Dockerfile never copied `content/`, and `mediaEngine.js` read `content/stark-lines.json` with an unguarded module-load `readFileSync` β†’ ENOENT at require time crashed `app.js`'s eager route chain (app β†’ routes/desk β†’ deskService β†’ mediaEngine) β†’ boot failed β†’ Coolify healthcheck rolled back. Every merge from S6 onward was merged-but-not-running for 14h. Fixed: Dockerfile COPYs content/; mediaEngine treats the file as optional garnish (loadStark try/catch β†’ {}); `src/preflight.js` now prints `[preflight] OK|DEGRADED` at boot so a missing file/env is legible instead of masked by a rollback. LESSON: any module-load file read is a boot-crash risk β€” keep them lazy + try/catch, and the Dockerfile must copy every dir the runtime reads. ## LIVE INFRA MAP (verified in prod) - **VYNDR Web** β€” Next.js :3000, vyndr.app. **VYNDR API** β€” Express :3001, api.vyndr.app (`/api/internal/*` behind `VYNDR_INTERNAL_KEY`). - **Scheduler (in-process, API app, `SNAPSHOT_CRON=1`)** β€” snapshot slots 14,19,22,1,3 UTC; settle pass (outcomes + ledger, idempotent) runs FIRST each slot; 20-min intraday odds refresh during slate hours (`INTRADAY_REFRESH=0` kills); desk-ready ping after the day's first slot; daily pulse 13:00 UTC; missed-cron + 3-slot-failure + quota pagers β†’ ntfy `vyndr-pipeline-kev2026`. Boot logs: `[snapshotScheduler] armed` + `[settlement] armed`. - **Data**: Redis (mastermind-cache) for all pipeline caches (SNAP_TTL 24h β€” do NOT shorten; the 11h overnight gap killed morning settles once). **Supabase = the ledger** (`ledger_entries` + `public_profiles` + `user_profiles.partner_ref`; RLS everywhere, service-role-only writes; migrations 001–022 applied, files in `supabase/migrations/`). - **Deploys**: gitea `git.builtbykev.com/builtbykev/vyndr` β†’ Coolify webhook (HMAC secret fixed Jul 11; auto-deploy proven). GitHub `origin` remote is dead-PAT hygiene debt only. - **Free feeds** (zero out-of-pocket law): PropLine Γ—3 keys (9k/day capacity; intraday uses ≀144/day), odds-api backup (quota-gated), statsapi.mlb.com, ESPN site API. tesseract.js OCR self-hosted (traineddata caches on first call β€” needs outbound network once). ## ENV VARS STILL PENDING ON KEV'S SIDE (Coolify, API app) 1. `SNAPSHOT_EXPECTED_INTERVAL=1200` β€” **confirmed absent** (fresh restart still served the 18000 default). SYNC badge tracks intraday cadence once set. 2. `DESK_OWNERS=kevdevelops@gmail.com` β€” /desk 403s everyone until set. 3. `LISTMONK_URL` / `LISTMONK_USER` / `LISTMONK_TOKEN` / `LISTMONK_LIST_ID` (all four together) β€” newsletter no-ops politely without them; box-side setup in `docs/NEWSLETTER.md`. 4. Optional: `GHOST_URL` + `GHOST_ADMIN_API_KEY` (drafts), `PULSE_HOUR_UTC`. **Entity placeholders** (S2, blocked on Kev's values): `[ENTITY NAME]`, `[STATE OF FORMATION]`, `[ARBITRATION VENUE]`, `[CONTACT EMAIL]` in `web/src/app/terms/page.tsx` (6Γ—) + `web/src/app/privacy/page.tsx` (5Γ—). A test keeps unverified entity names out until replaced. ## HONEST OPEN ITEMS - **Phase 4.5 β€” WNBA settlement via ESPN box scores. DUE ~Jul 24** (hard date). Until then WNBA/soccer/NBA ledger rows pend forever; accuracy = MLB only. - **The record needs time**: no percentage anywhere before nβ‰₯20 settles, by design. First percentages expected ~day 3–4 of MLB volume. - **Slip reader**: acceptance was a clean synthetic image; needs one real phone screenshot (DK) to validate OCR robustness. - **Landing LCP**: S6's fixes are static-analysis-provable but unmeasured β€” run PageSpeed mobile vs the 3.8s baseline; next suspect is the LiveHeroProp post-fetch swap. - **Partner report endpoint** returns real numbers only for signups AFTER migration 021 (metadata path live; OAuth signups carry no ref β€” known gap in `docs/PARTNERS.md`). - **NBA Oct / NFL Sep readiness** + soccer end-to-end: `specs/vyndr-roadmap.md`. - **Killed permanently** (founder ruling): AI chat assistant, light mode. ## OPERATING LAWS (never relax) Zero out-of-pocket Β· absent beats wrong, refusal beats hollow Β· nβ‰₯20 before any percentage Β· nothing auto-posts anywhere, ever Β· grades never change in-game (revisions are public via `revised_from_grade`) Β· one meaning per color, data in mono Β· no exclamation points, anywhere Β· the pipeline is the only source of numbers, including in marketing (`mediaEngine` lint enforces). β€” STATE as of ec5ee3d Β· the record is writing β€”