On the wire · 14:55:29 UTC Live 79 · Prematch 8,495 · Markets 34,202 Vol. I · No. 1 · Monday 9 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Guida · Python · Node

Quote Pinnacle in tempo reale via WebSocket con Python e Node

Aggiornato il · PinnFeed · 5 min di lettura

In questa guida costruisci un client che tiene in memoria una copia live dei prezzi di Pinnacle: si connette, si iscrive, carica lo snapshot, applica ogni delta, risponde agli heartbeat e si riconnette da solo. Le altre guide (alert di steam, value betting, il bot Telegram) si basano su questo.

Ti serve una chiave PinnFeed sul piano Builder (il piano WebSocket, 99 €/mese, 3 giorni gratis). Con una chiave Free o Hobby puoi seguire gli esempi REST della documentazione (in inglese).

Passo 1 — Ottieni una chiave e mettila nell'ambiente#

Crea una chiave nella dashboard ed esportala, così non finisce mai nel tuo codice:

export PINNFEED_KEY="pf_live_..."

Passo 2 — Capire i frame#

Dopo la connessione e l'invio di subscribe, il server manda:

  1. subscribed e poi uno snapshot in parti (part, parts, final) con tutti gli eventi e i mercati che corrispondono ai tuoi filtri;
  2. frame delta, ognuno con una lista changes: upsert contiene l'oggetto nuovo completo, remove solo il suo id;
  3. un ping ogni 20 secondi. Rispondi con {"type":"pong"} o il server chiude il socket dopo 60 secondi di silenzio.

I mercati sono identificati da id (eventId:key), quindi un dizionario è tutto lo stato che ti serve. Ogni esito ha decimal, american, fair (quota equa senza margine) e fairProb. Il riferimento del WebSocket (in inglese) elenca tutti i campi.

Passo 3 — Il client Python#

Installa l'unica dipendenza con pip install websockets (Python 3.9+). Salvalo come pinnfeed_stream.py; le guide sullo steam e su Telegram lo importano.

# pinnfeed_stream.py: mantiene una copia live del book di Pinnacle dal WebSocket di PinnFeed.
# pip install websockets   (Python 3.9+)
import asyncio, json, os, random
import websockets

WS_URL = os.environ.get("PINNFEED_WS", "wss://pinnfeed.io/v1/stream")
KEY = os.environ["PINNFEED_KEY"]
SIDES = {"home", "away", "draw", "over", "under"}


class Book:
    """Eventi e mercati per id, aggiornati dai frame snapshot + delta."""

    def __init__(self):
        self.events, self.markets = {}, {}

    def apply(self, msg, on_market=None):
        if msg["type"] == "snapshot":
            for e in msg["events"]:
                self.events[e["id"]] = e
            for m in msg["markets"]:
                self.markets[m["id"]] = m
        elif msg["type"] == "delta":
            for c in msg["changes"]:
                if c["kind"] == "event" and c["op"] == "upsert":
                    self.events[c["event"]["id"]] = c["event"]
                elif c["kind"] == "event" and c["op"] == "remove":
                    self.events.pop(c["id"], None)
                    for mid in [k for k, m in self.markets.items() if m["eventId"] == c["id"]]:
                        del self.markets[mid]
                elif c["kind"] == "market" and c["op"] == "upsert":
                    old = self.markets.get(c["market"]["id"])
                    self.markets[c["market"]["id"]] = c["market"]
                    if on_market:
                        on_market(old, c["market"], self.events.get(c["market"]["eventId"]))
                elif c["kind"] == "market" and c["op"] == "remove":
                    self.markets.pop(c["id"], None)


async def stream(subscription, on_market=None, on_ready=None):
    """Si connette, si iscrive e tiene aggiornato il book; si riconnette con backoff all'infinito."""
    delay = 1
    while True:
        book = Book()
        try:
            async with websockets.connect(WS_URL, additional_headers={"x-api-key": KEY}, max_size=None) as ws:
                await ws.send(json.dumps({"type": "subscribe", **subscription}))
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg["type"] == "ping":
                        await ws.send('{"type":"pong"}')
                    elif msg["type"] == "error":
                        print("server error:", msg["code"], msg["message"])
                    elif msg["type"] in ("snapshot", "delta"):
                        book.apply(msg, on_market if msg["type"] == "delta" else None)
                        if msg["type"] == "snapshot" and msg.get("final") and on_ready:
                            on_ready(book)
                        delay = 1
        except (OSError, websockets.ConnectionClosed) as e:
            print(f"disconnected ({e}); reconnecting in {delay}s")
        await asyncio.sleep(delay + random.random())
        delay = min(delay * 2, 30)


if __name__ == "__main__":
    def ready(book):
        print(f"book ready: {len(book.events)} events, {len(book.markets)} markets")

    def moved(old, new, event):
        if old and event:
            for o_old, o_new in zip(old["outcomes"], new["outcomes"]):
                if o_new["name"] in SIDES and o_old["decimal"] != o_new["decimal"]:
                    print(f'{event["home"]} v {event["away"]} {new["type"]} {o_new["name"]}: '
                          f'{o_old["decimal"]} -> {o_new["decimal"]} (fair {o_new["fair"]})')

    asyncio.run(stream({"sports": ["soccer"], "markets": ["moneyline"], "main": True, "specials": False}, moved, ready))

Eseguilo con python pinnfeed_stream.py. Vedrai book ready: N events, M markets e poi una riga per ogni movimento di prezzo.

Passo 4 — Il client Node#

La stessa logica in Node 18+ con npm install ws. Salvalo come stream.mjs ed esegui node stream.mjs.

// stream.mjs: book di Pinnacle live dal WebSocket di PinnFeed (Node 18+).
// npm install ws
import WebSocket from 'ws';

const WS_URL = process.env.PINNFEED_WS || 'wss://pinnfeed.io/v1/stream';
const KEY = process.env.PINNFEED_KEY;
const SIDES = new Set(['home', 'away', 'draw', 'over', 'under']);
const events = new Map();
const markets = new Map();

function connect(delay = 1000) {
  const ws = new WebSocket(WS_URL, { headers: { 'x-api-key': KEY }, perMessageDeflate: true });
  ws.on('open', () => {
    delay = 1000;
    ws.send(JSON.stringify({ type: 'subscribe', sports: ['soccer', 'tennis'], markets: ['moneyline'], main: true, specials: false }));
  });
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    if (msg.type === 'ping') return ws.send('{"type":"pong"}');
    if (msg.type === 'error') return console.error('server error:', msg.code, msg.message);
    if (msg.type === 'snapshot') {
      if (msg.part === 1) { events.clear(); markets.clear(); }
      msg.events.forEach((e) => events.set(e.id, e));
      msg.markets.forEach((m) => markets.set(m.id, m));
      if (msg.final) console.log(`book ready: ${events.size} events, ${markets.size} markets`);
    }
    if (msg.type === 'delta') {
      for (const c of msg.changes) {
        if (c.kind === 'event') c.op === 'upsert' ? events.set(c.event.id, c.event) : events.delete(c.id);
        if (c.kind === 'market' && c.op === 'remove') markets.delete(c.id);
        if (c.kind === 'market' && c.op === 'upsert') {
          const old = markets.get(c.market.id);
          markets.set(c.market.id, c.market);
          const ev = events.get(c.market.eventId);
          if (!old || !ev) continue;
          c.market.outcomes.forEach((o, i) => {
            const prev = old.outcomes[i];
            if (prev && SIDES.has(o.name) && prev.decimal !== o.decimal) console.log(`${ev.home} v ${ev.away} ${o.name}: ${prev.decimal} -> ${o.decimal} (fair ${o.fair})`);
          });
        }
      }
    }
  });
  ws.on('close', (code) => {
    const wait = Math.min(delay * 2, 30000);
    console.log(`closed (${code}); reconnecting in ${wait / 1000}s`);
    setTimeout(() => connect(wait), wait + Math.random() * 1000);
  });
  ws.on('error', (e) => console.error('ws error:', e.message));
}
connect();

Passo 5 — Restringi l'iscrizione#

I filtri si applicano lato server, quindi ricevi ed elabori solo ciò che chiedi:

{ "type": "subscribe", "sports": ["tennis"], "live": true, "markets": ["moneyline", "total"], "main": true }
  • sports: slug o id; leagues ed events: liste di id.
  • live: true solo live, false solo pre-match; se lo ometti, entrambi.
  • main: true tiene solo il testa a testa, l'handicap e il totale principali di ogni evento.
  • Inviare di nuovo subscribe sostituisce l'iscrizione e manda un nuovo snapshot.

Checklist per la produzione#

  • Riconnettiti con backoff e jitter (entrambi i client lo fanno: 1 s, 2 s, 4 s, fino a 30 s) e ricostruisci dal nuovo snapshot.
  • Attiva la compressione: websockets di Python negozia permessage-deflate di default; in Node passa perMessageDeflate: true. Gli snapshot si riducono di 5–10 volte.
  • Leggi in fretta. Se per un client lento si accumulano più di 16 MB, il server lo chiude con il codice 4008. Fai il lavoro pesante fuori dal ciclo del socket.
  • Usa seq per i log, non per rilevare buchi: i filtri saltano i lotti a cui non sei iscritto.

Domande frequenti#

Quale piano mi serve per il WebSocket?#

Builder (99 €/mese) o superiore. Le chiavi Free e Hobby ricevono 403 plan_lacks_ws sul WebSocket e possono usare il REST.

Quante connessioni posso aprire?#

Builder consente 3 connessioni WebSocket per account, Pro 10.

Perché alcuni esiti hanno id di partecipanti invece di home o away?#

Gli speciali e alcuni antepost elencano partecipanti invece di squadra di casa e ospite. Iscriviti con "specials": false e filtra su home, away, draw, over e under se vuoi solo le linee delle partite, come fanno gli esempi.

Il piano WebSocket: Builder, 99 €/mese.

Quote Pinnacle in tempo reale su un unico WebSocket: tutti i 13 sport, tutti i mercati, quote eque senza margine e quote di chiusura. 3 giorni gratis, disdici con un clic.

Form K-1 · Free API key

Get your free API key.

No password. We email you a link; click it and your key is on screen.

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