Core concepts

Odds movement

open_price, delta_since_open and is_closing — reading a price series correctly.

Odds lines are a time series. Three fields let you read movement without fetching the whole history.

The movement fields

FieldMeaning
open_priceThe earliest captured price in this (market, outcome, source) series.
delta_since_openprice − open_price. Positive means the price drifted out (less likely); negative means it shortened.
is_closingTrue on the last line captured before scheduled_at — the closing price.
is_winnerWhich outcome came in. null until the market settles.

open_price and delta_since_open are computed per response, not stored — so they're correct on every row without you fetching the series first.

Append-on-change

A new row is written only when the price changes. This is the single most important thing to know about the series, and it has a consequence people get wrong:

Age is not staleness

A line captured eleven hours ago is not stale — it means the price hasn't moved in eleven hours, which is itself information. A recency check that discards "old" lines will discard perfectly valid prices for quiet markets.

Judge whether data is flowing by whether any market is updating, not by the age of one line.

Snapshot vs history

The same endpoint serves two different things:

# Snapshot: latest line per outcome, across every open market. NOT paginated.
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
  "https://api.esportsodds.gg/v1/cs2/odds"

# History: every captured line for one match, newest first, cursor-paginated.
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
  "https://api.esportsodds.gg/v1/cs2/odds?match=019f3d18-c15f-7319-81a7-343e8a80a578"

In snapshot mode limit truncates silently and meta.next_cursor is always null — if meta.count equals your limit, assume there were more.

The closing line

is_closing marks the last price before the scheduled start. It's the standard benchmark for evaluating a forecast: comparing a prediction against the closing line asks "did this beat where the market ended up", which is a much harder and more meaningful test than comparing against an opening price.

To collect closing lines for settled matches, walk history mode per match and take the flagged rows; they also carry is_winner, so you have prediction and outcome in one place.

Reading a series

lines = get(f"/v1/cs2/odds?match={match_id}&source=eo_market&limit=1000")["data"]

# History comes back newest-first; reverse for chronological order.
series = sorted(lines, key=lambda l: l["captured_at"])

for l in series:
    implied = 1 / l["price"]
    print(f"{l['captured_at']}  {l['label']:20} {l['price']:6.2f}  p={implied:.3f}"
          f"  {'CLOSE' if l['is_closing'] else ''}")

Group by (market_id, outcome_key, line) if a match has more than one market — otherwise you'll interleave a moneyline with a handicap and see movement that isn't there.

On this page