Skip to content

About

Official Node.js/TypeScript SDK for the PinnFeed real-time Pinnacle odds API (REST + WebSocket)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

pinnfeed-node

CI License: MIT

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 /v1 endpoint (uses global fetch, Node 18+)
  • WebSocket client (PinnFeedStream) with automatic reconnect, exponential backoff with jitter, heartbeat (answers server pings, detects dead connections) and re-subscribe; EventEmitter and for await APIs
  • OddsBook: in-memory book kept up to date from snapshot + delta frames
  • ESM + CommonJS builds with type declarations

Install

The package is not on npm yet. Install it from GitHub (the package prepare script builds dist/):

npm install github:pinnfeed/pinnfeed-node

Get an API key

Create 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).

REST quickstart

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).

WebSocket quickstart

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 subscribe right after connecting (the server closes with 4002 if none arrives within 30 s).
  • Replies pong to every server ping (sent every 20 s; the server drops clients silent for 60 s) and reconnects if nothing arrives for idleTimeoutMs (default 60000).
  • Reconnects with exponential backoff + jitter (initialBackoffMs, maxBackoffMs, maxRetries) on network errors, 4008 slow_consumer and 429 connection_limit, then re-subscribes and receives a fresh snapshot.
  • Fails with AuthenticationError on 401 or close code 4001 key_revoked, and PlanError on 403 plan_lacks_ws (Free/Hobby keys), with upgradeUrl set.

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.

Errors

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.

Examples

Run with npx tsx examples/<file>.ts:

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.

Development

npm ci
npm run typecheck && npm test && npm run build

Tests run against a local fake server; no API key needed.

Links

Docs · OpenAPI · Pricing · Python SDK · support@pinnfeed.io

MIT licensed. PinnFeed is not affiliated with Pinnacle.

About

Official Node.js/TypeScript SDK for the PinnFeed real-time Pinnacle odds API (REST + WebSocket)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages