Official Node.js / TypeScript client for PinnFeed: real-time Pinnacle odds over REST and WebSocket, with normalized markets, no-vig fair prices, price drops and closing-line history.
- Typed REST client for every
/v1endpoint (uses globalfetch, Node 18+) - WebSocket client (
PinnFeedStream) with automatic reconnect, exponential backoff with jitter, heartbeat (answers server pings, detects dead connections) and re-subscribe; EventEmitter andfor awaitAPIs OddsBook: in-memory book kept up to date fromsnapshot+deltaframes- ESM + CommonJS builds with type declarations
The package is not on npm yet. Install it from GitHub (the package prepare script builds dist/):
npm install github:pinnfeed/pinnfeed-nodeCreate a key at pinnfeed.io/signup and export it:
export PINNFEED_API_KEY=ps_live_...The Free plan is REST only with a 30-second delay; the WebSocket starts at Builder. Plans and limits: pinnfeed.io/pricing. As of 9 Nov 2026, PinnFeed Builder (€99/mo or €990/yr, VAT incl.) was the lowest published live + prematch Pinnacle WebSocket on monthly or annual billing (details and sources).
import { PinnFeed } from 'pinnfeed'; // or: const { PinnFeed } = require('pinnfeed')
const pf = new PinnFeed(); // reads PINNFEED_API_KEY
console.log((await pf.account()).plan);
const { events } = await pf.odds('soccer', { market: 'moneyline', main: true, limit: 5 });
for (const ev of events)
for (const m of ev.markets ?? [])
console.log(ev.home, 'v', ev.away, m.outcomes.map((o) => [o.name, o.decimal, o.fair]));| Method | Endpoint | Notes |
|---|---|---|
account() |
GET /v1/account |
plan, limits, today's usage |
sports() |
GET /v1/sports |
live/prematch counts, inPlan |
events(filters) |
GET /v1/events |
events without markets |
odds(sport, filters) |
GET /v1/odds |
events with markets; sport required; withMarkets option |
event(id, filters) |
GET /v1/events/{id} |
one event with markets |
drops({ minutes, minPct, limit, ...filters }) |
GET /v1/drops |
recent price drops (window 1–60 min) |
closing(eventId) |
GET /v1/closing/{eventId} |
closing lines, Builder+ |
closingDay(date, filters) |
GET /v1/closing |
closing lines for a UTC day, Builder+ |
Filters (all optional; arrays or comma-separated strings): sport, league, event, market (moneyline, spread, total, team_total), live (true/false/'live'/'prematch'), main, specials, q, limit, offset. Response headers of the last call are on pf.lastHeaders (e.g. x-plan, x-data-delay-seconds).
import { PinnFeedStream } from 'pinnfeed';
const stream = new PinnFeedStream({ sports: ['soccer', 'tennis'], markets: ['moneyline'], main: true });
for await (const msg of stream) {
if (msg.type === 'snapshot' && msg.final) console.log('book ready:', stream.book.events.size, 'events');
if (msg.type === 'delta')
for (const ch of msg.changes)
if (ch.kind === 'market' && ch.op === 'upsert') console.log(ch.market.eventId, ch.market.type, ch.market.outcomes);
}Or event style: stream.on('delta', fn).on('reconnecting', fn).on('error', fn).connect(); stop with stream.close().
What the client handles for you (protocol: pinnfeed.io/docs/websocket):
- Sends your
subscriberight after connecting (the server closes with4002if none arrives within 30 s). - Replies
pongto every serverping(sent every 20 s; the server drops clients silent for 60 s) and reconnects if nothing arrives foridleTimeoutMs(default 60000). - Reconnects with exponential backoff + jitter (
initialBackoffMs,maxBackoffMs,maxRetries) on network errors,4008 slow_consumerand429 connection_limit, then re-subscribes and receives a fresh snapshot. - Fails with
AuthenticationErroron401or close code4001 key_revoked, andPlanErroron403 plan_lacks_ws(Free/Hobby keys), withupgradeUrlset.
Change filters live with stream.subscribe({ sports: ['basketball'] }). format: 'raw' (Pro and Enterprise) gives type: 'raw' frames with Pinnacle's own records, un-normalized (typed as RawFrame; format). The stream uses the ws package so it can send the key as an x-api-key header.
All errors extend PinnFeedError with status, code, message, retryAfterMs and upgradeUrl from the API's { "error": {...} } body: AuthenticationError (401), PlanError (403), NotFoundError (404), RateLimitError (429). Codes: pinnfeed.io/docs/errors.
Run with npx tsx examples/<file>.ts:
live-stream.ts: stream moneyline moves for a sportdropping-odds-alert.ts: alert on drops via REST/v1/drops(any plan) or on the WebSocketvalue-vs-fair.ts: compare another bookmaker's price with Pinnacle's no-vig fair priceclosing-line-value.ts: closing-line value of a bet you placed (Builder+)
PinnFeed carries one bookmaker (Pinnacle), so there is no cross-bookmaker arbitrage example; value-vs-fair.ts shows the supported use: comparing your own book's prices with Pinnacle's no-vig line.
npm ci
npm run typecheck && npm test && npm run buildTests run against a local fake server; no API key needed.
Docs · OpenAPI · Pricing · Python SDK · support@pinnfeed.io
MIT licensed. PinnFeed is not affiliated with Pinnacle.