Integrating an odds feed with AI coding tools
The OpenAPI spec for OddsRelay's odds API is one keyless file, https:/ (version 3.1), listing every route, parameter, error code and example reply. Hand it to an AI coding assistant and the model can draft your client before you have a key.
A contract beats a pasted docs page
A model writes a client from what it is given. Given a web page, it guesses at field names. Given an OpenAPI document, it reads them. In the contract each parameter has a type, each JSON error has a machine code, and the data examples are trimmed from live replies, which is what a model copies when it writes your types.
The API is strict about inputs, and that helps here. Any parameter a route does not read is a 400 bad_ that names it. A parameter the model invented fails loudly on the first call instead of quietly returning a board you did not filter.
Where to point the assistant
- The contract at
https:/. It is keyless. This is the file to give the model first./ api.oddsrelay.io/ v2/ openapi.json - The developer guides under /docs/guides. Each one is also served as Markdown at
/, which drops into a context window cleanly. The quickstart is a complete example client to hold the model's output against.docs/ guides/ <slug>.md /andllms.txt /on the site: an index of the docs, and the docs, endpoints and error catalogue in one plain-text file.llms-full.txt
curl -s --compressed https://api.oddsrelay.io/v2/openapi.json -o openapi.json
Spell out the behaviour you want
A vague prompt gets a vague client: one fetch, no caching, a key pasted into the source. Name the board, the region and the filters, and spell out the behaviour the docs expect. Something like this:
Using openapi.json, write a small TypeScript client for GET /v2/odds/standard. - region=uk, sports=soccer, one bookmaker filter. - Read the key from the ODDSRELAY_KEY environment variable. Send it as "Authorization: Bearer <key>". Never put it in the URL or in logs. - Send Accept-Encoding: gzip. - Before the first real call, price it with quote=true. - Request format=bare, keep the ETag and send it back as If-None-Match. On 304, keep the rows you have. - On 429, wait for Retry-After. On 402, stop and report the error code. - Types must use only the fields in the contract's schemas and examples.
The last line matters most. Models like to add fields that sound plausible, and a type with an invented field compiles happily until the day your code reads it.
What the reply looks like
The feed covers 140+ bookmakers, 60+ of them live in the UK & Ireland. On the standard board each outcome carries back offers, a bookmaker's price, and lay offers from Betfair, Smarkets and Matchbook, each with its price and available, the liquidity on offer. Here is one event in that shape, trimmed to one outcome and without competition_. The lay offer is made up for the example:
{
"meta": {
"feed_type": "standard",
"region": "uk",
"odds_format": "decimal",
"processed_at": "2026-09-17T02:20:57.271Z",
"last_seen": {
"smarkets": { "tennis": "2026-09-17T02:20:56Z" },
"william_hill": { "tennis": "2026-09-17T02:20:55Z" }
},
"count": 1,
"version": "v2",
"next_cursor": null
},
"data": [{
"event_id": "or_evt_917dd44bce05",
"sport_key": "tennis_itf_w35_shenyang_chn",
"sport_title": "ITF W35 Shenyang CHN",
"commence_time": "2026-09-17T03:00:00Z",
"home_team": "Sijia Wei",
"away_team": "Zhuoxuan Bai",
"markets": [{
"key": "h2h",
"outcomes": [{
"name": "Sijia Wei",
"back": [{ "bookmaker": "william_hill", "price": 2.9, "link": null }],
"lay": [{ "exchange": "smarkets", "price": 3.0, "available": 175, "link": null }]
}]
}]
}]
}The reply carries prices and the available liquidity, and stops there. So if your assistant writes a type with a rating field, it has made one up. The rating belongs in your code. The oddsmatcher widget works it out the same way, as back over lay times 100, with no commission in it:
const back = outcome.back[0].price; // 2.9 const lay = outcome.lay[0].price; // 3.0 const rating = (back / lay) * 100; // 96.7
For every field in the envelope, see the anatomy of an odds API response.
Review what the model wrote
An assistant gets you a draft in minutes. Reading it is still your job. Check these first:
- Field names. Check every one against the contract. Lay depth is
available. Matched offers carry nolast_: that field belongs to the raw board. Aupdate priceoravailablecan benull, and an outcome can arrive with no lay offers, so a type that rules outnullfor them, or makeslayrequired, is wrong. - Board shapes. The matched boards share the envelope, not every field.
layis an array onstandard,bogand2up, an object ofwinandplacearrays oneach-wayandextra-place, and absent ondutching. A client typed from the standard board breaks on each-way, so type the shapes the contract gives. - Gzip. The standard, dutching and raw boards answer
400to a request that refuses gzip. Python'surllibsendsidentityunless told otherwise, so a generated Python client can fail on its first call. SendAccept-Encoding: gzip. - Polling. A self-serve key is pull only, so the client polls, and polling an odds feed efficiently covers the loop. It should send
If-None-Matchevery time. Ask forformat=. Without it,bare metacarries each board version's time, so theETagmoves even when no price did (polling cheaply). An unchanged board comes back as a304with no body and costs no tokens. The conditional requests guide has the details. - Errors. Every JSON error is
{"error": {"code",. Branch on"message", "request_ id"}} code, never on the message text. The front door's own 429 and 502 come with an empty body and nocode, so check for a body before readingerror.code. On the 429, wait forRetry-After. On the 502, retry after a growing wait. - Freshness. Use the times the data carries:
meta.processed_for the board andat meta.last_for each venue and sport. Withseen format=the board time moves to thebare X-Processed-Atheader, andlast_comes from keyedseen /. A model may invent a staleness cutoff. Make sure any cutoff in the code is one you chose. The freshness guide explains both fields.v2/ coverage
The key never belongs in the code
Server keys start or_ and go only in the Authorization or x-api-key header, read from an environment variable. Never put one in a query string or a browser bundle. Browser keys start or_ and answer only calls from their own website. A key written into a source file is a bug. Fix it before you run anything.
Giving an agent the feed as a tool
The contract also serves a model that calls the feed at run time instead of writing code for it. Expose one narrow function that your server runs, say a standard-board lookup that takes sports and bookmakers, and let the model call that. The key stays on your server. The filters keep each reply to what the question needs.
A call that returns a board spends tokens, priced from the board and filters it asks for. Have the function run quote= first and refuse anything over a budget you set. The filters and tokens guide explains the pricing.
Is there an odds API MCP server?
OddsRelay publishes oddsrelay-mcp, a read-only Model Context Protocol (MCP) server on npm, so an AI assistant can call the feed without a function of your own. It runs on your own machine over stdio and works in Claude Desktop, Claude Code, Cursor and any other client that starts MCP servers that way. It needs Node 18 or later, and reads your key from ODDSRELAY_. In Claude Code it is one line:
claude mcp add --transport stdio oddsrelay --env ODDSRELAY_KEY=your_key_here -- npx -y oddsrelay-mcp
Claude Desktop and Cursor take the same npx -y oddsrelay-mcp command in the mcpServers block of their config file, which the server's README spells out. The key then sits in that file as plain text, so keep it out of anything you commit. Claude Desktop can also install the server from the oddsrelay-mcp.mcpb bundle on the releases page, which asks for the key in a masked field and runs on the Node built into Claude Desktop. The server is also listed in the MCP Registry as io.github.oddsrelay/, and on Smithery.
The server has seven tools, and none of them writes anything. sports, events, bookmakers, coverage and usage cost no tokens. odds reads one matched board for one sport at the bookmakers the assistant names, and event_ reads one event's prices from the raw board. Both quote the call first and refuse it when the price is above their max_ argument, which has a small default. The assistant then gets the price instead of the data, and can narrow the filters or ask you before it tries again.
Start with the contract
Run the prompt above against the contract and read what comes back. See coverage for what it will return. The API reference renders the same contract for human readers.
Related posts
- The anatomy of an odds API responseEvery field in a /v2 odds response, from the meta envelope down to each back and lay offer.
- Efficient odds API polling with ETag and 304sSend the ETag back as If-None-Match and an unchanged reply is an empty 304 that costs no tokens.
- How to display odds data in your appTurn a nested odds response into table rows, format prices and show each price's age.
18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.