Skip to main content
Track wallets, markets, and tokens locally. The SDK streams live events into a SQLite database and backfills recent history on startup. All queries run instantly against the local DB with zero API calls.

Why Use the Cache

Without the cache, every page view that shows trader positions requires upstream API calls. For apps tracking dozens or hundreds of wallets, this hits rate limits fast. With the cache:
  • One API call per wallet to backfill recent history (500 trades)
  • Live WebSocket stream keeps everything up to date after that
  • All queries are local — positions, trades, stats are instant
  • Persists across restarts — SQLite file stays on disk

Quick Start

1

Install

npm install polynode-sdk better-sqlite3
better-sqlite3 is an optional peer dependency. Only needed if you use the cache.
2

Create a watchlist

Create polynode.watch.json in your project root:
{
  "version": 1,
  "wallets": [
    { "address": "0xabc...", "label": "trader-1", "backfill": true },
    { "address": "0xdef...", "label": "trader-2", "backfill": true }
  ],
  "settings": {
    "ttl_days": 30
  }
}
3

Start the cache

import { PolyNode, PolyNodeCache } from 'polynode-sdk';

const pn = new PolyNode({ apiKey: 'pn_live_...' });
const cache = new PolyNodeCache(pn, {
  dbPath: './my-cache.db',
  watchlistPath: './polynode.watch.json',
});

await cache.start();
4

Query locally

const trades = cache.walletTrades('0xabc...', { limit: 50 });
const positions = cache.walletPositions('0xabc...');
const stats = cache.stats();

Backfill Timing

Backfill fetches positions and recent trades for each wallet. Two requests per wallet (positions + trades).
WalletsRequestsTime at 1 req/s
12~2 seconds
1020~20 seconds
50100~2 minutes
100200~3.5 minutes
For deeper history, set backfillPages higher. Each additional page adds one request per wallet:
PagesTrades per walletTime for 10 wallets
1 (default)up to 500~10 seconds
2up to 1,000~20 seconds
6up to 3,000 (max)~60 seconds
Upstream data caps at 3,000 historical trades per wallet. The live stream captures everything going forward with no limit.

Configuration

const cache = new PolyNodeCache(pn, {
  // File paths
  dbPath: './polynode-cache.db',       // SQLite database location
  watchlistPath: './polynode.watch.json', // Watchlist file

  // Backfill
  backfillRatePerSecond: 1,            // Requests per second (default: 1)
  backfillPages: 1,                    // Pages per wallet (default: 1, max: 6)
  backfillPageSize: 500,               // Trades per page (default: 500, max: 1000)

  // Storage
  ttlSeconds: 30 * 86400,             // Auto-prune after 30 days
  purgeOnRemove: false,               // Delete data when wallet removed from watchlist

  // Progress callback
  onBackfillProgress: (p) => {
    console.log(`${p.label}: ${p.status} (${p.fetched} trades)`);
  },
});

Query Methods

All queries run against the local SQLite database. No API calls. Every example below shows real output from a live backfill.

Wallet Trades

const trades = cache.walletTrades('0xad53...', { limit: 3 });
Example output
[
  {
    "side": "BUY",
    "price": 0.821,
    "size": 5.92,
    "market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
    "outcome": "Yes",
    "timestamp": "2026-03-21T18:00:28.223Z"
  },
  {
    "side": "SELL",
    "price": 0.18,
    "size": 40.51,
    "market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
    "outcome": "No"
  },
  {
    "side": "BUY",
    "price": 0.181,
    "size": 19.1,
    "market_title": "Will Iran conduct a military action against Israel on March 20, 2026?",
    "outcome": "No"
  }
]
Filters: side, since, until, orderBy, limit, offset:
// Only BUY trades
const buys = cache.walletTrades('0xad53...', { limit: 3, side: 'BUY' });

// Pagination
const page1 = cache.walletTrades('0xad53...', { limit: 5, offset: 0 });
const page2 = cache.walletTrades('0xad53...', { limit: 5, offset: 5 });

// Time range
const recent = cache.walletTrades('0xad53...', { since: 1774000000 });

// Ascending order
const oldest = cache.walletTrades('0xad53...', { orderBy: 'timestamp_asc', limit: 3 });

Wallet Positions

Positions are backfilled directly from the API with full P&L, current price, and value data. Trade timestamps are enriched from the local trades table.
const positions = cache.walletPositions('0xad53...');
// 200 positions from 500 cached trades
Example output (first 2 of 500)
[
  {
    "wallet": "0xad53...",
    "token_id": "75929940...",
    "market_title": "Will \"How to Make a Killing\" score at least 59 on the Rotten Tomatoes Tomatometer?",
    "outcome": "Yes",
    "size": 10000,
    "avg_price": 0.001,
    "cur_price": 0.0005,
    "current_value": 5.0,
    "cash_pnl": -5.0,
    "percent_pnl": -50.0,
    "redeemable": false,
    "trade_count": 3,
    "first_trade_at": 1710000000,
    "last_trade_at": 1774100000
  },
  {
    "wallet": "0xad53...",
    "market_title": "Will Resni.ca (Res) be part of the next Government of Slovenia?",
    "outcome": "Yes",
    "size": 7.50,
    "avg_price": 0.41,
    "cash_pnl": 2.5,
    "trade_count": 1
  }
]

Multi-Wallet Positions

Query positions for multiple wallets in one call:
const all = cache.multiWalletPositions(['0xad53...', '0x2afd...', '0xe4ca...']);
Returns an object keyed by wallet address, where each value is an array of positions:
Example output
{
  "0xad53...": [
    { "wallet": "0xad53...", "market_title": "...", "outcome": "Yes", "size": 10000, "avg_price": 0.04, "cash_pnl": -50.0, "trade_count": 3 },
    { "wallet": "0xad53...", "market_title": "...", "outcome": "No", "size": 500, "avg_price": 0.41, "cash_pnl": 12.5, "trade_count": 1 }
  ],
  "0x2afd...": [
    { "wallet": "0x2afd...", "market_title": "...", "outcome": "Yes", "size": 250, "avg_price": 0.55, "cash_pnl": 30.0, "trade_count": 2 }
  ],
  "0xe4ca...": [...]
}

Market Trades

const trades = cache.marketTrades('0xe1cc...', { limit: 3 });
Example output
[
  { "taker": "0xad53...", "side": "BUY", "price": 0.821, "size": 5.92 },
  { "taker": "0xad53...", "side": "SELL", "price": 0.18, "size": 40.51 },
  { "taker": "0xad53...", "side": "BUY", "price": 0.181, "size": 19.1 }
]

Market Positions

All positions across all cached wallets for a market:
const positions = cache.marketPositions('0xe1cc...');
// 9 positions across multiple wallets
Example output
[
  { "outcome": "Yes", "size": -172.12, "avg_price": 0.8399 },
  { "outcome": "No", "size": -81.39, "avg_price": 0.1808 },
  { "outcome": "No", "size": 28.66, "avg_price": 0.18 }
]

Token Trades

const trades = cache.tokenTrades('11382339...', { limit: 3 });
// All returned trades match the requested token_id

Trade by Transaction Hash

Look up all trades within a single transaction:
const trades = cache.tradeByTxHash('0x6815497d...');
Example output
[
  { "side": "BUY", "price": 0.821, "size": 5.92 },
  { "side": "SELL", "price": 0.18, "size": 40.51 },
  { "side": "BUY", "price": 0.181, "size": 19.1 }
]

Wallet Settlements

const settlements = cache.walletSettlements('0xad53...', { limit: 20 });

Cache Stats

const stats = cache.stats();
Example output
{
  "trade_count": 1509,
  "settlement_count": 3,
  "db_size_kb": 10567.3,
  "oldest_trade": "2026-03-14T19:19:25.000Z",
  "newest_trade": "2026-03-21T18:00:28.223Z",
  "backfill_complete": 3,
  "backfill_total": 3,
  "backfill_failed": 0
}

Watchlist

File Format

{
  "version": 1,
  "wallets": [
    { "address": "0xabc...", "label": "whale", "backfill": true }
  ],
  "markets": [
    { "condition_id": "0x789...", "label": "BTC 100k", "backfill": true }
  ],
  "tokens": [
    { "token_id": "12345...", "label": "BTC Yes", "backfill": true }
  ],
  "settings": {
    "ttl_days": 30,
    "backfill_rate": 1,
    "purge_on_remove": false
  }
}

Hot Reload

Edit the watchlist file while the cache is running. Changes are detected automatically within 500ms:
  • New entries trigger backfill and update the WebSocket subscription
  • Removed entries optionally purge data (if purgeOnRemove is enabled)

Runtime API

Add or remove wallets programmatically. Backfill starts immediately for new entries.
// Add a wallet — backfill starts within 1 second
cache.addToWatchlist([
  { type: 'wallet', id: '0x99ba...', label: 'UnholyScissors' }
]);

// After ~2 seconds:
cache.stats();
// { trade_count: 1857, backfill_complete: 4 }
// (was 1509 trades / 3 complete before adding)

// Remove a wallet
cache.removeFromWatchlist([
  { type: 'wallet', id: '0x99ba...' }
]);

View Methods

Pre-built queries that return data shaped for dashboards. No SQL, no aggregation — just call the method.

Watchlist Summary

All watched wallets with summary stats in one call:
const summary = cache.watchlistSummary();
Example output
[
  { "wallet": "0xad53...", "label": "whale-1", "position_count": 42, "total_pnl": 1250.50, "total_value": 8400.00, "last_active": 1774200000 },
  { "wallet": "0x2afd...", "label": "degen", "position_count": 15, "total_pnl": -320.00, "total_value": 1200.00, "last_active": 1774180000 }
]

Wallet Dashboard

Single wallet view with positions grouped, P&L totals, win/loss counts, and recent trades:
const dash = cache.walletDashboard('0xad53...');
// dash.total_pnl, dash.win_count, dash.loss_count, dash.positions, dash.recent_trades

Leaderboard

Rank watched wallets by any metric:
const leaders = cache.leaderboard('total_pnl');
// Also: 'total_value', 'trade_count', 'win_rate'
Example output
[
  { "wallet": "0xad53...", "label": "whale-1", "value": 1250.50, "rank": 1 },
  { "wallet": "0x2afd...", "label": "degen", "value": -320.00, "rank": 2 }
]

Market Overview

All cached positions for a market across watched wallets:
const overview = cache.marketOverview('0xcondition...');
// overview.positions, overview.total_volume, overview.unique_wallets

Reactive Subscriptions

Fire callbacks when new data lands in the cache from the live WebSocket stream.
// Subscribe to all changes
const unsub = cache.onChange((event) => {
  // event.type: 'trade' | 'settlement'
  // event.wallet: string
  // event.data: TradeRow | SettlementRow
  console.log(`New ${event.type} for ${event.wallet}`);
});

// Wallet-specific — only fires for this wallet
const unsub2 = cache.onWalletChange('0xad53...', (event) => {
  updateUI(event.data);
});

// Cleanup
unsub();
unsub2();

Export Helpers

Dump filtered data for charting libraries, spreadsheets, or custom analysis.
import * as fs from 'fs';

// CSV export
const csv = cache.exportCSV('trades', { wallet: '0xabc...', limit: 1000 });
fs.writeFileSync('trades.csv', csv);

// JSON array export
const json = cache.exportJSON('positions', { wallet: '0xabc...' });

// Raw rows for data libraries
const rows = cache.exportRows('trades', { wallet: '0xabc...', since: 1774000000 });
Filter options: wallet, conditionId, tokenId, side, since, until, limit, orderBy.

Query Builder

Chainable fluent API for complex queries without writing SQL.
// Filter trades by wallet, side, time, and market
const results = cache.query('trades')
  .wallet('0xabc...')
  .side('BUY')
  .since(1774000000)
  .market('0xcondition...')
  .limit(50)
  .orderBy('timestamp_desc')
  .run();

// Filter positions by size and profitability
const winners = cache.query('positions')
  .wallet('0xabc...')
  .minSize(100)
  .minPnl(0)  // only profitable
  .run();
Available filters: .wallet(), .market(), .token(), .side(), .since(), .until(), .limit(), .skip(), .orderBy(), .minSize(), .minPnl() (positions only).

How It Works

                     ┌──────────────────┐
                     │  PolyNodeCache   │
                     │                  │
      ┌──────────────┤  backfill (1x)   │
      │              │  live stream     │
      │              │  prune timer     │
      ▼              │  file watcher    │
┌───────────┐        └────────┬─────────┘
│  REST API │ (backfill)      │ (live events)
│  1 req/s  │        ┌───────▼──────────┐
└───────────┘        │ WebSocket stream │
      │              │ trades + settle. │
      │              └───────┬──────────┘
      │                      │
      ▼                      ▼
┌──────────────────────────────────────┐
│  SQLite (WAL mode)                   │
│  trades — full inverted index        │
│  settlements — pending + confirmed   │
│  backfill_state — crash recovery     │
└──────────────────────────────────────┘
  1. On start: opens SQLite, loads watchlist, connects WebSocket, begins backfill
  2. Backfill: fetches recent trades via REST (1 request per wallet), stores in SQLite
  3. Live stream: WebSocket delivers new trades and settlements in real-time
  4. Dedup: INSERT OR IGNORE with unique constraint prevents duplicates between backfill and live data
  5. Prune: hourly timer removes data older than the configured TTL

Persistence

The SQLite database persists across restarts. When you call cache.start() again:
  • Existing data is preserved
  • Completed backfills are not repeated
  • WebSocket stream reconnects and picks up live events
  • Any trades that happened while offline are captured on the next backfill

Stop and Cleanup

await cache.stop();  // closes WebSocket, stops backfill, closes DB

// Manual prune
const deleted = cache.prune();  // removes data older than TTL

Testing Utilities

The SDK includes helpers that return known-active Polymarket wallets. Useful for examples, integration tests, and getting started without needing to find wallet addresses yourself.
import { getActiveTestWallet, getActiveTestWallets } from 'polynode-sdk';

// Get a single active wallet (instant, uses cached fallback)
const wallet = await getActiveTestWallet();

// Get multiple active wallets
const wallets = await getActiveTestWallets(5);

// Fetch a fresh wallet from live leaderboard data
const fresh = await getActiveTestWallet({ fresh: true });
Combine with the cache for a zero-config quickstart:
import { PolyNode, PolyNodeCache, getActiveTestWallet } from 'polynode-sdk';

const pn = new PolyNode({ apiKey: 'pn_live_...' });
const wallet = await getActiveTestWallet();

const cache = new PolyNodeCache(pn, {
  dbPath: './cache.db',
  watchlistPath: './polynode.watch.json',
});

await cache.start();
cache.addToWatchlist([{ type: 'wallet', id: wallet, label: 'test-trader' }]);

// Wait for backfill, then query
setTimeout(() => {
  const trades = cache.walletTrades(wallet, { limit: 10 });
  console.log(`${trades.length} trades for ${wallet}`);
}, 3000);
getActiveTestWallet() returns instantly by default using a cached list of known-active wallets. Pass { fresh: true } to fetch the current top trader from live data (adds ~1-2s network latency).

Full Example

import { PolyNode, PolyNodeCache } from 'polynode-sdk';

const pn = new PolyNode({ apiKey: 'pn_live_...' });

const cache = new PolyNodeCache(pn, {
  dbPath: './cache.db',
  watchlistPath: './polynode.watch.json',
  backfillPages: 1,
  onBackfillProgress: (p) => {
    const icon = p.status === 'complete' ? '✓' : '⟳';
    console.log(`${icon} ${p.label}: ${p.fetched} trades`);
  },
});

await cache.start();
// Output:
// [PolyNodeCache] Backfilling 10 entities (1 page of 500 each) — ETA: ~10s
// ⟳ trader-1: 500 trades
// ✓ trader-1: 500 trades
// ⟳ trader-2: 346 trades
// ✓ trader-2: 346 trades
// ...

// Query locally — instant
const positions = cache.walletPositions('0xabc...');
for (const p of positions) {
  console.log(`${p.outcome}: ${p.size} shares @ ${p.avg_price.toFixed(4)}`);
}

// Add a wallet at runtime
cache.addToWatchlist([
  { type: 'wallet', id: '0xnew...', label: 'new-whale' }
]);

// Stats
const stats = cache.stats();
console.log(`${stats.trade_count} trades, ${(stats.db_size_bytes / 1024 / 1024).toFixed(1)} MB`);

// Cleanup
await cache.stop();