Documentation Index
Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
Use this file to discover all available pages before exploring further.
The RedemptionWatcher monitors wallets for redeemable positions. It combines wallet position data from REST with real-time condition_resolution events from the oracle stream. When a market resolves, any tracked wallet holding that condition gets an instant alert with full payout details.
One class, one WebSocket connection, zero polling.
Install
Quick Start
import { RedemptionWatcher } from 'polynode-sdk';
const watcher = new RedemptionWatcher({ apiKey: 'pn_live_...' });
watcher.on('alert', (alert) => {
if (alert.isWinner) {
console.log(`${alert.wallet} can redeem ${alert.outcome} on "${alert.marketTitle}"`);
console.log(`Payout: $${alert.estimatedPayoutUsd}`);
}
});
watcher.on('ready', () => {
console.log(`Tracking ${watcher.size} positions across ${watcher.wallets.length} wallets`);
});
await watcher.start([
'0xabc...', // user wallet 1
'0xdef...', // user wallet 2
]);
How It Works
start(wallets) fetches positions for each wallet via REST (parallel), builds an internal index keyed by condition_id, then subscribes to the oracle WebSocket stream
- When a
condition_resolution event arrives, the watcher cross-references event.condition_id against the position index
- For each matched position, a
RedeemableAlert is emitted with wallet address, win/loss status, payout estimate, and full market metadata
- After alerts fire, the resolved condition is evicted from memory. Positions that drop to zero size (full sell) are also evicted. This keeps memory bounded to only active, non-zero positions regardless of how long the watcher runs.
- If
trackPositionChanges is enabled (default), position sizes stay accurate in real-time via the wallets WebSocket stream. If a wallet enters a new market after start(), the watcher automatically picks it up from the enriched transfer event and adds it to the index.
- A periodic REST refresh (default: every 5 minutes) re-fetches all wallet positions as a safety net, catching any positions the stream may have missed during brief disconnections.
condition_resolution is the moment positions become redeemable on the Conditional Tokens contract. For neg-risk markets (the majority on Polymarket), this fires in a separate transaction after the UMA resolution event. See Oracle Events for the full resolution lifecycle.
Lifecycle
1. Construct
const watcher = new RedemptionWatcher({
apiKey: 'pn_live_...',
trackPositionChanges: true, // live position + new market tracking (default)
});
2. Register Handlers
Register handlers before calling start() so you don’t miss the ready event.
watcher.on('alert', (alert) => {
pushNotification(alert.wallet, {
title: alert.isWinner ? 'Position Redeemable!' : 'Market Resolved',
body: `${alert.marketTitle} — ${alert.winningOutcome} wins`,
payout: alert.estimatedPayoutUsd,
});
});
watcher.on('ready', () => {
console.log(`Tracking ${watcher.size} positions`);
});
watcher.on('error', (err) => {
console.error('Watcher error:', err.message);
});
3. Start
await watcher.start(['0x02227b...', '0xc2e780...']);
start() fetches positions for all wallets in parallel, indexes them by condition_id, subscribes to the oracle stream, and emits ready. Typical startup takes 100-300ms depending on wallet count.
Real output from start() with one wallet:
Tracking 16 positions across 1 wallet(s)
Conditions: 16
4. Add/Remove Wallets at Runtime
// New user signs up — fetch their positions and start tracking
await watcher.addWallets(['0xefbc5f...']);
// wallets: 2, size: 55
// User leaves — purge state and update subscriptions
watcher.removeWallets(['0xefbc5f...']);
// wallets: 1, size: 16
5. Query State
watcher.wallets; // all tracked addresses
watcher.conditions; // all tracked condition IDs
watcher.size; // total position count
watcher.positionsFor('0x02227b...'); // positions for one wallet
6. Close
Unsubscribes from oracle and wallet streams, stops the refresh timer, disconnects the WebSocket, and resolves any pending async iterators.
Async Iterator
Consume alerts sequentially with for await:
for await (const alert of watcher) {
await processRedemption(alert);
// backpressure: next alert waits until this one is processed
}
The iterator terminates when watcher.close() is called.
Alert Object
When a condition_resolution event matches a tracked position, the watcher emits a RedeemableAlert:
interface RedeemableAlert {
wallet: string; // which tracked wallet holds this position
conditionId: string; // Polymarket condition ID (hex)
tokenId: string; // conditional token ID held by the wallet
outcome: string; // which outcome the wallet holds ("Over", "Yes", etc.)
winningOutcome: string; // which outcome won ("Under", "No", etc.)
isWinner: boolean; // whether this position pays out
size: number; // position size in tokens
estimatedPayoutUsd: number; // $1 per token if winner, $0 if loser
marketTitle: string; // human-readable market question
marketSlug: string; // URL slug on polymarket.com
marketImage?: string; // market image URL
resolvedPrice: number; // UMA resolved price (1 = first outcome, 0 = second)
payouts: number[]; // payout array from the contract ([1, 0] or [0, 1])
blockNumber: number; // Polygon block number
timestamp: number; // block timestamp in ms
}
Example alert (if a wallet held 500 “Over” tokens on the Dota 2 kills market below):
{
"wallet": "0x02227b8f5a9636e895607edd3185ed6ee5598ff7",
"conditionId": "0xf5fbfb50f2ad1f61569fa475b8883d2f1ee10fdceab24f069528b717a40cc101",
"tokenId": "57488988409414421964789033691537502121286921656702379301876764465477206797606",
"outcome": "Over",
"winningOutcome": "Over",
"isWinner": true,
"size": 500,
"estimatedPayoutUsd": 500,
"marketTitle": "Total Kills Over/Under 45.5 in Game 1?",
"marketSlug": "dota2-ty-mouz-2026-03-25-game1-kill-over-45pt5",
"marketImage": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"resolvedPrice": 1,
"payouts": [1, 0],
"blockNumber": 84667307,
"timestamp": 1774454649000
}
Tracked Positions
Each position loaded from the REST API is stored as a TrackedPosition:
interface TrackedPosition {
wallet: string;
tokenId: string;
conditionId: string;
outcome: string; // "Yes", "No", "Over", "Under", team names, etc.
size: number; // position size in tokens
marketTitle: string;
marketSlug: string;
marketImage?: string;
outcomes: string[]; // all outcomes for this market
tokenIds: string[]; // all token IDs for this market
negRisk?: boolean; // neg-risk framework (multi-outcome markets)
}
Real output from positionsFor():
[
{
"wallet": "0x02227b8f...",
"tokenId": "80628887006942228330415332521239838709...",
"conditionId": "0xf88a2140ac54353c2f51b2ee20de43c72b0d...",
"outcome": "No",
"size": 3440212.886182,
"marketTitle": "Will Manchester City FC win on 2026-02-21?",
"marketSlug": "epl-mac-new-2026-02-21-mac",
"negRisk": true
},
{
"wallet": "0x02227b8f...",
"tokenId": "11424825366156360215190655536940393847...",
"conditionId": "0x18f34febea506bed46b77c3126369eb9cc00...",
"outcome": "Yes",
"size": 1625004.089452,
"marketTitle": "Will Toulouse FC win on 2026-02-15?",
"marketSlug": "fl1-hac-tou-2026-02-15-tou",
"negRisk": true
}
]
The Event That Triggers Alerts
The watcher listens for condition_resolution oracle events. Here’s a real one from a Dota 2 esports market:
{
"event_type": "oracle",
"oracle_type": "condition_resolution",
"block_number": 84667307,
"timestamp": 1774454649000,
"tx_hash": "0x863f3e30d4d3e84c80682565d40502257ec1580e34f212838e6954d32b738f7b",
"condition_id": "0xf5fbfb50f2ad1f61569fa475b8883d2f1ee10fdceab24f069528b717a40cc101",
"question_id": "0x22d5713c949303f38b5add45b0dd694cb6914fcdd33d35e908ef47f918fa2147",
"adapter_address": "0x65070be91477460d8a7aeeb94ef92fe056c2f2a7",
"resolved_price": 1,
"resolved_outcome": "Over",
"payouts": [1, 0],
"market_title": "Total Kills Over/Under 45.5 in Game 1?",
"market_slug": "dota2-ty-mouz-2026-03-25-game1-kill-over-45pt5",
"event_title": "Dota 2: Team Yandex vs MOUZ (BO2) - ESL One Birmingham Group A",
"market_image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/dota2-7ffacddb21.jpg",
"outcomes": ["Over", "Under"],
"token_ids": [
"57488988409414421964789033691537502121286921656702379301876764465477206797606",
"44113030389329339383080791624849973102915570622427276234797737004302338372036"
],
"neg_risk": false
}
Winner detection: The watcher finds the wallet’s tokenId in event.token_ids, checks the corresponding index in event.payouts. If payouts[index] > 0, the position is a winner and pays out $1 × size.
Configuration
interface RedemptionWatcherConfig {
apiKey: string; // required — PolyNode API key
baseUrl?: string; // REST API base (default: https://api.polynode.dev)
wsUrl?: string; // WebSocket URL (default: wss://ws.polynode.dev/ws)
compress?: boolean; // zlib compression (default: true via PolyNodeWS)
autoReconnect?: boolean; // auto-reconnect on disconnect (default: true)
trackPositionChanges?: boolean; // live position delta tracking (default: true)
refreshInterval?: number; // periodic REST refresh in ms (default: 300000 = 5 min)
}
trackPositionChanges
When enabled (default), the watcher subscribes to the wallets WebSocket stream. This does two things:
- Size tracking — updates position sizes in real-time as ERC-1155 transfers occur. If a user buys or sells tokens after
start(), the size field stays accurate for payout calculations.
- New market discovery — when a wallet enters a market the watcher hasn’t seen before, the enriched transfer event carries full market metadata (
condition_id, market_title, outcomes, token_ids). The watcher creates a new tracked position automatically. No gaps.
refreshInterval
Periodic REST re-fetch of all wallet positions. Acts as a safety net in case a transfer event is missed due to a brief WebSocket disconnection, ensuring the watcher eventually picks up any positions the stream didn’t catch.
Default: 300_000 (5 minutes). Set to 0 to disable, or lower (e.g. 60_000) if you need faster recovery.
Full Example: Notification Service
import { RedemptionWatcher } from 'polynode-sdk';
const watcher = new RedemptionWatcher({
apiKey: process.env.POLYNODE_API_KEY!,
});
watcher.on('error', (err) => console.error('[watcher]', err.message));
watcher.on('ready', () => {
console.log(`Monitoring ${watcher.size} positions across ${watcher.wallets.length} wallets`);
});
// Load wallets from your database
const userWallets = await db.query('SELECT wallet FROM users WHERE active = true');
await watcher.start(userWallets.map(u => u.wallet));
// Process alerts
for await (const alert of watcher) {
await db.insert('redemption_alerts', {
wallet: alert.wallet,
market: alert.marketTitle,
outcome: alert.outcome,
is_winner: alert.isWinner,
payout_usd: alert.estimatedPayoutUsd,
condition_id: alert.conditionId,
block_number: alert.blockNumber,
});
if (alert.isWinner) {
await sendPushNotification(alert.wallet, {
title: 'Position Redeemable!',
body: `"${alert.marketTitle}" resolved ${alert.winningOutcome}. Redeem ~$${alert.estimatedPayoutUsd.toLocaleString()}.`,
});
}
}
Memory Management
The watcher is designed to run indefinitely with bounded memory, even when tracking thousands of wallets.
- Resolved conditions are evicted after alerts fire. A condition only resolves once, so there’s nothing left to watch.
- Zero-size positions are evicted when a wallet fully sells out of a market. If they buy back in, the position is re-created from the next
position_change event or the periodic REST refresh.
- The periodic REST refresh (default: every 5 minutes) acts as a safety net. Even if a stream event is missed, the refresh catches it within one cycle.
Memory usage is proportional to the number of active, non-zero positions across all tracked wallets at any given moment. Historical positions, resolved markets, and fully-sold positions do not accumulate.
API Reference
Constructor
new RedemptionWatcher(config: RedemptionWatcherConfig)
Methods
| Method | Returns | Description |
|---|
start(wallets) | Promise<void> | Fetch positions, subscribe to streams, begin watching |
addWallets(wallets) | Promise<void> | Add wallets at runtime, fetch their positions |
removeWallets(wallets) | void | Remove wallets, clean up state |
positionsFor(wallet) | TrackedPosition[] | Get tracked positions for one wallet |
close() | void | Unsubscribe, disconnect, clean up |
Properties
| Property | Type | Description |
|---|
wallets | string[] | All tracked wallet addresses |
conditions | string[] | All tracked condition IDs |
size | number | Total tracked position count |
Events
| Event | Payload | Description |
|---|
alert | RedeemableAlert | A tracked position’s condition resolved |
ready | (none) | Positions loaded and streams connected |
error | Error | REST fetch failed or stream error |