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
Install
npm install polynode-sdk better-sqlite3
better-sqlite3 is an optional peer dependency. Only needed if you use the cache.# Cargo.toml
polynode = { version = "0.4", features = ["cache"] }
tokio = { version = "1", features = ["full"] }
The cache feature includes rusqlite with bundled SQLite. No system dependency needed.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
}
}
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();
use polynode::{PolyNodeClient, cache::PolyNodeCache};
use std::sync::Arc;
let client = Arc::new(PolyNodeClient::new("pn_live_...")?);
let mut cache = PolyNodeCache::builder(client)
.db_path("./my-cache.db")
.watchlist_path("./polynode.watch.json")
.build()?;
cache.start().await?;
Query locally
const trades = cache.walletTrades('0xabc...', { limit: 50 });
const positions = cache.walletPositions('0xabc...');
const stats = cache.stats();
let trades = cache.wallet_trades("0xabc...", &QueryOptions { limit: Some(50), ..Default::default() })?;
let positions = cache.wallet_positions("0xabc...")?;
let stats = cache.stats()?;
Backfill Timing
Backfill fetches positions and recent trades for each wallet. Two requests per wallet (positions + trades).
| Wallets | Requests | Time at 1 req/s |
|---|
| 1 | 2 | ~2 seconds |
| 10 | 20 | ~20 seconds |
| 50 | 100 | ~2 minutes |
| 100 | 200 | ~3.5 minutes |
For deeper history, set backfillPages higher. Each additional page adds one request per wallet:
| Pages | Trades per wallet | Time for 10 wallets |
|---|
| 1 (default) | up to 500 | ~10 seconds |
| 2 | up to 1,000 | ~20 seconds |
| 6 | up 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 });
[
{
"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:
{
"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 });
[
{ "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
[
{ "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...');
[
{ "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();
{
"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
{
"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();
[
{ "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'
[
{ "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 │
└──────────────────────────────────────┘
- On start: opens SQLite, loads watchlist, connects WebSocket, begins backfill
- Backfill: fetches recent trades via REST (1 request per wallet), stores in SQLite
- Live stream: WebSocket delivers new trades and settlements in real-time
- Dedup:
INSERT OR IGNORE with unique constraint prevents duplicates between backfill and live data
- 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();