
esportsodds.gg
io.github.esportsoddsPublished by GitHub account esportsoddsv0.4.5Updated Nov 3, 2026
CS2 match data for AI assistants: matches, maps, vetoes, rounds, player stats, teams, rankings.
Overview
esportsodds.gg's local MCP server; no sign-in required; STDIO.
No AI overview yet: these facts come from the registry entry and the server itself.
Installation
In SourceWeft
- Open esportsodds.gg in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
esportsodds
The official Python client for the esportsodds.gg CS2 data API.
Counter-Strike 2 matches and results, team and player stats, tournaments, rankings and a derived market line, returned as plain Python dicts. Standard library only: there is nothing else to install.
Get CS2 match data and stats without scraping HLTV. This package is a client for a paid API; it is not an official HLTV/Valve API.
Install
Python 3.10 or newer. No dependencies.
Try it first, with no account: set ESPORTSODDS_API_KEY to the public sample key eo_sample_public, which
reads a fixed sample of past matches. It is shared by everyone and rate-limited per network address.
For evaluation; our Terms apply.
No subscription yet? A free sandbox key reads a fixed
sample of past matches, so you can build against real responses first. The quickstart below asks
for upcoming matches, which are outside the sample, so with either key start with
sandbox_tour.py
and a tournament id from the guide. Any request outside the sample raises PermissionDeniedError
(sandbox_scope), and its message names the events that work.
Quickstart
Every call returns what the API sent, as ordinary dicts and lists. A list call returns a Page
(page.data, page.next_cursor, page.has_more, page.meta); a call for one thing returns a dict.
Matches come newest first unless you say otherwise, which is what you want for results. For
scheduled matches that order starts with the fixture furthest away, so the quickstart passes
sort="scheduled_at" to get the next ones to be played.
Your API key and the plan
There are two plans, billed monthly or yearly and paid up front: Standard is $99 a month for 50,000 requests
and Pro is $299 a month for 150,000 requests. A year costs ten months: $990 a year for Standard and
$2,990 a year for Pro, two months free. Requests are counted per account, per calendar month (UTC), across every
key the account owns. There is no trial. A free sandbox key
reads a fixed sample of past matches, up to 2,000 requests in total, with no card; it is not a plan, and
current data needs one. Past your plan's requests, calls get a 429 with the code quota_exceeded until the
month resets, and this client raises QuotaExceededError without retrying (retrying cannot help). A spent
sandbox key gets sandbox_quota_exceeded, raised the same way (it never resets), and a request
outside its sample raises PermissionDeniedError (sandbox_scope, not counted).
The bulk export (matches.bulk() and the walkers built on it) has one more limit. On a Standard account that first subscribed on or after 30 October 2026 you may make up to 250 bulk-export requests a month; past that the API
answers 429 with bulk_quota_exceeded, which raises QuotaExceededError without retrying and leaves every
other call working. Pro has no bulk limit, and a Standard account that first subscribed before that date has none either.
Create a key in your account after you subscribe, then keep it out of your code:
- The client sends the key only in the
Authorization: Bearerheader, never in a URL. It shows only the last four characters inrepr()and never puts the key in an error message or a log line. - It talks HTTPS only. Plain
http://is accepted forlocalhostand other loopback addresses (127.0.0.1,::1) alone, and redirects are never followed, so a key cannot be sent to a different host. - Keep the key on your server. A browser or mobile app should call your backend, not the API.
- You can look at real responses before subscribing on the public demo.
Configuration
A Client is safe to share between threads. Use it as a context manager (with Client() as client:)
or call client.close() when you are finished: it closes a custom transport that has a close()
method (the default one keeps no connections open, so it has nothing to release).
Methods
Every endpoint is one method. {game} is the client's game, and {id} accepts the UUID or, for
teams, players and tournaments, the slug.
Filters are keyword arguments named as in the API reference.
A list given for a comma-separated filter (has=["rounds", "depth"], include=["maps", "stats"])
is joined for you; a datetime or date is formatted for you (a naive datetime means UTC); an
unknown argument raises TypeError instead of being sent. Ids are checked and escaped before they
go into a URL.
The odds are our own derived lines (source="eo_market"), not any bookmaker's price. The model
line (source="eo_model") is coming soon and is not returned yet.
Pagination, the bulk export and the change feed
List calls return one page and a cursor. Walk them yourself:
or let the client do it. iter() yields rows, pages() yields pages, and both fetch lazily, one
request per page, so stopping early costs nothing more. max_pages is a safety limit; the last page
yielded still carries its next_cursor, so a job that stops (or crashes) can resume with cursor=.
iter and pages exist on matches, odds, teams, tournaments and players.
The bulk export is the cheap way to pull history: 25 matches per request, each with the sub-resources you ask for embedded, and one request against your allowance per page.
max_pages stops the walk after that many requests. Without it, iter_bulk keeps going until the filter runs out, which for a wide
one is the whole history, one request per page. On a Standard subscription with the 250-a-month limit, a long walk
stops with bulk_quota_exceeded when the month's requests are used. Walk with pages_bulk() and save each page's
next_cursor, and the walk resumes with cursor= after the month resets.
A busy moment answers 503 with a short Retry-After; the client waits and retries for you.
The change feed keeps a local copy current. Give iter_changes the changed_at of the newest
row you stored last time and it yields every match that changed since, oldest change first:
It starts ten minutes before since (overlap=) because a row can become visible shortly after the
changed_at it carries, and drops the repeats this causes. A quiet hour costs one request.
matches.history(match) and matches.iter_history(match) show what each of those changes was.
Errors
Every error is an EsportsOddsError. A reply with an error status raises an ApiError subclass;
branch on the class, or on error.code, never on error.message.
An unknown path id is a 404 (NotFoundError), not a 400, and an unknown id on a sub-resource
such as matches.maps(...) returns an empty page rather than an error.
An ApiError carries status, code, message, request_id, retry_after, headers and body.
Quote request_id when you report a problem; it is also in client.last_response.request_id.
Rate limits and retries
Two limits apply: a per-key token bucket (20 requests a second sustained, bursts of 100) and
the monthly allowance of your plan, 50,000 requests on Standard or 150,000 on Pro, per account,
shared by all of an account's keys. After every
call, client.last_response.rate_limit has the budget the API reported:
The client retries only what is safe to retry, up to max_retries times (3 by default), and logs
each retry at INFO on the esportsodds logger:
- A
429 rate_limitedor503waits theRetry-Afterthe API sent, plus up to half a second of jitter. A wait longer than a minute is raised instead of slept through. - A
502or504and a network failure back off from half a second, doubling, up to eight seconds, with jitter. - A
500is retried at most twice however highmax_retriesis: the API counts it against your allowance, so each retry spends a request you are paying for. - A
429 quota_exceededis never retried, and nothing else is.
Polling is rarely the cheapest design: the change feed and the WebSocket cost far fewer requests. The request budget guide has the numbers.
WebSocket (match updates)
The WebSocket pushes score and odds changes for the matches you subscribe to, and it costs no requests: neither the ticket, nor the connection, nor the messages count against your allowance. Updates arrive when our own collection sees a change, so the socket removes your polling delay but cannot make the data newer than that collection; the guide linked below lists the typical ages.
This package does not open sockets itself (that would need a dependency). It mints the ticket and
builds the address; any WebSocket library connects, for example
websockets (pip install websockets):
- A ticket lasts 60 seconds and is not single-use, so a retry inside that window can reuse it; mint a new one once it has expired. Your key never appears in the socket address.
- A
snapshotis the full current state of a match and anupdateis a change.seqcounts up per match; if one is skipped, subscribe to that match again to get a freshsnapshot. - Reconnect with exponential backoff and jitter, and re-send your
subscribe. There is no replay. - A key may hold five connections at once, so subscribe to many matches on one connection.
- The server pings every 54 seconds;
websocketsanswers for you.
The WebSocket guide lists every frame type.
AI assistants (MCP)
The package includes an MCP (Model Context Protocol) server, so an AI assistant such as Claude or
Cursor can answer questions from the API: a team's recent form, the vetoes of a match, a
player's career figures. It runs on your machine, reads ESPORTSODDS_API_KEY and only reads data.
Each tool call is one request against your allowance, and the list tools ask for 20 rows unless the
assistant sets limit.
Add it to your MCP client's configuration (Claude Desktop: claude_desktop_config.json; Cursor:
~/.cursor/mcp.json). With uv installed, uvx fetches the package itself, so there is nothing
else to set up:
Or pip install esportsodds and use esportsodds-mcp as the command, with no args (give its
full path, from which esportsodds-mcp, if the client cannot find it). In Claude Code:
The tools are matches_list, matches_get, matches_maps, matches_stats, matches_vetoes,
matches_rounds, matches_lineups, odds_list, teams_list, teams_get, teams_h2h,
teams_form, teams_maps, teams_results, teams_players, tournaments_list,
tournaments_get, tournaments_teams, players_list, players_get, players_career,
players_matches, rankings_list and maps_list. Each is one row of the methods table above,
with that method's arguments, and the server answers clients of every MCP revision from
2024-11-05 to 2026-07-28.
Examples
The examples/ folder has
runnable scripts. Each reads ESPORTSODDS_API_KEY.
FAQ
Is this an official API? No. It is not an official HLTV/Valve API, and esportsodds.gg is not affiliated with or endorsed by Valve or any stats site.
Why not scrape? Scraping breaks whenever a page changes and many sites forbid it. With this API you build without scraping HLTV or anyone else: you call stable, documented endpoints instead.
Is there a free tier? Not for current data. There is no trial; Standard is $99 a month for 50,000 requests and Pro is $299 a month for 150,000, paid up front. A free sandbox key reads a fixed sample of past matches, and the public demo shows real responses without an account.
What does a call cost? One request against your monthly allowance, whatever it returns. A bulk page, which
carries up to 25 matches with their detail, is also one request (and, on a Standard account that first subscribed on or after 30 October 2026, one of its 250 bulk-export requests a month). A walker (iter, pages,
iter_bulk, iter_changes) costs one request per page it fetches. The WebSocket costs none.
Does it work with asyncio? The client is synchronous and safe to share between threads. From async code, run a call in a thread:
Can I use another HTTP library? Yes. Pass transport= any object with a
send(TransportRequest) -> TransportResponse method. To go through a proxy with the default
transport, pass UrllibTransport(proxies={"https": "http://proxy.example:3128"}).
Which games are supported? CS2 (game="cs2"). The game argument exists for later titles.
Is it typed? Yes: the package ships py.typed and passes mypy --strict. Responses are plain
dicts, so they are dict[str, Any].
Where do I report a bug in this client? In this repository's issues. Questions about your account, billing or the data go through esportsodds.gg.
Licence
The code in this repository is released under the MIT licence. The licence covers this client, not the data it fetches: the data is licensed to you under sections 4 and 5 of the Terms of Service, which allow you to build on it for your own purposes and do not allow redistributing it.
Counter-Strike and CS2 are trademarks of Valve Corporation. esportsodds.gg is independent and is not affiliated with, endorsed by or sponsored by Valve.
Source: README.md at commit 973c5cb
Tools
0Version history
1- v0.4.5LatestNov 3, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts