HomeBlog › Catching Live Odds Drops with SSE: A World Cup 2026 Build
World Cup 2026

Catching Live Odds Drops with SSE: A World Cup 2026 Build

Catching Live Odds Drops with SSE: A World Cup 2026 Build

Build a live odds-drop monitor for the 2026 World Cup with Server-Sent Events: connect, filter signal from noise, and survive reconnects.

Catching Live Odds Drops with SSE: A World Cup 2026 Build

During a live match, the interesting data isn't the current price — it's the change. This is a build guide for a monitor that watches Pinnacle drops over Server-Sent Events during the 2026 World Cup, keeps only the moves that matter, and doesn't fall over when the connection blips.

Polling is the wrong tool for live markets

A REST poll gives you a snapshot on a timer. If you poll every two seconds, your worst-case staleness is two seconds plus the round trip — and during a live match that's enough to miss the move entirely. SSE inverts the model: the server pushes the change the instant it happens, so your latency is network transit, not your loop interval. In our benchmark that's about 15–40 ms end-to-end (your region's number will differ).

What a "drop" actually is

A drop is a meaningful downward move in a price — the market repricing one outcome as more likely. Not every wiggle is signal; prices jitter constantly. The job is separating a real, directional move from noise, which is what the filtering below is for.

Step 1: get a key, confirm the feed

curl -i -H "x-portal-apikey: $PINNAPI_KEY" \
 "https://pinnapi.com/health"

A 200 means you're good. Don't write stream logic until this passes — debugging a stream that was never authenticated wastes an evening.

Step 2: connect to the drop stream

import { EventSource } from "eventsource";

const es = new EventSource(`https://pinnapi.com/odds-drop?key=${process.env.PINNAPI_KEY}&min_drop=5`);

es.onopen = () => console.log("stream open");
es.onmessage = (e) => {
  const payload = JSON.parse(e.data);
  if (payload.type === "connected") return;   // first frame is a handshake
  for (const drop of payload) handleDrop(drop); // each frame is an array of drops
};
es.onerror = (err) => console.error("stream error", err);

Each drop looks like { sport, home, away, sect, outcome, from_price, to_price, nvp, dispatched_ms, ... }.

Step 3: cut the noise down to signal

Two filters do most of the work, and here's why these defaults:

const WINDOW_MS = 10_000;
const MIN_MAGNITUDE = 0.03;
const recent = new Map(); // `${id}:${sect}` -> [{to, ts}]

function handleDrop(d) {
  const key = `${d.id}:${d.sect}`;
  const magnitude = (d.from_price - d.to_price) / d.from_price;
  if (magnitude < MIN_MAGNITUDE) return;

  const hist = (recent.get(key) || []).filter(p => d.dispatched_ms - p.ts <= WINDOW_MS);
  hist.push({ to: d.to_price, ts: d.dispatched_ms });
  recent.set(key, hist);

  const oldest = hist[0];
  const windowMove = (oldest.to - d.to_price) / oldest.to;
  if (windowMove >= MIN_MAGNITUDE) {
    onSignal(d, windowMove); // a fast, directional move — act
  }
}

Treat these numbers as hypotheses to validate against your own logged data, not as laws. The next post in this series is about exactly that — measuring whether your thresholds actually predict anything.

Reconnects: stream for speed, REST for recovery

Streams drop. Phones change networks, load balancers cycle, matches run for two hours. The robust pattern is: stream for live speed, REST snapshot for recovery. On reconnect, don't try to replay missed events — pull one REST snapshot to re-sync state, then resume the stream.

es.onerror = async () => {
  await backfillViaRest(); // GET /kit/v1/markets to re-sync
  // EventSource auto-reconnects; state is now correct
};

Prematch and live are the same fixture, different phases

A useful simplification: a fixture doesn't change identity at kickoff, it changes phase. The same event_id carries from event_type=prematch to event_type=live, so you can subscribe before the match and keep the same handler when it goes live — no special-casing the transition.

The takeaway

The speed comes from SSE; the usefulness comes from filtering. A raw drop stream is firehose noise. Magnitude plus a tight velocity window turns it into a short list of moves worth acting on — and you should treat the exact thresholds as something to measure, not assume.

FAQ

How fast does pinnapi deliver World Cup odds? ~15–40 ms end-to-end in our benchmark; measure your own region before relying on it.

What is an odds drop and how do I detect it? A meaningful downward price move; detect it with a magnitude floor plus a short velocity window to filter out drift.

How do I detect odds drops programmatically? Subscribe to a push stream that watches every price server-side and emits an event the instant one falls past your threshold — polling snapshots and diffing them yourself is the slow, expensive version of the same job.

How do I set a threshold for odds drop alerts? Pass ?min_drop=N on the stream URL (percent of the pre-drop price; default 5, floor 1) and the server filters for you — then tighten further client-side if your strategy wants it. Think in probability terms, not raw decimals.

Can I use the SSE drop stream on the free trial? The free tier covers REST for learning; live drop streams are a paid capability.

How should I handle SSE disconnects during a match? Re-sync with one REST snapshot on reconnect rather than replaying the stream.

Get real-time Pinnacle odds in your code

Live & prematch markets with sub-second odds-drop alerts. Free trial key in seconds — no card.

Start free trial