On the wire · 05:11:17 UTC Live 585 · Prematch 14,343 · Markets 57,081 Vol. I · No. 5 · Friday 13 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Tutorial · Bun · WebSocket

Pinnacle odds WebSocket client in Bun

Updated · PinnFeed · 2 min read

Answer: Bun's built-in WebSocket accepts a non-standard headers option, so you can send x-api-key instead of putting the key in the URL. Subscribe on open, answer each ping with {"type":"pong"}, and compare each market upsert with the previous price to log moves. Run with bun run stream.ts. Builder or Pro key required.

stream.ts#

const last = new Map<string, number>();      // market id + outcome -> decimal

const ws = new WebSocket('wss://pinnfeed.io/v1/stream', {
  headers: { 'x-api-key': process.env.PINNFEED_KEY! },   // ps_live_...
} as any);

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'subscribe', sports: ['tennis'], markets: ['moneyline'], main: true }));
});

ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(String(ev.data));
  if (msg.type === 'ping') return ws.send('{"type":"pong"}');
  const list = msg.type === 'snapshot' ? msg.markets
    : msg.type === 'delta' ? msg.changes.filter((c: any) => c.kind === 'market' && c.op === 'upsert').map((c: any) => c.market)
    : [];
  for (const m of list) {
    for (const o of m.outcomes) {
      const k = `${m.id}|${o.name}`;
      const prev = last.get(k);
      if (prev && o.decimal && Math.abs(o.decimal / prev - 1) >= 0.05) {
        console.log(new Date().toISOString(), m.eventId, o.name, prev, '→', o.decimal, 'maxStake', m.maxStake);
      }
      last.set(k, o.decimal);
    }
  }
});

ws.addEventListener('close', (ev) => console.log('closed', ev.code, ev.reason));

A threshold check like m.maxStake >= 100 helps ignore thin markets. Add a reconnect with backoff for long runs: reconnect pattern.

Plans#

Builder (€99/mo) for WebSocket push, all 13 sports, 3 connections. 3-day trial; a card is required and it is charged after day 3 unless you cancel. Server-side drop alerts without a socket: /v1/drops.

Educational content for adults (18+). No tips, no guaranteed profit.

Questions people ask#

Will this code run in Node?#

Node's global WebSocket has no headers option; use the ws package there: Node example.

Why as any?#

The headers option is Bun-specific, so the DOM type definitions do not include it.

Is 5% a good alert threshold?#

It is only an example; tune it to your markets.

The WebSocket plan: Builder, €99/mo.

Real-time Pinnacle odds pushed over one WebSocket, all 13 sports, every market, no-vig fair odds and closing lines. 3 days free, cancel in one click.

Form K-1 · Free API key

Get your free API key.

One field. No password, no card: enter your email, click the link we send, and your key is on screen.

By continuing you agree to the Terms and Privacy Policy.

One free key per email. Link valid for 20 minutes.