BlogArbitrage and value

Pairing back and lay odds across UK bookmakers

To pair back and lay odds across UK bookmakers, take one outcome from a matched board, keep the back offers from the books your user holds, and give each one the lowest lay whose liquidity covers the lay stake.

The board has already agreed the fixture, market and line. What it can't choose is the pair, because that depends on your user.

This post is for teams building arbitrage and trading tools, who already know a back bet from a lay price. Matching bookmaker and exchange odds covers whether two prices are the same bet. This post starts one step later, with which of those prices share a row in your tool.

How do you pair a bookmaker back price with an exchange lay price?

Take both from the same outcome, because each outcome on OddsRelay's standard board carries a back list and a lay list. On a line market, each point has its own outcome, so Over at 2.5 goals never shares a list with Over at 3.5. Three back offers and two lay offers make six possible pairs:

GET /v2/odds/standard (one h2h outcome) · example

{
  "name": "Arsenal",
  "back": [
    { "bookmaker": "sky_bet", "price": 2.95, "link": null },
    { "bookmaker": "william_hill", "price": 2.9, "link": null },
    { "bookmaker": "paddy_power", "price": 2.88, "link": null }
  ],
  "lay": [
    { "exchange": "matchbook", "price": 2.94, "available": 38.5, "link": null },
    { "exchange": "smarkets", "price": 2.98, "available": 412, "link": null }
  ]
}

back comes best price first and lay lowest price first, so back[0] and lay[0] look like the answer. Often they're the wrong pair. Here, a user with no Sky Bet account can't take the first back offer, and a back stake of 100 asks more than the 38.5 behind the first lay can take.

Which lay do you pair when several exchanges price the same outcome?

Pair the lowest lay that can take the lay stake, and drop the back offer if none can. The board draws its lays from Betfair, Smarkets and Matchbook, and every lay offer carries available. The list is already in price order, so the first offer that fits is the one you want. The lay stake changes with the back price and each exchange's commission, so work it out per offer:

Your code: rows from one outcome · example

// Customer-side code. The books, stake and commission rates are your user's own.
// One rate per exchange: a missing rate gives NaN and skips that offer.
const books = new Set(["william_hill", "paddy_power"]);
const commission = { smarkets: 0.02, matchbook: 0.02 };
const backStake = 100;

const layStake = (back, lay) =>
  (backStake * back.price) / (lay.price - commission[lay.exchange]);

const rows = outcome.back
  .filter((back) => books.has(back.bookmaker))
  .flatMap((back) => {
    const lay = outcome.lay.find(
      (l) => l.available !== null && l.available >= layStake(back, l),
    );
    return lay ? [{ back, lay, layStake: layStake(back, lay) }] : [];
  });

Run that over the outcome above and the books check drops Sky Bet, the liquidity check drops Matchbook's lay, and two rows are left:

Back offerLay stake on MatchbookPairs withLay stake there
William Hill at 2.999.32, too much for its liquiditySmarkets at 2.9897.97
Paddy Power at 2.8898.63, too much for its liquiditySmarkets at 2.9897.30

Drop the back stake to 25 and both rows move to Matchbook, because a lay stake near 25 fits inside Matchbook's 38.5, and Matchbook's price is lower. Nothing in the reply changed, which is why these rows belong in your code and not in a cache of the board. Why lay liquidity matters sets out what a scanner can do with a short lay besides dropping it.

Commission stays in your code too, because each user pays their own rate and that rate can change which offer fits.

One best row hides the rest

Keep a row for every back offer that passes, instead of one best row per outcome. The best price across the market tells a user little if the next-best is the one at a book they can still bet with. Rows also make your screen easy to filter, and when a user loses an account, that book drops out of books with no change to anything else.

The bookmakers and exchanges filters narrow the books on the way in, and on the standard board a filtered call costs less. They still can't replace the check in your code when one reply serves users who hold different accounts. A filter also drops any outcome left with no back offer or no lay offer, as the matched-board filters guide explains.

What does a scanner do with a paired row?

A scanner runs its own sums on each row and decides what to show. The board flags nothing and computes nothing, so each of these is your code:

  • The crossing check. Whether the back price beats the lay once commission comes off. Arbitrage data has the line of code, and neither row above crosses.
  • The pair's age. meta.last_seen gives the last read of each venue and sport. A row is at most as old as the older of its two reads, so check both before an alert goes out.
  • The way to each leg. Each offer's link opens that venue's page, the event's own where we have one and otherwise a sport or home page, and it can be null, so check it before you send a user there.
  • The back side's limit. Back offers carry no size. What a bookmaker accepts is set per account, so only your user's own history can tell you.

A value tool swaps the crossing check for a fair price of its own. Value betting vs arbitrage sets the two side by side.

Pair one market on your users' books

Price a call for one sport and market with quote=true, which costs nothing, then run the loop above with your users' books, stakes and rates. Count how many rows survive at each stake. Odds data for trading tools sets out the calls. A scanner other people use runs on Enterprise, and so does one a business keeps for itself.

Related posts

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