# Track prices over the odds stream

Open the odds stream, read a snapshot and print the best price on each side.

> **Pro plan**
> The odds stream is part of the Pro plan. Other plans get a `403` problem. See [pricing](https://sockodds.com/pricing/).

## What it does

The program opens a WebSocket to the odds stream for one league's winner markets. The stream sends a snapshot straight away, then checks again every 15 seconds and sends a new snapshot only when something changed. When nothing changed, it sends a heartbeat instead.

For each game in the first snapshot, the program prints the best current price on each side and the key of the bookmaker offering it, then closes the connection. Prices arrive grouped by market, and each outcome lists every bookmaker's price best first, so the best price is the first one. To keep tracking, leave the connection open and handle every snapshot the same way.

The stream is for pre-match prices refreshed every 15 seconds. It is not a low-latency in-play feed.

## The program

Node 22 has a built-in WebSocket client that can send the `Authorization` header, so no package is needed. Python's standard library has no WebSocket client, so there is no Python version of this example.

```javascript
const url = "wss://api.sockodds.com/v1/stream/odds?league=rlwc&markets=winner&limit=5";
const socket = new WebSocket(url, {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});

function printBest(event) {
  // Each outcome's prices are sorted best first, so prices[0] is the best price on that side.
  const market = event.markets.find((m) => m.key === "winner" && m.period === "full");
  const outcomes = new Map((market?.outcomes ?? []).map((outcome) => [outcome.side, outcome]));
  console.log(event.name);
  for (const participant of event.participants) {
    const outcome = outcomes.get(participant.role);
    const best = outcome?.prices[0];
    console.log(best ? `  ${outcome.name}: ${best.decimal_odds.toFixed(2)} at ${best.bookmaker}` : `  ${participant.identity.name}: no current price`);
  }
}

socket.addEventListener("message", (message) => {
  const frame = JSON.parse(message.data);
  if (frame.type === "heartbeat") return; // nothing changed since the last snapshot
  if (frame.type === "error") throw new Error(frame.problem.detail);
  // A league snapshot is the odds route's body: a list of events in data.
  for (const event of frame.data) printBest(event);
  socket.close(); // remove this line to keep receiving snapshots
});

socket.addEventListener("close", (event) => {
  if (event.code !== 1000 && event.code !== 1005) console.error(`stream closed: ${event.code}`);
});
```

Output (from a local copy of the API with invented test prices):

```text
Australia v New Zealand
  New Zealand: 2.23 at playup
  Australia: 1.81 at playup
```

## Keeping a stream running

- **Snapshots, not changes.** Each snapshot is the full current body for your filters, the same as the [odds route](https://sockodds.com/docs/#listOdds) returns. Replace what you hold with it rather than merging.
- **Objects.** A snapshot counts towards your allowance like an odds request. A heartbeat is free. Every refresh still counts towards your per minute request limit, so narrow with `markets=`, `bookmakers=` and `limit=` (up to 20 events for a league).
- **Lifetime.** A stream lasts at most 3,600 seconds. When it closes with code `1000`, open a new one. You can hold up to 2 streams per key.
- **Read fast.** If your client falls behind, the server closes the stream with `1013`. If a snapshot is too large it closes with `1009`: narrow the filters.
- **Read only.** Any message you send closes the stream.
- **Freshness.** A quote older than ten minutes is withheld, as on every current route, and each price still carries `observed_at`.

See [streaming](https://sockodds.com/docs/streaming/) for every close code and limit, and the [API reference](https://sockodds.com/docs/#streamOdds) for the parameters. The output above comes from a local copy of the API with invented test prices.
