guide

"Read the CS2 market odds line via the API"

Jul 10, 2026

Bottom line: The CS2 odds endpoint serves one derived line — eo_market, an aggregate combined from multiple bookmakers and exchanges with the margin already removed. You pull the current snapshot, or a single match's line history, and convert each decimal price into an implied probability with 1 / price. Because the margin is stripped, the two sides of a market sum to about 100% with nothing left to de-vig yourself. No individual book is ever named. Built on the EsportsOdds CS2 data API.

What the odds endpoint serves

A quick framing point, because it shapes everything below. The API does not resell individual bookmaker prices. It serves a derived aggregate, eo_market, built from several sources with the bookmaker margin removed. Each line tells you how many sources fed it via book_count, but the sources themselves are never named. (A separate proprietary model line, eo_model, is in the works and gated until it clears validation; this guide uses the market line.)

Two ways to read it

GET /v1/cs2/odds has two modes. Without a match parameter it returns the current snapshot — the latest line for every open market, in one unpaginated response. With ?match={id} it returns that one match's line history, newest first, which is paginated.

Reading the market line: pull the snapshot, pick a match that has a line, fetch its history, then convert each price to a fair probability with one over price.

Start with the snapshot, filtering to the market source explicitly:

import os
import requests

BASE = "https://api.esportsodds.gg"
HEADERS = {"Authorization": f"Bearer {os.environ['ESPORTSODDS_API_KEY']}"}


def snapshot() -> list[dict]:
    r = requests.get(f"{BASE}/v1/cs2/odds", params={"source": "eo_market"},
                     headers=HEADERS, timeout=15)
    r.raise_for_status()
    return r.json()["data"]

The source parameter must be eo_market (or eo_model once it's live) — anything else returns a 400. Each row carries a label (the outcome, e.g. a team name), an outcome_key (home / away for a match winner), a decimal price, a book_count, and a captured_at timestamp.

Convert price to implied probability

Decimal odds and probability are two views of the same number. The implied probability of a decimal price is simply its reciprocal:

def implied(price: float) -> float:
    return 1.0 / price

Here's why that's all you need. On a raw bookmaker board, the two sides of a match-winner market add up to more than 100% — that excess is the margin. The eo_market line already has it removed, so the two implied probabilities sum to roughly 100% on their own.

Because the market line is already de-vigged, the two outcomes' implied probabilities sum to about 100% — there's no overround left to strip out.

def match_winner_probs(lines: list[dict]) -> dict[str, float]:
    winner = [ln for ln in lines if ln["outcome_key"] in ("home", "away")]
    return {ln["label"]: round(implied(ln["price"]) * 100, 1) for ln in winner}


# e.g. {"Natus Vincere": 63.2, "FaZe Clan": 36.8}  — sums to ~100

The result is a clean market-implied win probability for each side. Present it as exactly that: "market-implied," a neutral read of where the aggregate sits, not as a recommendation.

Track how a line moves

Pass a match ID and you get that match's history, so you can watch the line evolve as a game approaches. This mode is paginated, so use the cursor loop from the pagination guide:

def line_history(match_id: str) -> list[dict]:
    rows, params = [], {"source": "eo_market", "match": match_id, "limit": 500}
    while True:
        r = requests.get(f"{BASE}/v1/cs2/odds", params=params, headers=HEADERS, timeout=15)
        r.raise_for_status()
        payload = r.json()
        rows.extend(payload["data"])
        cursor = payload["meta"].get("next_cursor")
        if not cursor:
            return rows
        params["cursor"] = cursor


history = line_history("0191f2c8-8a1e-7c3a-9f5e-2b6d4e8a1c00")
one_side = [ln for ln in history if ln["outcome_key"] == "home"]
for ln in sorted(one_side, key=lambda x: x["captured_at"]):
    print(f"{ln['captured_at']}  {implied(ln['price']) * 100:5.1f}%  ({ln['book_count']} sources)")

Charted, that history shows the market-implied probability drifting as the match nears — the kind of movement analysts track for closing-line value:

An example line moving over time: the market-implied win probability for one team across successive snapshots, settling at the close.

The delta_since_open and open_price fields on each row give you the move relative to the opening line without recomputing it, and is_closing flags the final captured line.

A few things to keep straight

  • Never surface a source. The line is an aggregate by design; book_count is the honest way to convey depth, and no individual book is named — keep it that way in whatever you build.
  • Prices are decimal odds. Convert to probability with 1 / price; don't assume a different format.
  • The market line is de-vigged; a raw board isn't. If you ever mix in prices from elsewhere, those still carry a margin — this line doesn't.

Next steps

Pull the snapshot, convert with 1 / price, and track the history for movement. The endpoint reference documents every field on an odds line.