Files
vyndr/specs/STATE.md
T

47 KiB
Raw Blame History

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 — NOT WORKING. Deferred. (real state)

On-box is durable and proven. Off-box has never succeeded once. Every dump currently lives only on the Hetzner persistent volume — a single box-side loss takes the backups with it.

Fixed tonight (both real blockers, both now gone):

  1. BACKUP_SSH_KEY was not base64-decoded → now auto-detects base64 vs raw PEM.
  2. Container had rsync but no ssh binaryopenssh-client added to the Dockerfile. (Failed to exec ssh is gone.)

Remaining blocker — SSH auth is rejected at the Storage Box. rsync exits 255. Verified from the dev box with the private key directly:

debug1: Offering public key: ... SHA256:ZO3j7en9J7wN7EKcbYHfpyNcxTPw266hJxECUWXoMTo
debug1: Authentications that can continue:      <-- EMPTY
Permission denied ().

The server offers NO auth methods at all. That is not a wrong-key signature — it is the account refusing all authentication. Port 23 answers with an OpenSSH banner regardless, which is why SSH can look enabled when it isn't.

UNVERIFIED — I did NOT run a Chrome/UI diagnostic and have no data on: the Hetzner "SSH support" toggle state, an "External Reachability" setting, whether the key was added at project level vs the box itself, or any Hetzner incident. Those are the untested hypotheses, listed in likelihood order:

  1. SSH support disabled on the Storage Box (separate checkbox from adding a key).
  2. Key added to the wrong scope (project/sub-account vs this box).
  3. External reachability disabled for the box.
  4. Hetzner-side outage — no evidence either way; not checked.

To close: confirm the panel shows fingerprint SHA256:ZO3j7en9J7wN7EKcbYHfpyNcxTPw266hJxECUWXoMTo, enable SSH, then POST /api/internal/backup/run and look for off-box push OK. The full scratch-Postgres restore proof is still OWED and is blocked on this (needs the dump off the container; docker is available locally to do it immediately after).

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 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 gradesgameLogService 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 snapshotopp_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 16, 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 <api> 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 |modelconsensus| 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 DS0DS5 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 Skeletons 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/summaryexpected_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 001022 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=1200confirmed 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 34 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 —