{"as_of_iso":"2026-05-20T21:38:12Z","aggregate":{"books_total":17,"books_active":15,"by_ingest_path":{"residential_mesh":7,"mirror_broker_ws":1,"graphql_polling":1,"passthrough":1,"kambi_polling":1,"browser_session":1,"websocket_subscribe":1,"direct_mobile_api":1,"cloud_egress":1},"books_per_region":{"US-NJ":7,"US-CO":6,"US-VA":6,"US-PA":5,"US-NY":4,"US-MI":4,"US-IL":4,"US-IN":4,"US-WV":4,"US-TN":4,"US-IA":4,"US-AZ":4}},"books":[{"key":"bovada","title":"Bovada","regions":["US-OFFSHORE"],"bot_protection":"none observable on lobby endpoints","requires_browser_session":false,"ingest_path":"residential_mesh","typical_refresh_seconds":6,"coverage_markets":["h2h","spreads","totals","player_props","team_props","futures","live_in_play"],"known_limitations":["Geo-blocks several US states at the network layer; residential mesh routes around this transparently","Lobby JSON is large (500KB-1MB per sport); upload pipeline needs >1MB nginx body cap (see runbook 2026-05-19-gcp-nginx-body-limit)"],"last_audited":"2026-05-19"},{"key":"draftkings","title":"DraftKings","regions":["US-NJ","US-NY","US-PA","US-MA","US-MI","US-IL","US-CO","US-IN","US-VA","US-WV","US-TN","US-IA","US-AZ","US-CT","US-LA","US-NC","US-MD","US-KS","US-OH","US-ME","US-VT","US-KY"],"bot_protection":"Cloudflare on legacy /sportsbook.draftkings.com/sites/* paths; mostly open on /sportsbook-nash/api/sportscontent/*","requires_browser_session":false,"ingest_path":"residential_mesh","typical_refresh_seconds":60,"coverage_markets":["h2h","spreads","totals","player_props_main_line","milestone_ladders","futures"],"known_limitations":["Alt-line UNDER prices are exposed in DK's UI but not in any public Nash API path; we surface alt-line OVER plus the main O/U and document the gap rather than synthesize","Datacenter IPs consistently 403 from the eventgroups endpoint; residential IPs clear cleanly; the mesh routes DK URLs to the residential nodes"],"last_audited":"2026-05-19"},{"key":"fanduel","title":"FanDuel","regions":["US-NJ","US-NY","US-PA","US-MI","US-IL","US-CO","US-IN","US-VA","US-WV","US-TN","US-IA","US-AZ","US-CT","US-LA","US-NC","US-MD","US-KS","US-OH","US-KY","US-VT","US-ME"],"bot_protection":"Akamai Bot Manager (cookie + TLS fingerprinting)","requires_browser_session":false,"ingest_path":"residential_mesh (tls_impersonation)","typical_refresh_seconds":20,"coverage_markets":["h2h","spreads","totals","player_props","live_in_play"],"known_limitations":["FanDuel only prices player props in a ~90-minute window before first pitch / tipoff and pulls them at game start (live-only markets replace them). Outside that window the prop board exists but the prices are blank. This is FanDuel book behavior, not a coverage gap on our side; documented at fd_pricing_window.py","Per-event /api/event-page endpoint returns full 50-100-market prop board when in-window; /api/content-managed-page is the low-rate fallback for game lines"],"last_audited":"2026-05-19"},{"key":"caesars","title":"Caesars","regions":["US-NJ","US-PA","US-NY","US-MI","US-IL","US-CO","US-IN","US-VA","US-WV","US-TN","US-IA","US-AZ","US-CT","US-LA","US-NC","US-MD","US-OH","US-KY"],"bot_protection":"AWS WAF with X-Aws-Waf-Token challenge on event-deep paths","requires_browser_session":true,"ingest_path":"residential_mesh + browser_session","typical_refresh_seconds":30,"coverage_markets":["h2h","spreads","totals"],"known_limitations":["/home endpoint serves 47 events x 125 markets across 8 sports as h2h / spreads / totals without auth, but /events/{id}, /markets, /competitions all return 202 challenges. Player props live behind the challenge; reaching them requires a Playwright session that defeats the WAF","WAF rotated 2026-04-26: requests now 200 WITHOUT the WAF token; previous token-bearing fetches 403"],"last_audited":"2026-04-26"},{"key":"betmgm","title":"BetMGM","regions":["US-NJ","US-PA","US-NY","US-MI","US-IL","US-CO","US-IN","US-VA","US-WV","US-TN","US-IA","US-AZ","US-LA","US-MD"],"bot_protection":"Imperva (formerly Distil) on api endpoints","requires_browser_session":false,"ingest_path":"residential_mesh","typical_refresh_seconds":45,"coverage_markets":["h2h","spreads","totals","player_props"],"known_limitations":["Per-state subdomain (e.g. nj.betmgm.com, pa.betmgm.com) gates coverage; we route US-NJ as the canonical pull which covers the largest market set"],"last_audited":"2026-05-12"},{"key":"pinnacle","title":"Pinnacle","regions":["GLOBAL"],"bot_protection":"Akamai on www; guest API key required on guest.api.arcadia.pinnacle.com","requires_browser_session":false,"ingest_path":"mirror_broker_ws + guest_api_polling (fallback)","typical_refresh_seconds":1,"coverage_markets":["h2h","spreads","totals","player_props","team_props","futures"],"known_limitations":["Two ingest paths in parallel. Mirror broker (sub-second WS) is the primary path during peak hours and runs against operator white-label feeds that carry the same Pinnacle pricing engine; observed customer-side latency is typically sub-second when the broker is healthy. Guest API polling (~30s) is the fallback that keeps coverage warm if the mirror broker pauses","Guest API does not surface table tennis (sport_id=32 always shows matchupCount=0 even when matches are live on the site); do not waste cycles on TT discovery against Pinnacle","Player Props category covers true player props; Team Props category covers soccer game derivatives (Correct Score, Double Chance, BTTS, Winning Margin) and uses the same player_ prefix which must be stripped on output","MLB descriptions are 'Player (Total Stat)(must start)' not 'Player Total Stat'; the regex parser needs both forms"],"last_audited":"2026-05-19"},{"key":"novig","title":"Novig","regions":["US-OFFSHORE"],"bot_protection":"none observable","requires_browser_session":false,"ingest_path":"graphql_polling","typical_refresh_seconds":12,"coverage_markets":["h2h","spreads","totals","player_props","yes_no_props"],"known_limitations":["Yes/No markets (anytime HR, RBI, stolen base, first hit) ship outcomes in [No, Yes] order; we assign over/under by the outcome 'type' field, not position, so swapped-order responses still classify correctly"],"last_audited":"2026-05-15"},{"key":"prizepicks","title":"PrizePicks","regions":["US-MOST-STATES"],"bot_protection":"Cloudflare with bot challenge on /projections","requires_browser_session":false,"ingest_path":"residential_mesh (tls_impersonation)","typical_refresh_seconds":30,"coverage_markets":["player_props_pick_em"],"known_limitations":["DFS pick'em model not a true sportsbook; prices are pick lines, not American odds. We normalize as O/U markets with line + fixed effective payout multipliers"],"last_audited":"2026-05-17"},{"key":"bet365","title":"bet365","regions":["GB","EU","AU","US-NJ","US-CO","US-VA","US-OH"],"bot_protection":"DataDome + own anti-bot","requires_browser_session":false,"ingest_path":"passthrough + browser observation","typical_refresh_seconds":60,"coverage_markets":["h2h","spreads","totals","player_props","live_pbp"],"known_limitations":["Routed via Action Network's passthrough rather than direct, because direct bet365 endpoints require browser fingerprint + session cookies that fail in our ingest pipeline","Combo markets 85-88 (PRA, PR, PA, RA) need explicit mapping; fallback emits long-form keys that miss TOA canonical (player_pts_rebs_asts, player_pts_rebs, etc.)"],"last_audited":"2026-05-14"},{"key":"unibet_us","title":"Unibet (US)","regions":["US-NJ","US-PA"],"bot_protection":"Kambi platform protections","requires_browser_session":false,"ingest_path":"kambi_polling","typical_refresh_seconds":25,"coverage_markets":["h2h","spreads","totals","player_props"],"known_limitations":["ZEbet brand merged into Unibet by Kindred in 2026; ZEbet queries 301 to Unibet. Same Kambi engine, do not build ZEbet as a separate source"],"last_audited":"2026-05-04"},{"key":"winamax","title":"Winamax","regions":["FR"],"bot_protection":"CloudFront WAF with reputation tracking on deep paths","requires_browser_session":true,"ingest_path":"browser_session (eu)","typical_refresh_seconds":900,"coverage_markets":["h2h","spreads","totals"],"known_limitations":["JWT-authenticated socket.io feed at sports-eu-west-3.winamax.fr uses opaque .emit('m', r) messages with minified main bundle; reverse-engineering cost too high for the value. Working path is Playwright DOM observation, not the WS","curl probes burn CloudFront reputation and trigger blanket 403s on /paris-sportifs/sports/* within minutes; only the Playwright path survives long-running","AWS t3.micro (908MB RAM) cannot keep Chrome resident; runs as Type=oneshot systemd timer every 15 min so Chrome fully reaps between cycles"],"last_audited":"2026-05-04"},{"key":"sporttrade","title":"Sporttrade","regions":["US-NJ","US-CO","US-VA"],"bot_protection":"AWS WAF light","requires_browser_session":false,"ingest_path":"websocket_subscribe","typical_refresh_seconds":2,"coverage_markets":["h2h","spreads","totals"],"known_limitations":["Exchange model (P2P matching) rather than book; prices reflect user-posted limit orders. Useful as a sharp signal but not always tradeable size"],"last_audited":"2026-05-10"},{"key":"bwin","title":"Bwin","regions":["EU","DE","AT"],"bot_protection":"Akamai","requires_browser_session":false,"ingest_path":"residential_mesh (eu)","typical_refresh_seconds":60,"coverage_markets":["h2h","spreads","totals","table_tennis_main"],"known_limitations":["Table tennis lives under sportId=56; competition-level queries needed (sportIds=56 returns empty markets, competitionIds={id} returns full markets inline)","Active TT competition IDs as of 2026-05-03: Czech Liga Pro 87135, TT Elite Series Men 87160 + 87161, TT Cup Men's Singles 74537, TT Cup Women's Singles 74538"],"last_audited":"2026-05-03"},{"key":"pick6","title":"DraftKings Pick6","regions":["US-MULTI-STATE"],"bot_protection":"DraftKings mobile API edge","requires_browser_session":false,"ingest_path":"direct_mobile_api + mobile_app_capture_fallback","typical_refresh_seconds":60,"coverage_markets":["player_props_pick_em"],"known_limitations":["Pick6 is a payout-board DFS source, not a sportsbook price source. Rows carry player, market, and line; American prices are null unless the app exposes a priced outcome field","Primary path polls the public Android app board endpoint directly. Pixel app capture and the older Chrome CDP tab path are retained as fallbacks"],"last_audited":"2026-05-20"},{"key":"kalshi","title":"Kalshi","regions":["US-CFTC-DCM"],"bot_protection":"none (CFTC-regulated DCM public API; no auth on catalog reads)","requires_browser_session":false,"ingest_path":"cloud_egress","typical_refresh_seconds":300,"coverage_markets":["h2h_binary_outcome","spreads_ladder","totals_ladder","futures_binary"],"known_limitations":["Kalshi is a prediction-market exchange, not a sportsbook. Each event has 2-3 child YES/NO contract markets for h2h and 10-30 ladder rungs for spreads/totals; we group by event_ticker and emit one row per outcome (h2h) or per rung (spreads/totals) with American odds derived from yes_ask_dollars (1/p decimal formula)","h2h series in rotation: NBA, NFL, MLB, NHL, WNBA, MLS, EPL, UFC, ATP, WTA. Spread + total ladders in rotation: NBA, NFL, MLB, NHL, WNBA. Other series (NCAAB, NCAAF, soccer Big-5 ex EPL, props) are classified but not yet in the GH Actions rotation","Series tickers map to (sport_key, market_kind) via a lookup table in _parse_kalshi; unrecognized series tag the row with sport_key=null but still ingest for cross-book comparison"],"last_audited":"2026-05-19"},{"key":"thescore_bet","title":"theScore Bet (formerly ESPN BET)","regions":["US-NJ","US-NY","US-PA","US-MI","US-VA","US-IN","US-TN","US-IA","US-CT","US-LA","US-MD","US-KS","US-NC","US-OH","US-KY","US-CO","US-IL","US-WV","US-VT","US-AZ","US-ME"],"bot_protection":"Kambi platform (Akamai + Kambi client fingerprinting)","requires_browser_session":false,"ingest_path":"not_currently_integrated","typical_refresh_seconds":null,"coverage_markets":["h2h","spreads","totals","player_props","live_in_play"],"known_limitations":["ESPN BET became theScore Bet on 2025-12-01 when the PENN x ESPN deal terminated. Existing customers' bets and balances migrated; the brand and front-end changed; the underlying Kambi platform did not. We classified the rebrand on 2026-05-19 and reserved both 'thescore_bet' and 'espnbet' keys in BOOKMAKER_TITLES so any historical rows tagged 'espnbet' still resolve cleanly. Live ingest via the Kambi public catalog at kambicdn.akamaized.net is on the operator roadmap but not currently driving","Pre-rebrand ESPN BET rows in prop_snapshots remain tagged 'espnbet' and are NOT retroactively rewritten; the BOOKMAKER_TITLES alias surfaces both as 'theScore Bet'"],"last_audited":"2026-05-19"},{"key":"fdj_parionssport","title":"FDJ ParionsSport","regions":["FR"],"bot_protection":"n/a","requires_browser_session":false,"ingest_path":"decommissioned","typical_refresh_seconds":null,"coverage_markets":[],"known_limitations":["Online site decommissioned in 2026. parionssport.fdj.fr/paris-sportifs returns 410 Gone redirecting to a B2B retail terminal page; api.parionssport.fdj.fr DNS no longer resolves. Brand survives only at retail"],"last_audited":"2026-05-04"}],"_note":"Per-book metadata describing HOW we ingest each upstream source: bot-protection vendor, our routing strategy, observed refresh cadence, market coverage, and any genuine limitations. Every entry reflects observed behavior; we do not invent vendor names or synthesize coverage. Entries with ingest_path 'decommissioned' or 'not_currently_integrated' are documented for completeness."}