Skip to main content

/ METHODOLOGY

How a pick becomes a pick.

We get asked "what's the AI" a lot. Here's the full six-stage pipeline, with no hand-waving. If we don't explain the math, you shouldn't trust the picks.

Launch status (2026-04-22): Stages 1, 2, 4, and 5 shipped. Stage 4 (WHY-driver) wired end-to-end across Discord embed, /record web panel, and per-pick permalinks. Stage 5 (CLV) populating picks.closing_odds via a live capture service. Stage 3 (production XGBoost retrain) and Stage 6 (retrain loop) remain in progress; the pipeline stub emits the canonical WHY-driver schema so real model output will drop in without any surface changes.

Data intake

STAGE 1

Live odds from The Odds API across multiple retail + sharp books for the sports currently in coverage, with line movement history timestamped to the minute. Book count varies by sport and market: game lines line-shop across the full allowlist, while player props are limited to the books that publish them.

Not in coverage: Football (NFL / NCAAF) is not in coverage. There is no football injury feed, no weather feed and no multi-season football archive in production.

  • Multi-book game-line scanner for in-coverage sports
  • Player-prop coverage varies by book and market
  • Line movement history timestamped to the minute

Feature engineering

STAGE 2

Raw data becomes a per-game feature set for each sport in coverage. Pace-adjusted efficiency, rest differential, home/road splits, usage rates, recent form weighted exponentially by recency, schedule density, opponent-adjusted metrics. Feature set is versioned and reproducible.

Not in coverage: No NFL or NCAAF feature set is built in production. Football was previously listed here and should not have been.

  • Per-game feature sets for the sports in coverage
  • Pace-adjusted (not raw) efficiency numbers per team
  • Recency-weighted recent form (last 5 games > last 10 games > last 20)
  • Opponent-adjusted variance, not just averages

XGBoost ensemble scoring

STAGE 3

Ensemble of gradient-boosted decision trees per sport, trained on historical data with walk-forward cross-validation (never train on future, never leak results). Model outputs a win probability and confidence tier for each market per game. Applies only to sports with a promoted model.

Not in coverage: No NFL or NCAAF model has been trained, backtested, calibrated, shadowed or promoted. Football picks are not published.

  • Per-sport ensemble (not one-size-fits-all across all leagues)
  • Walk-forward CV prevents data leakage
  • Out-of-sample test on last season held out at training
  • Only sports with a promoted, versioned model are published

WHY-driver rationale

SHIPPED 2026-04-22STAGE 4

Where a promoted model produces a prediction, that prediction is decomposed into per-feature WHY-driver values. Canonical schema stored in picks.shap_top JSONB: { base_prob, model_prob, features[] }. One explainer module renders the Discord embed, the /record Recent Edges panel, and the per-pick /record/[id] permalink page, no drift between surfaces. Falls back to legacy why_* prose when shap_top is null so historical picks stay readable.

Not in coverage: The canonical schema and the shared explainer are shipped. Model attribution is only claimed for a pick produced by a promoted, versioned model; a WHY-driver-shaped payload alone is not attribution, and no football model is promoted.

  • Shipped: canonical picks.shap_top schema + shared explainer (bot + web)
  • Shipped: top drivers in signed basis points, color-coded by sign
  • Shipped: Recent Edges panel on /record with tooltip details per driver
  • Shipped: /record/[id] permalink page + per-pick OG card + /api/record/[id] JSON

Closing Line Value tracker

SHIPPED 2026-04-22STAGE 5

Every posted pick gets its closing line captured 2 min after kickoff by an in-process tick that reads the odds-feed cache. CLV is then computed as (decimal-at-post / decimal-at-close - 1) and aggregated on /record across 7d / 30d / 90d / all-time windows. Migration 0020 added event_id + commence_time + sport_key to picks so rows can be matched against the odds feed by identity, not fuzzy game-name parsing.

  • Shipped: migration 0020 adds linkage columns + partial capture-pending index
  • Shipped: src/bot/services/closing-line-capture.js hourly (2-min) tick
  • Shipped: aggregated CLV % on /record + per-pick CLV on /record/[id]
  • Shipped: weekly_snapshots cron freezes CLV alongside ROI/yield/hit-rate

Daily retrain feedback loop

STAGE 6

Settled results feed into the training set. Model retrains on a rolling window (weekly full retrain, daily incremental for fresh data) so tomorrow's predictions are informed by yesterday's results. Over 90 days the model measurably sharpens against the closing line.

  • Weekly full retrain with updated feature importances
  • Daily incremental fine-tune with the most recent game nights
  • A/B shadow models evaluated before any live swap
  • Rollback automatic if live CLV drops 0.5+ points vs prior version over 50 picks

/ FOUR TRUST SURFACES

A methodology page can claim anything. The artifacts below are the parts a reader can independently audit. Each surface answers a different question, and all four must agree for our claim of edge to hold.

Daily P&L heatmap
Are we publishing every day?

Each cell is one ET day, color-coded by net units. Empty days are gray (no slate or no edge). A capper who only posts on the days they win has a heatmap full of gray cells; that gap is the give.

Where: /record · /record/all-time · /leaderboard · /c/[handle]
CLV trend
Are we beating the closing number?

Every pick captures the line at post time and at close. Posted decimal / closing decimal - 1 = CLV. The 14-day trailing mean smooths daily variance and shows whether the model + timing edge is holding. The metric Pikkit charges $299/mo to surface; ours is free.

Where: /record · /record/all-time · /api/record/clv
Odds distribution
Are we cherry-picking favorites?

Every settled pick bucketed by the odds we shipped, with W-L and net units per tier. A 60% hit rate from -300 favorites and a 60% hit rate spread across +100..+200 dogs are not the same product. The split is the receipt.

Where: /record · /record/all-time · /api/record/odds-distribution
Hash chain + immutability
Did the pick exist before the result?

Every row has a SHA-256 pre-commit hash computed at post time and an append-only Postgres trigger that blocks updates and deletes. Changing a posted pick after the result requires breaking the hash chain. We ship the head hash on every embed; you can paste any hash on /verify to resolve it back to its row.

Where: /record · /verify · per-pick OG card

/ RECEIPTS: the methodology isn't marketing, it's verifiable

Every claim above is backed by a public surface you can inspect without an account. These are the artifacts:

picks table locked on insert via picks_immutable_guard trigger · weekly_snapshots append-only via weekly_snapshots_immutable_guard · closing line captured via src/bot/services/closing-line-capture.js