npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pandascore/odds-sdk

v1.2.0

Published

SDK to use Pandascore Betting Feed

Readme

PandaSDK (TypeScript)

PandaSDK is the TypeScript SDK for the PandaScore Sportsbook. It connects to the real-time trading feed over AMQPS, delivers structured odds, fixture, scoreboard and settlement messages, and provides REST helpers for matches, markets and settlements.

Features

  • RabbitMQ feed integration - connect to the PandaScore AMQPS feed and receive structured JSON events.
  • Resilient reconnection - exponential backoff with jitter, automatic recovery of missed data, and in-order message buffering during recovery.
  • Startup and restart handling - cold-start snapshot, warm-restart replay, and a persistable checkpoint.
  • Feed status signal - a single FeedStatus you can gate bet acceptance on.
  • Typed message callbacks - an optional FeedListener with onMarkets / onFixture / onScoreboard / onSettlements.
  • HTTP clients - fetch matches, markets, booked matches, and settlements.
  • Extensive logging - file + console logging with contextual metadata.

Table of Contents

Installation

npm install @pandascore/odds-sdk

Or via yarn:

yarn add @pandascore/odds-sdk

Configuration

import { PandaSDK } from '@pandascore/odds-sdk';

const MySDK = PandaSDK.initialize({
  apiToken: '<your-api-token>',         // your API token
  apiBaseURL: '<your-api-base-url>',    // ask your integration manager for the base URL
  feedHost: '<your-feed-host>',         // ask your integration manager for the feed host
  company_id: 0,                        // your PandaScore company ID
  email: '<your-email>',                // your registered email
  password: '<your-password>',          // your connection password
  queues: [
    { queueName: 'my-queue', routingKey: '#' }, // '#' receives all message types
  ],
  oddsFormat: ['american', 'fractional'], // optional; decimal odds are always included
  logging: {
    directory: './PandaScore_logs',       // optional; omit to disable file logging
  },
  recoverOnReconnect: true,               // optional; default true - see Connection Behavior
  heartbeatMonitoring: true,              // optional; default true - see Connection Behavior
  prefetchCount: 1,                       // optional; default 1 - unacked messages per consumer
  recoveryWindowMs: 2 * 60 * 60 * 1000,   // optional; default 2h - see Recovery window
});

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | apiToken | string | - | REST API authentication token (required) | | company_id | number | - | Your PandaScore account ID (required) | | email | string | - | Account email (required) | | password | string | - | Account password (required) | | queues | array | - | At least one { queueName, routingKey } binding (required, max 10) | | apiBaseURL | string | - | REST API base URL (required) | | feedHost | string | - | AMQPS feed hostname (required) | | oddsFormat | ('american'\|'fractional')[] | [] | Extra odds formats to compute; decimal is always present | | recoverOnReconnect | boolean | true | Auto-recover markets and matches on reconnect | | heartbeatMonitoring | boolean | true | Detect silent drops via heartbeats; set false for a specific routing key | | prefetchCount | number | 1 | RabbitMQ QoS prefetch per consumer | | recoveryWindowMs | number | 7200000 | Max startup-recovery lookback for startWithRecovery | | logging.directory | string | ./logs | File-log directory; omit to disable | | customLogger | object | - | Inject your own logger (error/warn/info/debug/logApiResponse) |

Quick Start

const MySDK = PandaSDK.initialize({ /* ...config... */ });

// React to connection lifecycle (disconnection / reconnection / snapshot) and recovery data.
MySDK.events.on('notification', (n) => {
  if (n.type === 'disconnection') {
    // Feed is down - suspend your markets.
  } else if (n.type === 'reconnection') {
    if (n.complete) {
      // Apply n.recoveryData.markets and n.recoveryData.modifiedMatches, then resume.
    } else {
      // Recovery was incomplete - recover the window manually using n.since (see below).
    }
  }
});

// Gate business actions on feed health.
MySDK.onFeedStatusChanged((prev, next, reason) => {
  console.log(`feed status: ${prev} -> ${next} (${reason})`);
});

// Start consuming. On a fresh process, prefer startWithSnapshot (see Service Startup Guide).
await MySDK.startWithSnapshot((msg) => {
  // msg.type is "markets" | "fixture" | "scoreboard" | "settlements" | ...
});

Service Startup Guide

The right entry point depends on whether your process restarted or only the feed connection dropped.

| Situation | In-memory state | What to do | |---|---|---| | Feed dropped, process still running | Intact | Nothing - the SDK reconnects and recovers automatically | | First launch | Empty | startWithSnapshot() to load all booked matches, then go live | | Restart after downtime, with a saved checkpoint | Empty | startWithRecovery(since) to replay the gap, then go live | | You already hold match state from elsewhere | Provided | startLive() - deltas only, no snapshot or replay |

The three start methods

| Method | Use when | |--------|----------| | startWithSnapshot(handler) | Cold start, no checkpoint. Fetches all booked matches before going live. | | startWithRecovery(since, handler) | Warm restart with a persisted checkpoint. Replays the gap, then goes live. | | startLive(handler) | Advanced. You already have match state. No snapshot, no recovery. (getRMQFeed is an alias.) |

All three accept either a raw (msg) => void callback or a typed FeedListener. Live messages that arrive while the snapshot or replay is being fetched are buffered and delivered in order afterwards, so the bootstrap data is never overwritten by newer updates.

Cold start

// startWithSnapshot delivers a one-off `snapshot` notification, then streams live.
MySDK.events.on('notification', (n) => {
  if (n.type === 'snapshot') {
    if (n.complete) {
      const booked = n.recoveryData.bookedMatches; // rebuild your match state from these
    }
  }
});

await MySDK.startWithSnapshot((msg) => {
  // live updates, applied on top of the snapshot
});

You can also call fetchBookedMatches() directly if you want to build state before connecting:

const booked = await MySDK.fetchBookedMatches();

Warm restart

const since = loadCheckpoint(); // the ISO timestamp you persisted via onCheckpoint
try {
  await MySDK.startWithRecovery(since, (msg) => {
    // live updates
  });
} catch (err) {
  // The checkpoint is older than recoveryWindowMs (STRICT mode, the default).
  // Replay what we can and accept a partial picture:
  const { RecoveryWindowExceededError } = await import('@pandascore/odds-sdk');
  if (err instanceof RecoveryWindowExceededError) {
    await MySDK.startWithRecovery(since, (msg) => { /* ... */ }, undefined, 'ALLOW_PARTIAL');
  } else {
    throw err;
  }
}

Feed Status

FeedStatus is a single signal you can gate downstream actions (such as accepting bets) on. Subscribe with onFeedStatusChanged, or read the current value with getFeedStatus().

| Status | Accept bets? | Meaning | |--------|--------------|---------| | HEALTHY | yes | Live, ordered, up to date. | | DELAYED | yes (usually) | One missed heartbeat (~15s). Usually network jitter; data is still valid. Treat as a warning, not a stop. | | RECOVERING | no | Snapshot or replay in progress; live messages are buffered. | | PARTIAL_RECOVERY | no | Known-incomplete state. Sticky until you reconcile and call clearPartialRecovery(reason). | | STALE | no | Heartbeats gone; the connection may still be up. Treat data as untrusted. | | DISCONNECTED | no | AMQP is down; the SDK is reconnecting. |

let bettingEnabled = false;
MySDK.onFeedStatusChanged((prev, next, reason) => {
  bettingEnabled = next === 'HEALTHY' || next === 'DELAYED';
});

PARTIAL_RECOVERY is reached when a startup replay was clamped to the recovery window, or when automatic recovery failed after all retries. It does not clear on its own:

// After you have reconciled the missing window out of band:
MySDK.clearPartialRecovery('reconciled via fetchBookedMatches');

Connection Behavior

Heartbeat monitoring

PandaScore sends a heartbeat roughly every 10 seconds on the feed exchange. If no heartbeat arrives within 15 seconds the missed-beat counter increments; one miss flags DELAYED, and three consecutive misses mark the feed STALE and emit a disconnection notification.

This only works if heartbeats reach your queue. Heartbeats only match the catch-all binding #. If you bind to a specific routing key (for example, settlements only), heartbeats never arrive and the SDK would emit false disconnection warnings. Set heartbeatMonitoring: false in that case:

PandaSDK.initialize({
  // ...
  heartbeatMonitoring: false,
  queues: [{ queueName: 'settlements', routingKey: 'v1.*.match.*.settlements.updated' }],
});

AMQP-level drops (socket error, channel close) are detected immediately and emit a disconnection regardless of heartbeatMonitoring - the flag only controls the heartbeat-based timeout.

Reconnection and recovery

When a disconnection is detected the SDK reconnects automatically using exponential backoff with jitter (min(attempt * 5s, 60s) plus random jitter), so a fleet of clients does not reconnect in lockstep after an outage.

Once the feed is flowing again, and if recoverOnReconnect is true, the SDK:

  1. Buffers incoming live messages (so older recovered data is applied first).
  2. Calls recover_markets and the matches range endpoint to backfill the gap, retrying up to 4 times (immediately, then after 5s, 15s, 30s).
  3. Emits a reconnection notification carrying the recovered data and a complete flag.
  4. Drains the buffered live messages in order and returns the feed to HEALTHY.
MySDK.events.on('notification', async (n) => {
  if (n.type !== 'reconnection') return;
  if (n.complete) {
    applyRecovered(n.recoveryData.markets, n.recoveryData.modifiedMatches);
    resumeMarketOperations();
  } else {
    // Automatic recovery failed after all retries. The feed is PARTIAL_RECOVERY.
    // Recover the window manually using n.since, then clearPartialRecovery().
    const markets = await MySDK.recoverMarkets(n.since);
    const matches = await MySDK.fetchMatchesRange(n.since, new Date().toISOString());
    applyRecovered(markets, matches);
    MySDK.clearPartialRecovery('manual recovery complete');
    resumeMarketOperations();
  }
});

Set recoverOnReconnect: false to skip the backfill (the reconnect itself still happens).

Example log output

11:20:07 [WARN]  Disconnection detected at 2026-01-20T11:20:07Z.
         (notification: disconnection - suspend markets)
11:20:12 [WARN]  Reconnecting in 7s (attempt #1)
11:20:50 [INFO]  Recovery mode started - buffering live messages
11:20:58 [INFO]  Reconnection successful at 2026-01-20T11:20:58Z.
11:20:58 [INFO]  Recovery mode ended - drained 3 buffered messages
         (notification: reconnection, complete=true - apply data, resume)

Recovery window

recoveryWindowMs (default 2 hours) bounds how far back startWithRecovery will replay. The recover_markets endpoint returns the whole window in a single response, so the payload grows with your booked-match count.

| Account profile | Recommended recoveryWindowMs | |-----------------|--------------------------------| | Does not book eBattles | 7200000 (2 hours, default) | | Books eBattles | a few minutes, e.g. 5 * 60 * 1000 |

A since older than the window either throws RecoveryWindowExceededError (default, 'STRICT') or clamps to the window edge and marks the feed PARTIAL_RECOVERY ('ALLOW_PARTIAL'). See Warm restart.

Configuration matrix

| Routing key | heartbeatMonitoring | recoverOnReconnect | Behavior | |---|---|---|---| | # | true (default) | true (default) | Full disconnection detection + automatic backfill. Recommended for most integrations. | | # | true (default) | false | Disconnection detection and automatic reconnect, but no backfill. Use if you handle recovery yourself. | | Specific key | false | false | No heartbeat detection, no backfill. AMQP-level drops still reconnect. Use when handling everything in your app. | | Specific key | true (default) | any | Avoid - heartbeats never arrive and the SDK emits false disconnection warnings. |

Typed Listener

Instead of a raw callback you can pass a FeedListener and implement only the methods you care about. Pass it to any start method.

import { PandaSDK, FeedListener } from '@pandascore/odds-sdk';

const listener: FeedListener = {
  onMarkets(msg) { /* msg: MarketsMessage */ },
  onFixture(msg) { /* msg: FixtureMessage */ },
  onScoreboard(raw, scoreboardType) { /* e.g. "cs", "lol", "dota2" */ },
  onSettlements(msg) { /* msg: SettlementMessage */ },
  onUnknown(raw) { /* unrecognized type */ },

  // Wired automatically when the listener is passed to a start method:
  onCheckpoint(ts) { saveCheckpoint(ts); },
  onFeedStatusChanged(prev, next, reason) { /* gate bets */ },
};

await MySDK.startWithSnapshot(listener);

Messages arrive already parsed, and markets are already enriched with the configured odds formats, so handlers receive ready-to-use objects.

The backfill after a disconnection does not arrive on the listener. A FeedListener handles live messages (including the buffered ones drained after recovery), the onCheckpoint signal, and onFeedStatusChanged. The recovered markets/matches from a reconnection are delivered separately on the reconnection notification - see Reconnection and recovery. A service that needs a complete picture after a reconnect must wire both the listener and events.on('notification'). See examples/feed_listener_with_recovery.ts for the full pattern.

Checkpointing

To support warm restarts, persist the latest server timestamp and pass it to startWithRecovery next time.

// Option A: callback on every message
MySDK.onCheckpoint((ts) => saveCheckpoint(ts)); // ts is an ISO-8601 string

// Option B: read on demand
const latest = MySDK.getLastMessageTimestamp(); // string | null

Graceful Shutdown

await MySDK.close(); // stops heartbeat monitoring, cancels reconnects, closes the connection

After close() the feed will not reconnect or emit further disconnection events.

HTTP API

const match    = await MySDK.fetchMatch('979621');
const markets  = await MySDK.fetchMarkets('979621');
const range    = await MySDK.fetchMatchesRange(fromISO, toISO);
const booked   = await MySDK.fetchBookedMatches();           // active statuses, paginated
const recovered = await MySDK.recoverMarkets(sinceISO);
const settle   = await MySDK.fetchSettlements(979621);

Publishing RTBL bets

Optional - only needed if you are subscribed to the Real-Time Bet Log package.

await MySDK.connectToRabbitMQ();
await MySDK.createChannel();

const betData = {
  event_type: 'bet_placed',
  bet: {
    id: 'id-of-the-bet',
    type: 'single',
    user_id: 'user-id',
    cash_amount: 100,
    currency: 'USD',
    placed_at: new Date().toISOString(),
    selections: [
      { provider: 'PandaScore', provider_market_id: 'market-id', provider_selection_position: 1, decimal_odds: 1.5 },
    ],
  },
};

MySDK.publishBet(
  betData,
  (error) => console.error('Error:', error.message),
  (data) => console.log('Success:', data),
);

Settlement Feed

Note: This feed is not a replacement for market settlements. Use it when PandaScore market settlements are not in use, or when you manage your own settlements and need the raw outcome data. Coverage: CS2, Dota 2, League of Legends.

REST snapshot

import { SettlementMessage } from '@pandascore/odds-sdk';

const settlements: SettlementMessage = await MySDK.fetchSettlements(979621);
console.log(settlements.videogame_slug); // "cs-go" | "dota-2" | "league-of-legends"
console.log(settlements.games);          // per-game settlement data

The endpoint returns 404 if the match is not in an emitting state (not_started, postponed, canceled).

Streaming

Settlement updates carry type: "settlements" and arrive through the same handler as other feed messages (or onSettlements on a typed listener).

await MySDK.startLive((msg) => {
  if (msg.type === 'settlements') {
    for (const game of msg.games) {
      console.log(`Game ${game.position}: ${game.status}`);
    }
  }
});

To receive only settlements, bind your queue with v1.*.match.*.settlements.updated (or v1.cs-go.match.*.settlements.updated for a specific videogame) and set heartbeatMonitoring: false - see the Configuration matrix.

Settlement status values

| Status | Meaning | |--------|---------| | pending | Not yet resolved. Value is null. Expect a future update. | | settled | Resolved. Value is populated. | | voided | Resolved as void (the underlying event did not happen). Value is null. | | unavailable | The game is finished and this outcome will never resolve. Settle per your void policy. |

Per-videogame settlement types

import type { Cs2Settlements, Dota2Settlements, LolSettlements } from '@pandascore/odds-sdk';
  • CS2 (Cs2Settlements): winner, winner_first_half, rounds_won, first_to_n_rounds, round_winners, participants_winning_n_rounds, player_kills, player_headshots
  • Dota 2 (Dota2Settlements): winner, duration_seconds, first_kill, first_tower_destroyed, first_roshan_killed, team_kills, team_towers_destroyed, team_barracks_destroyed, team_roshans_killed, player_kills, kill_snapshots_at_ingame_timer_seconds
  • League of Legends (LolSettlements): winner, first_kill, first_tower_destroyed, first_inhibitor_destroyed, team_kills, team_towers_destroyed, team_inhibitors_destroyed, team_nashors_killed, team_drakes_killed, player_kills, player_assists

Recommended integration pattern

Subscribe to the streaming feed for live updates and call fetchSettlements on reconnect to backfill missed messages. The REST snapshot always reflects the full current state, so a missed streaming message never causes permanent inconsistency.

Data Models

All message and entity types are exported from the package and available as TypeScript types:

  • Markets: MarketsMessage, MarketsMessageMarket, MarketsMessageSelection
  • Fixtures: FixtureMessage, and the nested league, tournament, videogame, game, player and team types
  • Scoreboards: ScoreboardCs, ScoreboardDota2, ScoreboardLol, ScoreboardValorant, ScoreboardEsoccer, ScoreboardEbasketball, ScoreboardEhockey, ScoreboardEtennis
  • Settlements: SettlementMessage, SettlementGame, Cs2Settlements, Dota2Settlements, LolSettlements, and the per-entry types
  • SDK: FeedStatus, StatusChange, FeedListener, RecoveryMode, RecoveryWindowExceededError

Testing

npm test          # run the unit suite once (vitest)
npm run test:watch

The suite covers the feed-status state machine, recovery retry and failure signaling, in-order buffer draining, the heartbeat state machine, and typed-listener dispatch - all offline, with no network or broker required.