Changelog

What we shipped, when.

Every public change to Line Gap. We update this on every deploy that touches the product. Want the deep-dive? See the Research posts.

  1. fix

    Fix: duplicate Kalshi rows and inflated alt-line EV

    Two issues surfaced once Kalshi started flowing into the main OddsTable. Both shipped within the same day; the v1.2.0 launch entry is unchanged.

    Stale snapshots stacking as duplicates

    All three poll paths (retail / Kalshi / Pinnacle) inserted a new sportsbook_odds row every time a price changed but never marked the previous snapshot is_active = false. The dedup-on-insert guard skipped same-price re-INSERTs but did nothing about price moves. Over a day of polling, the same (player, prop, line, sportsbook) accumulated 4–6 active rows — the Deandre Ayton 19.5-points line was the visible symptom (six near-identical rows ranking high on EV).

    Affected counts at the time of fix:

    • 1,388 stale active Kalshi rows
    • 1,233 stale active retail rows (from the fetch-data path)
    • 390 stale active Pinnacle rows

    The fix:

    • One-time SQL cleanup deactivated all 3,011 stale rows.
    • New deactivate_superseded_odds() Postgres function keeps only the latest is_active = true row per dedup group; idempotent and scoped by sportsbook_key.
    • Every poll route (poll-odds, pollKalshiNba, pollKalshiNbaGames, pollPinnacleNba) now calls the function via .rpc() after its batch insert.

    Alt-line junk EVs

    Kalshi exposes the full strike ladder for every player prop (e.g. Ayton points at 5.5 / 9.5 / 14.5 / 19.5 / 24.5). Our projection model is well-calibrated near a player's seasonal mean; at tail strikes the StatProb stays in the 10–15% range even when the market's no-vig fair prob has collapsed to 2–4%. The math then displays "+1500% EV" — technically correct, structurally nonsense.

    The OddsTable now drops any row whose projection-implied probability diverges from the market-implied probability by more than 25 percentage points. Retail books don't expose alt ladders, so in practice this only filters Kalshi alt-line rows; retail and Pinnacle main-line rows are unaffected.

  2. v1.2.0featureimprovementinfrastructure

    Kalshi prediction-market lines, end-to-end

    The biggest release since we shipped CLV tracking. Kalshi's prediction-market prices are now a first-class part of the Line Gap surface — visible alongside DraftKings, FanDuel, BetMGM and the rest, with their no-vig math made explicit.

    Game-level Kalshi NBA markets

    • New polling cron at :04 past the hour pulls Kalshi's KXNBAGAME (moneyline), KXNBASPREAD, and KXNBATOTAL markets every 5 minutes.
    • Markets land in sportsbook_odds as Yes-canonical rows (player_id NULL, under_odds NULL) so they sit beside retail rows without duplicating storage.
    • A closest-to-retail strike selector picks one row per (game, market_type, side) so the comparison surface stays clean even when Kalshi exposes 10+ alternate strikes per game.
    • A both-sides-quoted gate (D-06) rejects phantom one-sided quotes before they pollute the table.

    Kalshi in the main book list

    • Open any NBA player prop on /odds and Kalshi appears as a regular book row alongside DraftKings, FanDuel and the rest — with a small PM badge to mark its no-vig pricing.
    • Kalshi's filter chip is now in the Sportsbooks dropdown.
    • The per-prop expansion table ranks Kalshi by EV against retail consensus, so when Kalshi prices a prop dramatically differently from the books, you see the divergence inline.

    Best Bets gets a "Kalshi edge" hint

    • Best Bets still anchors on the highest-EV retail line — Kalshi's unvigged prices would otherwise hijack every callout — but when Kalshi's fair probability diverges from retail consensus by ≥2 percentage points, the Best Bet callout now shows whether Kalshi agrees or disagrees with the retail pick, and by how much.
    • This is the "sharp money sees something the books don't" signal you used to have to compute yourself.

    Daily Games card expand

    • Click "Show prediction-market lines" on any NBA game card on the dashboard and you'll see Kalshi's moneyline / spread / total for that game, snapshot-timestamped to the latest poll.
    • Honest scope: retail moneyline/spread/total ingest isn't built yet, so this is currently a Kalshi-only view — full multi-book game-line comparison ships in a future phase.

    Projection enrichment for Kalshi rows

    • The hourly enrich-odds cron now stamps StatProb, Confidence, and EV onto Kalshi player-prop rows using the same projection math retail rows get. 77% of active Kalshi rows now carry full projection columns; the rest have no projection coverage (retired players, future games not yet projected).

    Schema & security

    • Forward-only migration widens the sportsbook_odds CHECK constraints to cover the new game-line market_type and prop_type values across NBA + NFL + NHL.
    • sportsbook_odds.player_id is now nullable (Phase 7 game-line rows persist with player_id NULL per the Yes-canonical convention).
    • Resolved every active finding in the Supabase Security Advisor: RLS enabled on daily_odds_summary, mutable search_path pinned on three SQL functions, and the line_movement_summary matview removed from the anonymous PostgREST surface.

    EV calculation hardened on the alt-strike ladder

    • Kalshi exposes every strike (e.g. 5.5 / 9.5 / 14.5 / 19.5 / 24.5 points for the same player), and retail books only post lines near a player's seasonal mean. That asymmetry surfaced a long-standing tail-bias in the "simple" projection model — at extreme strikes it would say "35% over 19.5" for a 10-ppg guard while the market priced the same event at 2%, producing +1690% EV displays that looked like impossible edges.
    • StatProb + EV now read from the full projection (game-context-aware variance), which is well-calibrated on tails. Retail rows are unchanged in the meat of the distribution; Kalshi extreme-strike rows now show realistic single-digit probabilities and small EVs.
    • Existing enriched rows in sportsbook_odds were updated in-place; future rows enrich correctly via the cron.

    Under the hood

    • 17 commits, 305 tests passing, no breaking changes to any retail-only surface (Best Bets, cross-book detection, sharp signals).
    • Documented two open follow-ups: pair spread_home / spread_away to a single strike, and backfill retail game-line ingest so the DailyGames expand can become a true multi-book comparison.