Skip to content

BlogFundamentals

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.txt and /llms-full.txt on the site: an index of the docs, and the docs, endpoints and error catalogue in one plain-text file.

Example · save the contract for your assistant (no key needed)

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:

Example prompt

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:

Example · one event from GET /v2/odds/standard (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:

Example · your code computes the rating from the pair

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 no last_update: that field belongs to the raw board. A price or available can be null, 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. lay is an array on standard, bog and 2up, an object of win and place arrays on each-way and extra-place, and absent on dutching. 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 400 to a request that refuses gzip. Python's urllib sends identity unless told otherwise, so a generated Python client can fail on its first call. Send Accept-Encoding: gzip.
  • Polling. The feed is pull-only, so the client polls. It should send If-None-Match every time. An unchanged board comes back as a 304 with no body and costs no tokens. The conditional requests guide has the details.
  • Errors. Every JSON error is {"error": {"code", "message", "request_id"}}. Branch on code, never on the message text.
  • Freshness. Use the times the data carries: meta.processed_at for the board and meta.last_seen for 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.