Quickstart notes
A tiny odds page with Node's built-in server
Browsers cannot call the odds API without showing your key. A thirty-line Node server fetches the board, caches it, and serves a table.
You want a web page that shows current odds, and the first instinct is to fetch from the browser. That leaks your key to every visitor, so the page has to be built on the server. The smallest version of that server is about thirty lines of Node with nothing to install: fetch the board, render a table, cache the answer for a minute.
The shape of it
The browser talks to your server; your server talks to the API. The key stays in the server's environment, sent in the x-portal-apikey header, exactly as the Node quickstart polling loop does it, checked 2026-10-26. The page that comes back to the visitor contains only prices.
Node 18 and later ship fetch and a built-in HTTP server, so the whole thing is one file:
import http from "node:http";
const HOST = process.env.ODDS_BASE_URL;
const KEY = process.env.ODDS_API_KEY;
let cache = { at: 0, rows: [] };
async function board() {
if (Date.now() - cache.at < 60_000) return cache.rows;
const url = HOST + "/kit/v1/markets?event_type=prematch&sport_id=1";
const r = await fetch(url, { headers: { "x-portal-apikey": KEY } });
if (!r.ok) return cache.rows; // serve the last good answer
const data = await r.json();
cache = {
at: Date.now(),
rows: data.events.map((ev) => {
const ml = (ev.periods?.num_0 || {}).money_line || {};
return `<tr><td>${ev.home} v ${ev.away}</td>` +
`<td>${ml.home ?? ""}</td><td>${ml.draw ?? ""}</td>` +
`<td>${ml.away ?? ""}</td></tr>`;
}),
};
return cache.rows;
}
http.createServer(async (req, res) => {
const rows = await board();
res.setHeader("content-type", "text/html; charset=utf-8");
res.end(`<table>${rows.join("")}</table>`);
}).listen(8080);
Run it with the two environment variables exported, and port 8080 answers with a table of the prematch soccer board.
The cache is the quota
Every page view that reached the API would spend your allowance. One call a minute is 1,440 calls a day, and the free key's daily budget is far smaller than that, as the quickstart's notes make plain, checked 2026-10-26. The one-minute cache in front of the fetch is what makes the page viable: a hundred visitors in a minute cost one call.
The same cache doubles as the failure mode. When the API answers an error, the server returns the last good board instead of a blank page, and the timestamps in your logs tell you how stale it got.
What the page must never contain
The HTML you serve is public by definition. Prices belong in it. The key, the base URL with any embedded credentials, and your cursor state do not. If you ever view-source the page and see anything but the table, the boundary broke.
This also settles the refresh question: the page reloads, the server serves the cache, the poller's schedule stays the single clock that spends quota.
Where this design stops
One process, one sport, one table, a minute of staleness. That covers a status page, a home dashboard, a quick check on your phone. When the page needs to react in seconds rather than minutes, the work moves upstream: poll faster or read the drop stream in the server, and let the page stay exactly this dumb. The Node quickstart is where that loop begins.
A thirty-line server is not a product. It is the smallest honest answer to the question every web project asks first: how do I show these prices without showing my key.