nerd

Match predictions (Elo win/draw/loss + value bet)

COVERAGE, stated plainly: this is not a per-sport prediction feed. Re-measured 2026-09-28 against production: 276 rows in total, and every row is tied to one match, so the sport of a row is the sport of its match. By sport, against that denominator of 276; football 176 (72 match_result for FIFA World Cup 2026 plus 104 value_prop), american_football 67, ice_hockey 29, baseball 2, basketball 2. Those five sports are exactly what meta.covered_sports names. A plan scoped to any OTHER sport receives 403 with the coverage named; a plan scoped to one of these five is admitted and sees only its own sports' rows. Predictions are written per batch against matches that already carry moneyline odds, not as a continuous per-sport feed, so which of the five sports has rows on a given day follows the odds board. (The 2026-09-02 figure of 129 rows, and its "there is no live writer" note, are superseded: the value_prop writer resumed and added 147 rows.)

FRESHNESS, AND WHY IT IS ON THE RESPONSE. 129 of those 276 rows (46.7%) are past their own valid_until; all 72 match_result rows and 57 of the 204 value_prop rows, the oldest by 92 days. They are still returned; nothing is filtered. Each row carries valid_until, expired (true / false / null when the row states no expiry, which is not the same as fresh) and expired_days, and meta carries returned, expired, expiry_unknown, as_of and; only when at least one row is expired; stale: true and a human message. An expired row describes a match already played and must not be read as a forecast: check valid_until per row before using it.

Pre-computed Elo-based win / draw / loss probabilities per match, with value-bet analysis (Elo-implied vs de-vigged market probability) when moneyline odds exist. Filter by tournament (e.g. wc2026), match_id, or type. Requires Solo plan or higher (Free receives 403); results are additionally scoped to the sports your key is entitled to. Response array item keys: match_id, prediction_type, home_team, away_team, home_win_probability, draw_probability, away_win_probability, home_elo, away_elo, model_version, valid_until, expired, expired_days, generated_at, kickoff_utc, plus value_bet / best_side / elo_prob / market_prob / edge (null until moneyline odds land).

UPDATE 2026-10-09 (correcting forward; the 276-row and 46.7% figures above are a 2026-09-28 snapshot and are left as the record of that date). Re-measured against the whole table, 642 rows: football 259 (72 match_result plus 187 value_prop), american_football 258, ice_hockey 85, baseball 24, basketball 16. 414 of 642 (64.5%) are past their own valid_until.

Two corrections to the paragraphs above. First, "null until moneyline odds land" was the wrong explanation for value_bet / best_side / elo_prob / market_prob / edge being null on a value_prop row: a value_prop row is only written for a match that ALREADY carries moneyline odds, and the row stores its own model probability, de-vigged market probability and edge. 570 of 642 rows (88.8%) were returning those five fields null, plus home_team, away_team and the three win/draw/loss probabilities, because the read path extracted only the key names a match_result row uses. value_prop rows now serve best_side, elo_prob, market_prob, edge and value_bet from their own stored analysis, and home_team / away_team from the match. home_win_probability, draw_probability and away_win_probability remain null on a value_prop row deliberately: that model is two-way and states no draw, so the number is reported only where it is side-scoped, in elo_prob against best_side.

Second, ORDERING. The list was ordered by kickoff ascending, i.e. oldest first, so ?limit=5 returned five expired rows, the first 124 days past expiry, while 228 live rows sat further down. Live rows (and rows whose expiry is unknown) now sort ahead of expired ones; within the live group soonest kickoff first, within the expired group most recent kickoff first. This is an ordering change and not a filter: nothing is withheld, meta.total is unchanged, and every expired row is still returned on a later page.

get/v1/predictions

Request

  • Base URL: https://api.bigballsdata.com
  • URL: https://api.bigballsdata.com/v1/predictions
  • Auth: one of:
    • API key in header x-api-key
    • HTTP bearer

Query parameters

tournament'wc2026'

Tournament filter, e.g. wc2026.

match_idstring uuid

Single match.

typestring

Prediction type filter.

sport'football' | 'basketball' | 'baseball' | 'ice_hockey' | 'cricket' | 'mma' | 'boxing' | 'formula1' | 'american_football'

Narrow to one sport, WITHIN your key's entitlement; it never widens access. Predictions cover football, american_football, basketball, baseball and ice_hockey (2026-09-28). Asking for one of the other four sports returns 200 with an empty array plus meta.coverage, meta.covered_sports and a meta.note saying nothing has ever been written for it; that is the true answer to a well-formed request, not an error. A value that is not a sport at all is a 400.

Until 2026-09-04 this parameter was accepted and silently discarded, so every value returned the same rows regardless of sport (#1376).

limitinteger
offsetinteger

Response

ok. When ?sport= named a sport with no predictions, data is empty and meta carries coverage, covered_sports and note.

object required

Changes

No recorded changes to this operation across all 2 revisions of this API.