# Next-to-jump racing board

A local Python and Flask app for upcoming horse, harness and greyhound races. Select a country, choose a race, and display the supplied racecard and decimal win/place prices. The browser receives only normalized display data; the API key stays in Python.

## Use a coding agent

Open this extracted folder in your coding agent and ask: "Read AGENTS.md and README.md, set up the virtual environment, run the demo, and verify the four demo runners. Give me the local URL and checks you actually ran."

[AGENTS.md](AGENTS.md) contains the build brief, public source links, API rules and checks for extensions. [The guide's agent section](https://odds-api.net/guides/racing-racecard-odds-board-python#agents) has copyable setup, customisation and review prompts. Set API keys locally; keep them out of the AI chat.

## Run the demo

Use Python 3.10 or newer. From this extracted folder:

```sh
python -m venv .venv
# macOS / Linux
source .venv/bin/activate
# PowerShell
.venv\Scripts\Activate.ps1

python -m pip install -r requirements.txt
python app.py --demo
```

Open http://127.0.0.1:5055. Demo meetings, runners and prices are fictional. Demo mode makes no upstream API requests. Prices update every four seconds. Runner 3 is scratched, and runner 4 has no quote, so both edge cases can be inspected immediately.

## Connect a key

Stop the demo with Ctrl+C. Set an environment variable in the shell you use to launch Python:

```sh
# macOS / Linux
export ODDS_API_KEY="your_key"
# PowerShell
$env:ODDS_API_KEY="your_key"

python app.py
```

The optional `.env.example` documents the settings; the app does not automatically load `.env` files. A key with racing-odds and streaming access is required. If access is denied, use the account page to check the key's current entitlements.

## How the app works

1. `/racing/events` finds races in the next 24 hours. The Python client follows pagination and sorts by advertised start. Lists are cached for 60 seconds per country/race-type pair.
2. `/racing/events/{event_id}` returns race metadata and `racecard.runners` when supplied. A missing or incomplete racecard is labelled partial.
3. `/racing/events/{event_id}/odds` supplies a current snapshot, including its `resume` token. The app explicitly requests compact runner fields with source timestamps and unavailable rows.
4. `/racing/events/{event_id}/odds/stream?since=...` supplies complete bookmaker upserts and removals. Each upsert replaces that bookmaker's previous snapshot, so withdrawn rows do not persist accidentally.
5. A `resync` reloads event detail and the odds snapshot before opening another stream. A disconnected browser also starts with a new snapshot; it does not resume from a cursor whose state was lost.

Only the selected race is streamed. Closing or hiding the tab closes the browser stream; the server releases its upstream connection when it observes closure, at most after the next message or read timeout. Switching races can briefly show a reconnect state while the previous upstream stream closes. This example permits one active upstream stream per process. Keep one viewer tab open.

The page checks the race list once per minute. The selected racecard also refreshes once per minute while its price stream stays connected, so field changes can arrive even without a price delta. With one page of results, this is about 60 list requests plus 60 racecard requests per visible hour, plus an event-detail and snapshot pair per race selection or reconnect. Streams consume the plan's stream allowance and stream-hours. HTTP 401, 403 and 429 stop automatic retries and require the Refresh / reconnect button after resolving the cause.

## Runner and price rules

- Join by event ID and runner number, never by array index or horse name.
- Racecard runners remain visible without a bookmaker price. A missing quote is displayed as a dash, not a scratching.
- Mark withdrawals only from explicit runner status, scratching or removal fields. Do not calculate deductions from an absent price.
- Win and place are displayed separately. This app does not rank place prices, infer equal place terms, calculate exchange commissions, or validate available exchange size.
- Quote timestamps are source observations when supplied. Stream heartbeats indicate connectivity, not that a bookmaker changed a quote. The initial snapshot age is labelled separately.
- Racecards can be available before the documented price-collection window. An empty snapshot is a valid waiting state.

## Files

- `app.py`: localhost Flask routes, list cache and snapshot/stream bridge.
- `racing_api.py`: public HTTP client, SSE decoder, bookmaker state and explicit demo data.
- `templates/index.html`: accessible filters, race list and price table.
- `static/board.js`: selection, local times, updates and reconnect controls.
- `static/board.css`: responsive board styles.

Run this teaching example on localhost. A hosted, multi-user product needs authentication, a shared stream fan-out service, per-user access checks and usage controls. The Flask development server is intended for local use.

Public contract: https://odds-api.net/docs · Racing product: https://odds-api.net/racing/horse
