Arbitrage data: what an arbitrage tool needs
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 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).
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 lists the boards). Here is one pair from the contract's own examples:
{
"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:
// 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 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). 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.
{
"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. 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 works through the stake on each leg.
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 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 sets out the ways a product can handle a short lay.
Smarkets' help centre 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 makes that join itself, and normalising odds across bookmakers covers the job layer by layer.
Why do sure bets disappear?
A sure bet 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 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 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:
- Price the call first. Add
quote=trueto any data request and the reply is its token cost, free. Narrowsports,bookmakers,exchangesandmarketsuntil the cost fits your plan (tokens and quotes shows how filters lower it). - Call with gzip. Send
Accept-Encoding: gzip(curl:--compressed). Standard, dutching and raw answerAccept-Encoding: identity, which Python'surllibsends unless told otherwise, with a400. - Send each reply's
ETagback asIf-None-Match. An unchanged board answers304with no body and costs nothing, so polling only spends tokens when something moved (polling with ETags). - Run the checks on each changed reply: crossing, then
availableagainst the lay stake, then the age of the older leg. - 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 shows. You can sign up free 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 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 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 sets out the call behind each check.
Sources, checked 29 September 2026
Related posts
- Why lay liquidity matters in a matched odds feedA lay price with no money behind it cannot be taken. How to check a pair against your stake.
- Dutching data explainedThe stake arithmetic behind dutching, and a feed that sends the legs already grouped.
- How to normalise odds across bookmakersBookmakers name the same fixture and selection differently. Where matching goes wrong, and how to join them.
18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.