Build a next-to-jump racing board with Python
Find the next races, load their racecards and follow bookmaker prices in a small Python app. Download the complete project and try it with fictional demo data first.
In this guide
What you’ll build
A local browser board with upcoming races, country filters, local start times, racecards and streaming win/place prices. The download works with horse, harness and greyhound racing.
Preview the finished board
| Runner | Example Book A | Example Book B | ||
|---|---|---|---|---|
| Win | Place | Win | Place | |
| 1Example RunnerDemo Jockey | 3.50 | 1.65 | 3.60 | 1.75 |
| 2Sample StarDemo Jockey | 5.20 | 2.05 | 5.10 | 2.05 |
| 3Withdrawn RunnerScratched | Withdrawn from the field | |||
| 4Unpriced RunnerDemo Jockey | — | — | — | — |
Use the horse racing API page for the product overview and the public endpoint reference for the complete contract.
Before you start
- Install Python 3.10 or newer.
- Have a terminal or PowerShell window ready.
- For real prices in step 2, use an odds-api.net key with racing-odds and streaming access. The demo runs without a key.
Build with an AI agent
Give a coding agent the working project and a specific task. The agent build brief (Markdown) includes setup commands, source links, API rules and checks. It is also included as AGENTS.md in the project ZIP.
- Open your coding agent in the folder where you want the project.
- Copy the prompt below. An agent with web and terminal tools can fetch the project and run the demo.
- If your agent cannot download files, download and extract the ZIP yourself, then open that folder in the agent and use the same prompt.
Set up and run the odds-api.net next-to-jump racing board in this workspace.
Read the build brief first:
https://odds-api.net/static/examples/racing-board/AGENTS.md
Use the supplied project, downloading and extracting it if it is not already here:
https://odds-api.net/static/downloads/racing-board-python.zip
Preserve existing files. Read the project's AGENTS.md and README.md, create a Python virtual environment, install requirements and run python app.py --demo on localhost. Run the available checks and inspect the board if you have browser tools. Confirm the four demo runners, separate win/place prices, scratched runner and unpriced runner.
Keep API keys server-side. Start without a key; I will set ODDS_API_KEY locally when I want real data. Preserve the project's runner matching, full-bookmaker updates, resync, request limits and single-stream behavior.
Complete the setup, then give me the local URL, commands to restart it and checks you actually ran. Tell me if a tool limitation prevents a check.Expected result: A running local demo you can inspect, with no API key or API credits needed. Set your key locally using step 2 when you want real prices; keep it out of the AI chat.
Prompts to customise and check the board
Once the demo works, replace the bracketed task with the change you want.
Read AGENTS.md and README.md in this racing-board project. Make this change: [describe the feature or design change].
Start from the existing working app. Preserve runner-number joins, explicit scratchings, separate win/place prices, snapshot-first reconnects and one active upstream stream. Check the public OpenAPI contract before using new fields or endpoints. Develop with demo or mocked data, keep secrets in Python, and do not increase polling silently.
Implement the change and run relevant checks. Show me the result locally, list changed files and report any unverified behavior.Review this racing board against its AGENTS.md and the public odds-api.net contract. Use demo or mocked responses.
Check runner matching, unpriced versus scratched runners, full-bookmaker replacement and removal, snapshot reload after resync, stale-price labels, 401/403/429 retry stops, and stream cleanup when switching races or hiding the tab. Inspect narrow and wide layouts and keyboard controls if browser tools are available.
Fix issues you can reproduce and add regression checks for changed state or streaming logic. Report what passed, what you changed and what you could not verify. Do not claim live API validation unless you actually ran it.For agents fetching this guide, the plain Markdown brief and OpenAPI JSON can be read directly without rendering this page or clicking the copy buttons.
1. Run the demo
Download the Python project, extract it and open a terminal inside racing-board. Then create a virtual environment and install the dependencies.
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python app.py --demopython -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
python app.py --demoCheck: Open http://127.0.0.1:5055. You should see four demo runners and prices changing every four seconds. Runner 3 is scratched; runner 4 has no quote. No API credits are used.
Files in the project
The download contains app.py, racing_api.py, the HTML template, browser JavaScript, CSS, requirements and a README. Read the source individually: Flask app, API client and state, browser code.
2. Connect your API key
Stop the demo with Ctrl+C. Use an odds-api.net key with racing-odds and streaming access, set it in the same terminal and restart without --demo.
export ODDS_API_KEY="your_key"
python app.py$env:ODDS_API_KEY="your_key"
python app.pyCheck: Choose a race. Its supplied runners and bookmaker prices should replace the fictional demo field.
The app reads environment variables directly. It does not load a .env file automatically. The key stays in Python; the browser uses local /api/races routes. Check your key's access on the account page if the API returns 403.
Keep the app bound to localhost. This example reads racecards and pre-jump prices. It does not place bets, provide in-play prices, settle results or supply historical form.
3. Load racecards and prices
- Discover races. Request
GET /v1/racing/eventswithrace_type,race_country,start_fromandstart_to. Follownext_cursor, sort byrace_start_timeand retainevent_id. - Load the racecard. Request
GET /v1/racing/events/{event_id}. Readdata.racecard.runnerswhen supplied. Preserve the field's completeness flag; priced runners alone do not establish a complete field. - Take a snapshot. Request
GET /v1/racing/events/{event_id}/odds. The app asks for compact runner fields withinclude_raw_payload=false, source timestamps and unavailable rows. Save the response'sresume. - Continue from the snapshot. Open
GET /v1/racing/events/{event_id}/odds/streamwithsince=resumeandcatchup=true. Python decodes SSE and sends only normalized display data to the browser.
A racecard can appear before bookmaker price collection opens. For eligible scheduled races, the documented collection window begins 24 hours before the advertised start. An empty odds snapshot is a waiting state; it does not mean that the field is scratched. The discovery list includes all returned statuses and filters out finished races, so an open race is not accidentally excluded by a hardcoded fetching filter.
The event list refreshes once per minute and the app follows up to 40 upcoming races. The selected racecard also refreshes once per minute while its price stream remains open, so field changes do not depend on a price delta. Country and race-type filters apply at discovery. Timezone changes happen in the browser with Intl.DateTimeFormat, so daylight saving changes follow the selected IANA timezone.
Inspect the first race from Python
Save this beside app.py as inspect_race.py and run it with the same environment variable. It uses the download's HTTP client and state model, so the inspection follows the same field and price rules as the browser board.
import os
from racing_api import RacingAPI, OddsState
api = RacingAPI(os.environ["ODDS_API_KEY"])
races = api.events("AU", "horse-racing")
if not races:
print("No upcoming races returned.")
else:
event_id = races[0]["event_id"]
state = OddsState(api.detail(event_id), api.snapshot(event_id))
for runner in state.view()["runners"]:
print(runner["runner_number"], runner["runner_name"],
runner["scratched"], runner["prices"])4. Match runners to prices
Join by event_id + runner_number. A runner's display name may have a number prefix at one bookmaker and no prefix at another. Array position also changes when a runner disappears from a feed.
The downloaded app starts with the supplied racecard and joins each bookmaker's runner rows onto it. A missing quote becomes a dash. It marks a withdrawal only from explicit is_scratched, runner_status or removal_date evidence, and suppresses prices for that runner.
Each upsert replaces one complete bookmaker snapshot. It is not a patch to one horse's price. A remove deletes that bookmaker's snapshot. Keeping this distinction prevents old quote rows from lingering after an update.
Win and place prices are displayed separately. The board does not label a place price as “best” without equivalent place terms, or compare exchange offers without accounting for available size and commission.
5. Keep updates in sync
There are three upstream message types:
delta: replace or remove the named bookmaker snapshot, then redraw the field.heartbeat: record that the connection is alive. A heartbeat does not refresh a bookmaker quote's source timestamp.resync: close that stream, reload event detail and the current odds snapshot, then connect using the new snapshot's resume token.
After a network interruption the browser uses a bounded reconnect delay. The Python bridge loads fresh state first. It does not save a cursor by itself and assume the matching state still exists. A fresh current snapshot is also the source after a resync.
The bridge follows this snapshot-first loop. The complete app adds browser delivery, rate-limit handling and connection cleanup around it.
from racing_api import OddsState
def follow_race(api, event_id):
while True:
state = OddsState(api.detail(event_id), api.snapshot(event_id))
yield state.view()
for event, payload in api.stream(event_id, state.resume):
if event == "resync":
break # Reload current detail and snapshot in the outer loop.
if event == "delta":
state.apply(payload)
yield state.view()
else:
return # The caller reconnects with bounded backoff.The board keeps quote timestamps visible and marks displayed prices as potentially stale while disconnected. It stops automatic retries for rejected keys, denied racing access and API limits; resolve the cause, then use Refresh. Hiding the tab pauses its stream.
6. Check request usage
With one result page, the one-minute list and selected-racecard refreshes use about 120 requests per visible hour: 60 for the list and 60 for the selected racecard. Each selected race or reconnect adds one event-detail request and one snapshot request. Additional pagination adds requests; active streams also consume the plan's stream allowance and stream-hours.
The example caches lists for 60 seconds and permits one upstream price stream per Python process. Keep one viewer tab open. When switching races, the previous upstream connection may take until the next message or timeout to close; the board retries while that connection releases.
For a hosted product, replace the local development server with authenticated serving, per-user entitlements and shared stream fan-out. The local example deliberately keeps that deployment work out of the tutorial. See pricing and quotas before changing polling frequency.
7. Test the board
- Run demo mode and confirm the scratched and unpriced runners remain distinct.
- Change country, race type and timezone; check the new list and displayed start times.
- Connect your key and compare one displayed runner and price with the corresponding API response.
- Interrupt the connection. Confirm that prices are marked potentially stale and that reconnection starts with fresh state.
- Hide and return to the tab. Confirm the stream pauses and the race list refreshes.
The runnable example and its download are the original material behind this guide. Fictional demo fixtures are labelled; they are not a racing dataset or a measurement of bookmaker coverage.
Contract and source files
Racing board questions
Can I try the racing board without an API key?
Yes. Run python app.py --demo. Demo meetings, runners and changing prices are fictional, and demo mode makes no upstream requests.
Does this work for harness and greyhound racing?
Yes. The race-type filter uses horse-racing, harness-racing or greyhound-racing. Country filters are AU, NZ, GB and IE.
Does a missing bookmaker price mean a horse is scratched?
No. The app displays a dash for missing prices and uses explicit scratching, withdrawal or removal fields to mark a non-runner.
What happens when the stream disconnects?
The app marks displayed prices as potentially stale, reconnects with backoff and loads a fresh racecard and odds snapshot before accepting new deltas. A resync also reloads the current snapshot.
Which key do I need for real racing prices?
Use an odds-api.net key with racing-odds and streaming access. API keys stay in the Python process; the browser connects only to the local Flask app.