# Arbitrage data: what an arbitrage tool needs

> What an arbitrage tool needs from an odds feed: back and lay prices for the same market, the money at each lay, and a time on every price, with real /v2 JSON.

Published 29 September 2026

**Betting arbitrage data is every venue's price for the same outcome, lined up so a tool can see when the prices cross: bookmakers' back prices, exchanges' lay prices, and the money available at each lay.** The sum that spots a crossed price takes a few lines. Getting all three for one fixture, one market and one line, across the UK and Irish bookmakers, is most of the build.

This guide is for teams building [arbitrage](https://oddsrelay.io/glossary/arbitrage) and value tools: scanners, alert services and trading software. It covers what the data has to carry, where each piece sits in an OddsRelay `/v2` reply, and which parts stay in your own code.

## What data does an arbitrage tool need?

An arbitrage tool needs five things for every outcome it checks: each venue's price, proof that the prices belong to the same fixture, market and line, the money behind any exchange lay, a time on each price, and a link to where each leg is placed. Leave one out and the tool starts flagging arbs nobody can take.

| What the tool needs | Why the check fails without it | Where it sits in /v2 |
| --- | --- | --- |
| Every bookmaker's back price | The best back price is one leg of the arb, and a venue you don't read is a price you never see | `back` on the matched boards, best price first. `price` on raw |
| Exchange lay prices | A lay covers every other result in one bet, so a back and a lay make a complete pair | `lay` on the matched boards, lowest price first. `lay_price` on raw |
| The money at each lay | A lay with too little behind it part-fills, and the second leg is left open | `available` on every lay offer |
| One fixture, market and line | Prices from different lines can look crossed and cover nothing | One `event_id`, one market `key` and, on line markets, one `point` per pair on the matched boards |
| A time on the data | A crossed price is often one venue's older read | `meta.processed_at` and `meta.last_seen`. `last_update` per market on raw |
| A link to each venue's event | Your user places each leg at its own venue | `link` on each offer. `includeLinks=true` on raw |

Coverage decides how often prices cross at all. A tool reading a handful of bookmakers sees fewer crossed prices than one reading every book its users hold an account with. The feed covers 140+ bookmakers, 60+ of them live in the UK & Ireland, and the matched boards set bookmaker back prices beside lay prices from Betfair, Smarkets, Matchbook and BETDAQ. bet365 is in every plan, with featured football and UK and Irish racing ([bet365 on OddsRelay](https://oddsrelay.io/bookmakers/bet365)).

## Back against lay, or back against back

Arbitrage across UK and Irish venues takes one of two shapes, and a matched board carries each of them.

The first shape is **back against lay**: a bookmaker's back price and an exchange's lay price on the same outcome. The lay covers every other result, so one pair covers the whole market. That is what the standard board sends, for every sport and market an exchange lays ([the matched feed](https://oddsrelay.io/products/matched-feed) lists the boards). Here is one pair from the contract's own examples:

GET /v2/odds/standard?sports=tennis&bookmakers=william_hill&exchanges=betfair_exchange&markets=h2h · example:

```json
{
  "meta": {
    "feed_type": "standard",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-09-17T02:20:57.271Z",
    "last_seen": {
      "betfair_exchange": { "tennis": "2026-09-17T02:20:56Z" },
      "william_hill": { "tennis": "2026-09-17T02:20:55Z" }
    },
    "count": 26,
    "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": "https://sports.williamhill.com/betting/en-gb/tennis/OB_EV41212781/sijia-wei-vs-zhuoxuan-bai"
        }],
        "lay": [{
          "exchange": "betfair_exchange",
          "price": 3.45,
          "available": 175,
          "link": "https://www.betfair.com/exchange/plus/tennis/event/36078894"
        }]
      }]
    }]
  }]
}
```

The back price, 2.9, sits below the lay price, 3.45, so these prices don't cross. That is the usual state of a pair: a bookmaker's back price normally sits under the exchange's lay. A back-against-lay arb needs the back price to beat the lay once the exchange's commission is taken off. The check, and the lay stake that balances the legs, are a few lines of your own code:

Your code: one back offer against the lowest lay · example:

```ts
// Customer-side arithmetic. The feed sends prices; the commission is your users' own.
const commission = 0.02;
const backStake = 100;

const back = outcome.back[0]; // back offers come best price first
const lay = outcome.lay[0]; // lay offers come lowest price first

const crosses = back.price * (1 - commission) + commission > lay.price;
const layStake = (backStake * back.price) / (lay.price - commission);
const fillable = lay.available !== null && lay.available >= layStake;
```

On the example's prices the check is false: 2.9 × 0.98 + 0.02 is 2.862, well under 3.45. The lay-stake line is the standard back-to-lay sum, (back stake × back price) ÷ (lay price − commission). Commission comes off the exchange side only, at whatever your users pay ([exchange commission](https://oddsrelay.io/glossary/exchange-commission) explains it).

The second shape is **back against back**: a back price on every outcome of one market, each at a different venue, with nothing laid. The dutching board sends exactly that ([the dutching feed](https://oddsrelay.io/products/dutching-feed)). Each back offer carries a `dutch_id`, and the offers sharing an id are the legs of one dutch, covering every outcome of a two- or three-outcome market.

GET /v2/odds/dutching?sports=tennis&bookmakers=william_hill,betfair_exchange&markets=h2h · example:

```json
{
  "event_id": "or_evt_8fe55f3bdad5",
  "sport_key": "tennis_atp_challenger_tiburon_usa_men_singles",
  "sport_title": "ATP Challenger Tiburon, USA, Men Singles",
  "commence_time": "2026-09-17T23:10:00Z",
  "home_team": "Michael Zheng",
  "away_team": "Trevor Svajda",
  "markets": [{
    "key": "h2h",
    "outcomes": [
      {
        "name": "Michael Zheng",
        "back": [{
          "bookmaker": "william_hill",
          "price": 1.33,
          "link": "https://sports.williamhill.com/betting/en-gb/tennis/OB_EV41211653/michael-zheng-vs-trevor-svajda",
          "dutch_id": "b8d7916c"
        }]
      },
      {
        "name": "Trevor Svajda",
        "back": [{
          "bookmaker": "betfair_exchange",
          "price": 3.5,
          "link": "https://www.betfair.com/exchange/plus/tennis/event/36077864",
          "dutch_id": "b8d7916c"
        }]
      }
    ]
  }]
}
```

Add `1 / price` across a dutch's legs, the sum of their [implied probabilities](https://oddsrelay.io/glossary/implied-probability). Below 1, the prices cross. Here the legs are 1.33 and 3.5, the sum is about 1.038, and the dutch sits at a book of 103.8%, on the ordinary side of 100. [Dutching data explained](https://oddsrelay.io/blog/dutching-data-explained) works through the stake on each leg.

Your code: the book on each dutch in one market · example:

```ts
const books = new Map<string, number>();
for (const o of market.outcomes) {
  for (const b of o.back) {
    if (b.dutch_id && b.price) books.set(b.dutch_id, (books.get(b.dutch_id) ?? 0) + 1 / b.price);
  }
}
const crossed = [...books].filter(([, sum]) => sum < 1).map(([id]) => id);
```

On the dutching board both venue filters, `bookmakers` and `exchanges`, take venues of either kind, and a dutch is kept only when every leg is at a venue you named. So one call can be narrowed to the accounts your users hold. The [matched-board filters guide](https://oddsrelay.io/docs/guides/matched-board-filters) lists every filter and what a filtered reply leaves out.

## The money behind the lay decides whether an arb is real

A crossed price is only an arb if both legs can be placed at the stakes the sum asks for. On the exchange side the data answers that directly, because each lay offer carries `available`, the money waiting at that price.

Lay offers come lowest price first, and the lowest price is not always the deepest. The contract's Best odds guaranteed example shows it on one horse at Ayr: a Betfair lay at 4.3 with 17.26 available, and a Smarkets lay at 4.4 with 75.93. A tool that sizes its lay against the first offer alone would ask the thinner exchange for more than it holds.

So loop over the `lay` array rather than taking its first entry. Take the lowest lay whose `available` covers the lay stake, and drop the pair when none does. [Why lay liquidity matters](https://oddsrelay.io/blog/why-lay-liquidity-matters) sets out the ways a product can handle a short lay.

Smarkets' [help centre](https://help.smarkets.com/hc/en-gb/articles/212729225-Matched-partially-matched-bets) says a bet placed without enough money at its price can stay unmatched or only partly matched, which leaves the second leg of an arb open. That is the failure `available` lets a tool catch before it shows the pair.

The back side carries no size. How much a bookmaker accepts on a price is set account by account, so your tool learns it from its users, never from a feed.

## One fixture, one market, one line

Two prices only form an arb when they are for the same outcome of the same market. Pair a back of Over 3.5 goals with a lay of Over 2.5, and a match with exactly three goals loses both legs.

On the matched boards every pair already shares one `event_id`, one market `key` and, on line markets, one `point`. The venues' own names for teams, markets and lines are reconciled before the reply is built, and that is the part of an arbitrage build that needs the most upkeep.

Raw keeps each venue's own names and market keys. The contract's two raw examples show what that means: one tennis match appears under two sport keys and two event ids, spelled "Tiana Tian Deng" at a bookmaker and "Tiana Deng" at an exchange. A tool that looks for arbs in [the raw feed](https://oddsrelay.io/products/raw-feed) makes that join itself, and [normalising odds across bookmakers](https://oddsrelay.io/blog/how-to-normalise-odds-across-bookmakers) covers the job layer by layer.

## Why do sure bets disappear?

A [sure bet](https://oddsrelay.io/glossary/surebet) disappears as soon as one leg moves: the bookmaker shortens its price, someone takes the money at the lay, or the event starts. Crossed prices are rare and they do not last, so the question for the data is how old each price was when the tool saw it.

Every reply carries the board's time in `meta.processed_at`, also sent as the `X-Processed-At` header. The matched boards add `meta.last_seen`, the last read of each venue and sport, so now minus a bookmaker's entry is the most its prices can have aged. Raw marks each market with `last_update`, when that venue's market last changed. Every product on sale meets a maximum update time of 10–20 s: how old a bookmaker's price can be when it reaches you.

Use those times as a gate before an alert goes out. A pair whose older leg has aged past what your users can act on is noise, whatever the prices say, and a large stake deserves a fresh look at both venues first. [The freshness guide](https://oddsrelay.io/docs/guides/freshness) turns `last_seen` into a price's age in code.

The prices are pre-match: the feed carries each market up to the start and nothing after it. A tool that needs prices once an event is under way takes them from a second source, and [pre-match and live odds data](https://oddsrelay.io/blog/live-vs-pre-match-odds-data) sets out the difference.

## How do you find arbitrage bets with an API?

You pull one board filtered to the venues your users hold accounts at, walk each market's outcomes, and run the crossing, liquidity and age checks in your own code on every reply. The loop, in order:

1. Price the call first. Add `quote=true` to any data request and the reply is its token cost, free. Narrow `sports`, `bookmakers`, `exchanges` and `markets` until the cost fits your plan ([tokens and quotes](https://oddsrelay.io/docs/guides/filters-and-tokens) shows how filters lower it).
2. Call with gzip. Send `Accept-Encoding: gzip` (curl: `--compressed`). Standard, dutching and raw answer `Accept-Encoding: identity`, which Python's `urllib` sends unless told otherwise, with a `400`.
3. Send each reply's `ETag` back as `If-None-Match`. An unchanged board answers `304` with no body and costs nothing, so polling only spends tokens when something moved ([polling with ETags](https://oddsrelay.io/docs/guides/conditional-requests)).
4. Run the checks on each changed reply: crossing, then `available` against the lay stake, then the age of the older leg.
5. Show each leg with its `link`, which opens the venue's own page for the event.

The same loop on raw walks pages with a cursor and follows changes with `updatedSince`, as [paging through raw odds](https://oddsrelay.io/docs/guides/walking-raw) shows. You can [sign up free](https://app.oddsrelay.io/signup) and run it on the Free plan before choosing a plan.

## What the feed leaves to your code

The feed sends prices, sizes and times. It flags no arb, works out no percentage and splits no stake. Raw applies no commission to any price.

That split suits a tool builder. Commission differs by exchange and sometimes by user, stakes depend on each user's accounts, and venues settle some markets under different rules ([matching bookmaker and exchange odds](https://oddsrelay.io/blog/matching-bookmaker-and-exchange-odds) shows where). Your code already holds all of that, so the arithmetic belongs there too. Our view is that arb logic should live in your code even where a supplier offers to compute it, because a ready-made arb carries someone else's commission and stake in its sums.

Value tools read the same prices with one more input: a fair price your own model sets, compared with each bookmaker's back price. The feed carries no starting or closing prices, so a tool that tracks CLV takes its closing prices from elsewhere. [Value betting vs arbitrage](https://oddsrelay.io/blog/value-betting-vs-arbitrage-data) sets the two tools' inputs side by side.

## Run the crossing check on your own sports

Pick one sport and the venues your users hold accounts at, quote the call, and run the three checks above over a day of replies. How many pairs cross, and how many of those survive the liquidity and age checks, tells you what the data can support before you build the rest. [Odds data for arbitrage and value-betting tools](https://oddsrelay.io/for/trading-tools) sets out the call behind each check.

## Sources

- [Smarkets Help Centre: Matched/partially matched bets](https://help.smarkets.com/hc/en-gb/articles/212729225-Matched-partially-matched-bets), checked 29 September 2026

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

Source: https://oddsrelay.io/blog/betting-arbitrage-data
