# Integrating an odds feed with AI coding tools

> Give an AI coding assistant the keyless OpenAPI 3.1 contract and a precise prompt, then review what it writes, or connect our read-only MCP server.

Published 1 July 2026 · updated 13 October 2026

**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](https://oddsrelay.io/docs/guides). Each one is also served as Markdown at `/docs/guides/<slug>.md`, which drops into a context window cleanly. The [quickstart](https://oddsrelay.io/docs/guides/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:

```bash
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:

```json
{
  "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](https://oddsrelay.io/widgets/oddsmatcher) 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:

```ts
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](https://oddsrelay.io/blog/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](https://oddsrelay.io/blog/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](https://oddsrelay.io/docs/guides/polling-cheaply)). An unchanged board comes back as a `304` with no body and costs no tokens. The [conditional requests guide](https://oddsrelay.io/docs/guides/conditional-requests) 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](https://oddsrelay.io/docs/guides/freshness) 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](https://oddsrelay.io/docs/guides/filters-and-tokens) 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:

```sh
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](https://github.com/oddsrelay/oddsrelay-mcp) 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](https://github.com/oddsrelay/oddsrelay-mcp/releases/latest), 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](https://smithery.ai/servers/oddsrelay/oddsrelay-mcp).

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](https://oddsrelay.io/coverage) for what it will return. The [API reference](https://oddsrelay.io/docs/reference) renders the same contract for human readers.

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

Source: https://oddsrelay.io/blog/integrating-an-odds-feed-with-ai-tools
