Your application tracks hundreds of odds. A few prices change. Should it fetch the entire board again?
Full snapshots are convenient for a small prototype. An incremental feed offers another approach: load the starting state, then apply incoming changes to a local copy.
In this tutorial, we’ll build a small terminal odds monitor with Python and PulseScore Stream API. We’ll separate three responsibilities: receiving data, maintaining local state and displaying prices.
We provide the API feed and reference client through PulseScore. The monitor is an application you run yourself.
Disclosure: We build PulseScore. This tutorial uses our Stream API, currently in Beta. Product details were checked on October 3, 2026.
What we’ll build
Our application will:
- Subscribe to one live soccer stream.
- Maintain the current event board using our Python reference client.
- Print up to eight selections when the initial snapshot is complete.
- Refresh the displayed prices as updates arrive.
Our reference client handles snapshots, patches, removals and connection recovery. The tutorial code handles the display.
What you need
- Python 3.10 or newer.
- A PulseScore account with streaming access: PRO, MAX or ULTRA.
- An API key stored in your environment.
The free REST allowance does not include streaming access.
Live events must be available for the selected bookmaker and sport. An empty board is a valid response.
1. Set up the project
Create a project directory, then set up a virtual environment:
mkdir odds-monitor
cd odds-monitor
python -m venv .venv
Activate it on macOS or Linux:
source .venv/bin/activate
On Windows, use PowerShell:
.\.venv\Scripts\Activate.ps1
Install the WebSocket dependency:
python -m pip install "websockets>=12"
Download our Python reference client and save it as apply.py in your project directory:
curl --fail --location https://pulsescore.net/stream-clients/apply.py --output apply.py
On Windows, you can use curl.exe for the same command.
Review the downloaded client before running it. Keep a reviewed copy with your project so upstream changes do not silently alter your application.
Set your API key on macOS or Linux:
export PULSESCORE_API_KEY="YOUR_API_KEY"
Or in PowerShell:
$env:PULSESCORE_API_KEY="YOUR_API_KEY"
Replace the placeholder locally. Keep your actual key out of source control, screenshots and recordings.
2. Build the display layer
Save the following as dashboard.py, beside apply.py:
import asyncio
import os
import sys
import time
from apply import StreamConnection
STREAM = "bet365/live/soccer"
ready = False
last_render = 0.0
def rows_from(events):
rows = []
for event in events:
match = f"{event.get('home', '?')} vs {event.get('away', '?')}"
for market in event.get("markets", []):
if market.get("isActive") is False:
continue
for selection in market.get("selections", []):
price = selection.get("odds")
if price is None or selection.get("isActive") is False:
continue
outcome = selection.get("canonicalOutcome") or selection.get("rawName", "?")
market_name = market.get("canonicalMarket") or market.get("rawName", "?")
line = selection.get("line", market.get("line"))
period = market.get("period", "?")
rows.append((match, market_name, period, outcome, line, price))
if len(rows) == 8:
return rows
return rows
def on_frame(stream, kind, client, raw):
global ready, last_render
if stream != STREAM:
return
if kind == "snapshot":
ready = False
return
if kind in ("snapshot_end", "resumed"):
ready = True
if not ready or kind not in ("snapshot_end", "resumed", "delta"):
return
now = time.monotonic()
if kind == "delta" and now - last_render < 1:
return
board = client.board(stream)
if board is None:
return
last_render = now
print("\nMATCH | MARKET | PERIOD | OUTCOME | LINE | ODDS")
rows = rows_from(board.to_list())
for row in rows:
print(" | ".join(str(value) for value in row))
if not rows:
print("No priced selections in this stream right now.")
def main():
connection = StreamConnection(
url="wss://api.pulsescore.net/api/stream/ws",
key=os.environ["PULSESCORE_API_KEY"],
streams=[{"bookmaker": "bet365", "kind": "live", "sport": "soccer"}],
on_frame=on_frame,
on_state=lambda state, info: print(f"Feed state: {state}", file=sys.stderr),
max_retries=5,
)
try:
asyncio.run(connection.run())
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
Run it:
python dashboard.py
The script prints successive views in your terminal. It does not start a web server or create a browser dashboard.
Each displayed row contains:
- The match.
- The market type.
- The period.
- The outcome.
- The handicap or total line, when available.
- The current odds.
Markets and selections explicitly marked inactive are omitted from this small display.
3. Understand the snapshot-to-delta workflow
The starting board can arrive in several snapshot frames.
Our reference client assembles those frames into local state. The display waits for snapshot_end before showing that state as a complete board.
After initialization, incoming deltas update the local board. The display reads the resulting state rather than trying to interpret each patch independently.
This separation keeps the presentation code small:
- Our reference client understands the protocol.
- The local board holds the latest applied state.
- The display chooses which rows to show.
A replacement snapshot temporarily stops rendering until the replacement is complete.
4. Separate feed updates from display updates
The example caps the display at eight selections and limits rendering triggered by deltas to approximately once per second.
That is a presentation choice. It does not prevent our reference client from applying incoming changes.
Our Stream API batches changes roughly once per second. This cadence is not a guarantee that every bookmaker price reaches your application within one second.
For a larger application, move expensive rendering or database work away from the receive callback. Your message reader should remain responsive while other parts of the application process the data.
5. Handle empty and interrupted feeds
If the selected stream contains no priced selections, the monitor prints:
No priced selections in this stream right now.
An empty board does not automatically mean the connection failed. There may be no live events for the selected source and sport.
Connection state is printed separately to help distinguish the feed’s connection status from its contents.
For a production interface, make those distinctions visible to users. Cached prices shown during a disconnection should be labelled as potentially stale.
Our reference client handles connection recovery, but your application still decides how to communicate reconnecting, rebuilding and stopped states.
Before expanding the prototype
Once the single-stream monitor works, you can build a more useful view around the same local state.
When adding sources or features:
- Label every price with its source.
- Keep period and line information visible.
- Validate event matching before comparing prices across bookmakers.
- Treat missing scores and clocks as missing data.
- Test event removals and replacement snapshots.
- Check current coverage and account limits before expanding subscriptions.
You can then build a match detail page, a market monitor or your own comparison interface. Those features belong to your application; our API supplies the underlying data.
Validation
We checked the wrapper locally with synthetic frames, including snapshot completion and terminal rendering.
Authenticated live operation requires a streaming-enabled account and available events. We have not tested this wrapper against the authenticated live service. This tutorial does not report a live latency benchmark or a production deployment test.
What would you build next?
The terminal monitor is a small starting point: receive a snapshot, maintain local state and display the selections your application needs.
What would you add first: price-change highlighting, a match filter or observation timestamps?
Top comments (0)