Integrating an odds feed with AI coding tools
OddsRelay · · Updated · 5 min read
Hand your AI coding assistant the OddsRelay OpenAPI 3.1 contract and it has the whole API in one file: every route, parameter, error code and example reply. The contract needs no key, so the model can read it and draft your client before you have one.
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 and each error has a machine code, and the example responses 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_request 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://api.oddsrelay.io/v2/openapi.json. It is keyless. This is the file to give the model first. - The developer guides under /docs/guides. Each one is also served as Markdown at
/docs/guides/<slug>.md, which drops into a context window cleanly. The quickstart has the whole flow in three languages. /llms.txtand/llms-full.txton the site: an index of the docs, and the docs, endpoints and error catalogue in one plain-text file.
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. - 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, Matchbook and BETDAQ, each with its price and available, the money on offer. Here is one event, trimmed:
{
"meta": {
"region": "uk",
"odds_format": "decimal",
"processed_at": "2026-09-17T02:20:57.271Z",
"last_seen": { "william_hill": { "soccer": "2026-09-17T02:20:55Z" } },
"count": 1,
"unreliable_links": [],
"version": "v2",
"next_cursor": null
},
"data": [{
"event_id": "or_evt_917dd44bce05",
"sport_key": "soccer_epl",
"sport_title": "Premier League",
"commence_time": "2026-09-20T14:00:00Z",
"home_team": "Arsenal",
"away_team": "Chelsea",
"markets": [{
"key": "h2h",
"outcomes": [{
"name": "Arsenal",
"back": [{ "bookmaker": "william_hill", "price": 2.9, "link": null }],
"lay": [{ "exchange": "betfair_exchange", "price": 3.0, "available": 175, "link": null }]
}]
}]
}]
}The board is paired against exchange lay and liquidity-gated. The rating and qualifying loss are simple arithmetic on the pair, and the oddsmatcher widget shows them. So if your assistant writes a type with a rating field, it has made one up. The arithmetic belongs in your code, at your own commission rate:
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_update: that field belongs to the raw board. Apriceoravailablecan benull, and an outcome can arrive with no lay offers, so a type that makes them required 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. The feed is pull-only, so the client polls. It should send
If-None-Matchevery time. 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", "message", "request_id"}}. Branch oncode, never on the message text. - Freshness. Use the times the data carries:
meta.processed_atfor the board andmeta.last_seenfor each venue and sport. A model may invent a staleness cutoff. Make sure any cutoff in the code is one you chose. The freshness guide explains both fields.
The key never belongs in the code
Server keys start or_live_ 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_pub_ 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=true first and refuse anything over a budget you set. The filters and tokens guide explains the pricing.
Start with the contract
Run the prompt above against the contract and read what comes back. When the client looks right, request access for a key, and see live coverage for what it will return. The API reference renders the same contract for human readers.