BlogBuilding with odds data

Integrating an odds feed with AI coding tools

The OpenAPI spec for OddsRelay's odds API is one keyless file, https://api.oddsrelay.io/v2/openapi.json (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_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 is a complete example client to hold the model's output against.
  • /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.

Save the contract for your assistant (no key needed) · example

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:

Prompt · example

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_key. The lay offer is made up for the example:

GET /v2/odds/standard · one event, trimmed · 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:

Your code computes the rating from the pair · example

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 rules out null for them, or makes lay 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. 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-Match every time. Ask for format=bare. Without it, meta carries each board version's time, so the ETag moves even when no price did (polling cheaply). 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. The front door's own 429 and 502 come with an empty body and no code, so check for a body before reading error.code. On the 429, wait for Retry-After. On the 502, retry after a growing wait.
  • Freshness. Use the times the data carries: meta.processed_at for the board and meta.last_seen for each venue and sport. With format=bare the board time moves to the X-Processed-At header, and last_seen comes from keyed /v2/coverage. 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.

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_KEY. In Claude Code it is one line:

Add the server to Claude Code · example

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/oddsrelay-mcp, 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_odds reads one event's prices from the raw board. Both quote the call first and refuse it when the price is above their max_tokens 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

18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.