← All findingssource · memory/reference_odds_book_quality.md
Audited every book on a 454-prop WNBA board (odds-service
/api/v1/odds/props/bulk?league=wnba) on 2026-07-31 at ~23:10 UTC:
| book | two-way quotes | one-way | fresh (>22:00Z) | stale |
|---|---|---|---|---|
| Pinnacle | 106 | 0 | 0 | 106 |
| DraftKings | 416 | 0 | 261 | 155 |
| FanDuel | 376 | 0 | 231 | 145 |
| BetOnline.ag | 277 | 0 | 177 | 100 |
| Caesars | 175 | 0 | 114 | 61 |
| BetRivers | 0 | 233 | 165 | 68 |
BetRivers posts NO under prices — ever. It is structurally impossible to de-vig, and its over prices are wide outliers (Arike reb 2.5 at −113 while the real market was −147/−148/−154; Boston reb line 18.5 vs a 16.5 consensus). Any "edge" whose best price comes from BetRivers is an artifact. Exclude it from both the de-vig set and the best-price scan.
Pinnacle posts ONE scrape (~19:41 UTC) and is stale by tip. Using it as the fair-value anchor against a market that has since drifted manufactures edge in whichever direction the market moved — this flipped Awa Fam reb OVER 5.5 from "+1.2% vs Pinnacle" to −5.4% vs the current market. Despite feedback_pinnacle_default, on a same-night screen prefer a ≥3-book fresh two-way consensus and treat Pinnacle as a cross-check only.
Two more gotchas, same audit:
is_liveis unreliable — the same scrape second returnslive:truefor one book andlive:falsefor another on games whose SDS boxscore still saysstatus: scheduled. Confirm tip via SDS boxscore, not this flag.- When scanning for "best price," restrict to the freshest scrape batch. Scanning all pregame quotes pulled a 17-hour-old +103 on a prop that had since steamed to −147, inventing a fake +13.5%. See feedback_devig_phantom_edge.
Which markets exist, per league (verified 2026-08-24)
odds-service requests markets from LEAGUE_MARKETS in
app/services/odds_fetcher.py. Each market = 1 quota unit per call.
- NBA: 11 — points, rebounds, assists, threes, steals, blocks, turnovers, pra, pr, pa, ra.
- WNBA: 8 — the same minus steals, blocks, turnovers. WNBA books have NEVER published those: measured 3,585 steals + 2,753 blocks + 4,648 turnovers + 17,424 minutes bets over 30 days with zero closing lines. WNBA used to share NBA's list, paying quota for nothing.
- So steals/blocks/turnovers CLV arrives automatically when NBA opens — the WNBA blindness is the books, not us.
⚠️ player_steals_blocks is NOT a valid Odds API market — 422 "Invalid
markets", identical to a key I made up to check. sportlib's
nba.odds_api_markets lists it anyway, so that registry entry is wrong.
This matters more than a missing prop: one bad key 422s the WHOLE per-event
request, so trusting the registry would have stored ZERO NBA props of any
kind.
How to verify a market key (do this before adding one): call the
per-event endpoint, /v4/sports/{sport}/events/{id}/odds?markets=KEY.
- 200 + bookmakers = valid and priced
- 200 + zero bookmakers = valid key, this league's books don't offer it
- 422 = invalid key, will take the whole request down
Do NOT test on /v4/sports/{sport}/odds — it rejects player_points too, with
a different message ("Markets not supported by this endpoint"). The two
failures look alike and mean opposite things.
Shipped 2026-08-24 (odds-service b1aa3e7): NBA_MARKETS 10→11 (+turnovers), new WNBA_MARKETS = 8. Verified post-deploy: WNBA feed still returns 498 rows across the same 8 markets, so the split cost nothing and saves 2 quota units per call.