Odds for the same match vary from bookmaker to bookmaker — sometimes by a lot. A comparison tool ("line shopping" app) surfaces the best available price for each outcome across every book, which is one of the most genuinely useful things you can build with a sports data API. In this tutorial we'll build one end to end: fetch odds, normalize them, compute implied probability, and highlight the best price per outcome.
What We're Building
A tool that:
Pulls odds for a fixture across multiple bookmakers
Converts everything to a consistent decimal format
Calculates implied probability and the market's overround (vig)
Highlights the best price for each outcome
Flags a "no-vig fair odds" line so you can see true market value
Step 1: Fetch Normalized Odds
Since bookmakers each publish in their own format, we want a source that's already normalized. Using Orbistats' Odds API:
javascript
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.orbistats.com/v1";
async function getOdds(fixtureId, market = "1x2") {
const res = await fetch(
${BASE_URL}/football/odds?fixture_id=${fixtureId}&market=${market},
{ headers: { Authorization: Bearer ${API_KEY} } }
);
if (!res.ok) throw new Error(Odds request failed: ${res.status});
return res.json();
}
Expected shape (per the Sports Data API docs pattern):
json
{
"fixture_id": 1958466,
"market": "1X2",
"bookmakers": [
{ "name": "BookA", "home": 1.95, "draw": 3.40, "away": 3.80 },
{ "name": "BookB", "home": 2.05, "draw": 3.30, "away": 3.60 },
{ "name": "BookC", "home": 1.90, "draw": 3.50, "away": 4.00 }
]
}
Step 2: Find the Best Price Per Outcome
This is the actual "line shopping" logic — for each outcome, find which book pays the most:
javascript
function findBestPrices(oddsResponse) {
const outcomes = ["home", "draw", "away"];
const best = {};
outcomes.forEach((outcome) => {
let bestBook = null;
let bestPrice = 0;
oddsResponse.bookmakers.forEach((book) => {
if (book[outcome] > bestPrice) {
bestPrice = book[outcome];
bestBook = book.name;
}
});
best[outcome] = { price: bestPrice, book: bestBook };
});
return best;
}
Even in the small example above, notice BookB pays best on home (2.05) but worst on away (3.60) — a bettor manually checking one book at a time would never catch that BookC pays 4.00 on the same outcome. That gap is the entire value proposition of a comparison tool.
Step 3: Calculate Implied Probability
Convert each price to implied probability so you can reason about value, not just raw payout:
javascript
function impliedProbability(decimalOdds) {
return 1 / decimalOdds;
}
Step 4: Calculate the Overround (Vig) Per Book
Every bookmaker's three prices will sum to more than 100% implied probability — that excess is their margin. Comparing it across books tells you which one is cheapest to bet at:
javascript
function calculateOverround(book) {
const total =
impliedProbability(book.home) +
impliedProbability(book.draw) +
impliedProbability(book.away);
return {
book: book.name,
overround: ((total - 1) * 100).toFixed(2) + "%",
};
}
function compareMargins(oddsResponse) {
return oddsResponse.bookmakers.map(calculateOverround);
}
Step 5: Compute No-Vig "Fair" Odds
Once you've picked the best price per outcome, you can also compute what the true market-implied probability looks like with the vig stripped out — useful for spotting genuine value rather than just a good raw number:
javascript
function noVigFairOdds(oddsResponse) {
const totalImplied = ["home", "draw", "away"].reduce((sum, outcome) => {
const best = Math.max(...oddsResponse.bookmakers.map((b) => b[outcome]));
return sum + impliedProbability(best);
}, 0);
const fairOdds = {};
["home", "draw", "away"].forEach((outcome) => {
const best = Math.max(...oddsResponse.bookmakers.map((b) => b[outcome]));
const fairProb = impliedProbability(best) / totalImplied;
fairOdds[outcome] = (1 / fairProb).toFixed(2);
});
return fairOdds;
}
Step 6: Render the Comparison Table
javascript
function renderComparisonTable(oddsResponse) {
const best = findBestPrices(oddsResponse);
const container = document.getElementById("odds-table");
let html = ;
<table>
<thead>
<tr><th>Bookmaker</th><th>Home</th><th>Draw</th><th>Away</th></tr>
</thead>
<tbody>
oddsResponse.bookmakers.forEach((book) => {
html += <tr>;
<td>${book.name}</td>
<td class="${book.home === best.home.price ? "best-price" : ""}">${book.home}</td>
<td class="${book.draw === best.draw.price ? "best-price" : ""}">${book.draw}</td>
<td class="${book.away === best.away.price ? "best-price" : ""}">${book.away}</td>
</tr>
});
html += "";
container.innerHTML = html;
}
css
.best-price {
background: #1e4d2b;
color: #4ade80;
font-weight: 600;
}
Step 7: Keep It Live
Odds move constantly, especially close to kickoff. Instead of refetching on a timer, hook into Orbistats' WebSocket API for push-based updates:
javascript
const ws = new WebSocket(wss://api.orbistats.com/v1/live?token=${API_KEY});
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === "odds.changed") {
renderComparisonTable(update.data);
}
};
Or if you'd rather not hold a socket open client-side, Webhooks can push odds.changed events to a backend endpoint instead, and you broadcast to connected clients from there.
Why This Matters Beyond Betting Apps
Line shopping logic isn't just for consumer betting tools — the same "compare implied probability across sources, flag the outlier" pattern shows up in trading dashboards, arbitrage detection, and pricing-model validation. If you want to go deeper on the math behind the implied-probability and no-vig calculations used here, TheStatsAPI's implied probability calculator and odds-api.io's margin calculator both walk through worked examples with the same formulas.
Wrapping Up
The core of any odds comparison tool is three things: normalize prices into one format, compute implied probability so numbers are comparable, and highlight the best price per outcome. Everything past that — live updates, no-vig fair odds, arbitrage flags — builds on that same foundation. Full odds endpoint reference is in the API reference docs if you want to extend this to more markets (spreads, totals, player props).

Top comments (0)