Skip to content
OddsRelay

How to display odds data in your app

You have the odds data; now you need to render it. A practical guide to mapping the response to rows, formatting decimal prices, showing the matched pair, and keeping it fresh.

James6 min read

Displaying odds data is mostly four steps: map the response's events, markets and selections to UI rows, format the decimal prices so a user reads them cleanly, show the matched back/lay pair with its rating and qualifying loss, and refresh on the feed's cadence rather than on every render. Get those four right and the interface stays honest: a user never sees a stale price dressed up as a current one. This guide walks each step with the field names OddsRelay actually returns.

How do you map an odds response to UI rows?

You flatten the nested envelope into a flat list of rows, one per selection. An odds response nests three levels: an event holds one or more markets, a market holds selections, and a selection carries the price. Most odds tables render one row per selection, so your first job is to walk that tree and produce a flat array your table component can loop over.

Keep the mapping thin. Read the fields you display, carry a stable key for each row so the framework can diff efficiently, and avoid reshaping the data into a bespoke model you then have to maintain. The anatomy of a response covers the full envelope; for display you mostly touch event, market, selection, the back block and the lay block.

Here is a single matched row, trimmed to the fields a table actually uses (illustrative, not live data):

One matched row · illustrative shape
{
  "event": "Arsenal vs Chelsea",
  "market": "match_odds",
  "selection": "Arsenal",
  "back": { "bookmaker": "bet365", "odds": 2.10 },
  "lay":  { "exchange": "betfair", "odds": 2.14, "liquidity": 1840 },
  "rating": 98.1,
  "qualifying_loss": -0.12
  // ... region, feed_type and freshness fields elided
}

Each field maps to one visible element. event and market become the row's context, often a header or grouping label. selection is the runner name. back.bookmaker renders as a book slug or logo (bet365 here), and back.odds is the headline price. The lay block gives you the exchange name, the lay price, and the liquidity you can surface as an availability hint. rating and qualifying_loss are the derived fields that tell a user how good the pair is.

How do you format a decimal price without losing precision?

Format for display, but never mutate the underlying value. Decimal odds like 2.10 or 2.62 should read consistently, usually to two decimal places, so a column of prices lines up. The trap is rounding the stored number: keep the exact value the feed sent for any comparison or calculation, and format only the copy you render.

  • Store the raw number, format a string. Compute on 2.14; display "2.14". Never overwrite the stored value with its rounded form.
  • Fix the decimal places for alignment. Two places reads cleanly for most markets; a price of 2.1 shown as 2.10 keeps the column tidy.
  • Watch floating-point drift. qualifying_loss values like -0.12 can accrue tiny errors if you re-derive them; prefer the value the feed already computed.
  • Localise the separator, not the maths. Some regions expect a comma decimal separator; change the display, never the number you compare.

The same rule applies to the derived fields. A rating of 98.1 and a qualifying_loss of -0.12 arrive already computed, so display them as given rather than recalculating from the two odds. Re-deriving on the client invites rounding differences that make your figures disagree with the feed's, which erodes trust faster than a missing number.

How do you show the matched pair clearly?

Show the back and lay side by side, then the derived verdict, so a user reads the opportunity in one glance. A matched row is a relationship between two prices, not a single figure. Present the back price with its bookmaker, the lay price with its exchange, and let the rating and qualifying_loss do the summarising. This is what OddsRelay delivers that a raw price list cannot: bet365 back odds already paired against Betfair, Smarkets or Matchbook lay odds, rated for you.

FieldUI elementNote
back.bookmaker + back.oddsBook logo and the back priceThe price the user backs at (e.g. bet365 at 2.10)
lay.exchange + lay.oddsExchange label and the lay priceThe current exchange price to lay at
lay.liquidityAvailability hint or badgeHow much is layable now; low liquidity is worth flagging
ratingThe headline quality scoreA rating near 100 is a near-perfect match
qualifying_lossExpected cost of the qualifying betUsually small and negative; show it plainly
Each derived field earns a distinct UI element so the pair reads at a glance.

Group the table sensibly. Rows sort well by rating, and users often filter by market or by book. Because the feed covers 60+ UK books with bet365 included, and three exchanges (Betfair, Smarkets and Matchbook) on the lay side, your interface should let a user narrow to the book or market they care about rather than scrolling a wall of rows.

How do you indicate freshness so a user never trusts a moved price?

Attach an age to every price and show it, then visibly de-emphasise anything past a threshold. Odds move, and a price that was accurate ten seconds ago may not be now. The response carries freshness fields precisely so you can render this: display a last-updated time, and when a row exceeds a staleness limit, grey it out, badge it, or drop it from the actionable set. A stale price presented as current is worse than a gap, because it looks usable and is not.

Set the threshold to match the feed's cadence. With pre-match polling on roughly a few-second cycle, a row a few seconds old is normal and a row minutes old signals a problem worth surfacing. Tie your staleness banding to that cadence rather than to an arbitrary number.

How often should the display refresh?

Refresh on the feed's cadence, not on every render. Re-fetching inside a render loop wastes requests and can hammer the endpoint; instead, poll on a timer that matches the feed's update rate and let your UI re-render from the last fetched snapshot. With pre-match polling on roughly a few-second cycle, a client poll every few seconds keeps prices current without redundant calls.

Use conditional requests so an unchanged board costs almost nothing. The feed supports ETag and returns 304 Not Modified when the data hasn't moved, so a poll that finds no change transfers no body and your rendered state stays put. This is the core of polling efficiently: a steady timer plus ETag/304 gives you fresh prices and a light footprint at once.

  • Poll on a timer, decoupled from render, at roughly the feed's cadence.
  • Send the ETag from the last response; on 304 keep the current rows and skip the re-parse.
  • Re-map only on a `200`, then update the freshness stamps your staleness banding reads.

Putting it together

The pipeline is short: fetch on a timer, map the envelope to flat rows, format decimals for display while keeping the raw values, render the matched pair with its rating and qualifying_loss, and band each row by freshness so nothing stale looks live. The field names are the same ones in the API docs, and you can see which books and markets are populated right now on the coverage dashboard.

If you would rather render real rows than mock ones, a free trial gives you the full UK feed, bet365 included and matched against exchange lay prices, so you can wire your table against live shapes. It powers a leading UK matched-betting platform today, and the coverage is checkable before you commit.

Fundamentals

Written by

James

Founder, OddsRelay

James is the founder of OddsRelay — the odds-data feed behind matched betting, arbitrage and odds-comparison products: 60+ UK bookmakers with bet365 included, matched against exchange lay prices and delivered as one clean, documented API. He writes here about how that data layer actually behaves — coverage, matching, freshness and the trade-offs — from the side that builds and runs it. The same feed powers a leading UK matched-betting platform today.

Part of the Fundamentals cluster

What is an odds API? A 2026 guide for builders

18+ · Data product for licensed operators. Please gamble responsibly.