polymarket-wallet-listener
v0.7.3
Published
polymarket wallet trade listener
Maintainers
Readme
polymarket-wallet-listener
TypeScript SDK for real-time Polymarket wallet monitoring via WebSocket. Watch specific wallets and get notified of trades, splits, merges, redemptions, transfers, resolutions, and more — with full market metadata and real-time pricing from server-side enrichment.
Designed for copy-trading, whale-watching, and position-tracking workflows.
Features
- Typed event callbacks:
sub.traded(cb),sub.splitted(cb),sub.merged(cb), etc. — one method per event type, fully typed - 22 event types: trade, match, cancel, fee, split, merge, redeem, convert, prepare, resolve, transfer, transfer_batch, token_registered, trading_paused, trading_unpaused, order_preapproved, order_preapproval_invalidated, user_paused, user_unpaused, user_pause_block_interval_updated, fee_receiver_updated, max_fee_rate_updated
- Mempool predictor support: every event carries
stage('pending' | 'confirmed' | 'reverted') andeventIdfor optimistic UI and dedup - Server-side enrichment: every event carries
gamma(market metadata) andclob(real-time pricing) fields automatically - Copy-trade primitives on every fill:
event.direction(UP/DOWN) is decidable from a pending fill;event.isIntentflags maker-vs-exchange intent events that carry exact labels. RawtokenId/side/outcome/price/sizeare the on-chain values by default. - Incremental subscriptions: leverages server
extend/excludeprotocol for efficient wallet addition/removal - Per-outcome filtering with side and minimum size thresholds
- Automatic proxy wallet derivation (Gnosis Safe CREATE2) with option to skip
- Multi-wallet subscriptions in a single WebSocket connection
- Auto-reconnect with exponential backoff and jitter
- Keepalive ping/pong health checks
- Works in Node.js and browsers (custom WebSocket constructor supported)
- Dual-format build: CJS + ESM with full TypeScript declarations
Install
npm install polymarket-wallet-listenerQuick Start
Copy-trade pattern (recommended)
For copy-trading, read event.direction — it's the only label decidable from
a pending fill on binary markets. Then resolve to an exact (side, outcome)
action using your bot's local inventory.
import { Watcher } from "polymarket-wallet-listener";
const watcher = new Watcher({ wsUrl: "ws://your-stream-server/ws" });
const sub = watcher.subscribe("0xWhaleAddress");
sub.traded(async (event) => {
// direction is the load-bearing signal:
// UP = leader is acquiring Up exposure (or shedding Down)
// DOWN = leader is acquiring Down exposure (or shedding Up)
if (event.direction == null) return; // gamma missing, can't decide
// Inventory-driven resolution (0xUpQty/0xDownQty are your bot's holdings).
const upQty = await getUpInventory(event.gamma);
const downQty = await getDownInventory(event.gamma);
const action =
event.direction === "DOWN"
? upQty > 0.5
? { side: "Sell", outcome: "Up" }
: { side: "Buy", outcome: "Down" }
: downQty > 0.5
? { side: "Sell", outcome: "Down" }
: { side: "Buy", outcome: "Up" };
console.log(
`COPY ${action.side} ${action.outcome} $${event.size} @ ${event.price}`,
);
});Why default raw, not canonical?
On binary markets a single order_filled log can't distinguish SELL Up
from BUY Down — both produce identical on-chain fills. Pre-0.6 the SDK
guessed and rewrote side/outcome/price/size accordingly, getting it
wrong on ~70% of trades. The 0.6 default exposes raw on-chain values plus
event.direction (decidable from asset flow) and event.isIntent (flags
the maker-side events that DO carry exact labels). Copy-trade bots act on
direction; reconciliation against the activity API also keys on
direction + tokenId.
If you want the canonicalized single-stream perspective for a dashboard, opt in:
const sub = watcher.subscribe("0xWhaleAddress", { canonicalize: true });
// event.side / event.outcome / event.price / event.size are rewritten to canonical perspectiveOther event types
sub.splitted(async (event) => console.log(`SPLIT $${event.amount}`));
// Disposer pattern — call off() to stop listening
const off = sub.merged(async (event) => console.log(`MERGE $${event.amount}`));
// off()
// Clean up all handlers
sub.unwatch();
watcher.close();With watch() catch-all
const sub = watcher.subscribe("0xWhaleAddress", {
events: ["trade", "split", "merge", "redeem"],
});
sub.watch(async (event) => {
switch (event.type) {
case "trade":
console.log(`${event.side} ${event.outcome.name} $${event.size}`);
break;
case "split":
console.log(`Split $${event.amount}`);
break;
}
});With slug-based outcome filtering
import { Watcher, Side } from "polymarket-wallet-listener";
const watcher = new Watcher({
wsUrl: "ws://your-stream-server/ws",
gammaUrl: "https://gamma-api.polymarket.com",
});
const outcomes = await watcher.outcomes("btc-updown-5m-17xx91");
const sub = watcher.subscribe("0xWhaleAddress", {
outcomes: [
{ ...outcomes[0], side: Side.Buy, size: 10 }, // "Up" buys >= $10
{ ...outcomes[1] }, // "Down" all trades
],
});
sub.traded(async (event) => {
console.log(
`${event.side} ${event.outcome.name} $${event.size} @ ${event.price}`,
);
});Architecture
┌──────────────────┐
│ Gamma API │ (optional — for slug-based
│ (market data) │ outcome lookups only)
└────────┬─────────┘
│ outcomes()
▼
┌─────────┐ subscribe() ┌───────────────────────────────────┐
│ Your │ ────────────▶ │ Watcher │
│ Code │ │ │
│ │ ◀──────────── │ ┌─────────┐ ┌─────────────────┐ │
│ │ traded(cb) │ │ Router │ │ ProtocolState │ │
│ │ splitted(cb) │ │ │ │ (extend/exclude │ │
│ │ watch(cb) │ │ │ │ diff engine) │ │
└─────────┘ │ └────┬────┘ └───────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────┐ │
│ │ WebSocket connection │ │
│ └────────────┬───────────────┘ │
└───────────────┼──────────────────┘
│
▼
┌──────────────────────────┐
│ Stream Server │
│ subscribe/extend/exclude│
│ gamma + clob enrichment │
└──────────────────────────┘Flow:
- On
subscribe(), a lazy WebSocket connection is opened to the stream server ProtocolStatecomputes the optimal wire message:subscribe(initial),extend(additions), orexclude(full event type removal). Rapid changes are debounced viaqueueMicrotask.- The server enriches each event with
gamma(market metadata) andclob(real-time pricing) before sending EventRoutermatches incoming events to subscriptions and applies client-side outcome/side/size filters- Matched events are delivered to typed handlers (
traded,splitted, etc.) and thewatch()catch-all - Optionally,
watcher.outcomes(slug)can fetch outcome metadata from the Gamma API for slug-based filtering
API Reference
new Watcher(options)
Creates a new watcher instance. The WebSocket connection is lazy -- it is established on the first subscribe() call.
interface WatcherOptions {
wsUrl: string; // Upstream WebSocket URL
gammaUrl?: string; // Polymarket Gamma API base URL (optional — only for slug lookups)
reconnect?: {
// Reconnection options
enabled?: boolean; // Default: true
baseDelay?: number; // Default: 1000ms
maxDelay?: number; // Default: 60000ms
maxAttempts?: number; // Default: Infinity
jitter?: number; // Default: 0.25 (25% random jitter)
};
keepalive?: {
// Keepalive ping/pong
interval?: number; // Default: 30000ms (30s between pings)
timeout?: number; // Default: 10000ms (10s pong deadline)
};
WebSocket?: unknown; // Custom WebSocket constructor (for browsers or testing)
}Note:
gammaUrlis optional. The stream server enriches every event with market metadata and pricing automatically. You only needgammaUrlif you usewatcher.outcomes(slug)for client-side outcome filtering.
watcher.outcomes(slug, cache?)
Fetch outcome metadata for a market by slug. Returns an array of OutcomeInfo objects containing CLOB token IDs, names, and current prices. Requires gammaUrl in WatcherOptions.
const outcomes = await watcher.outcomes("btc-updown-5m-17xx91");
// [{ id: "241342...", name: "Up", price: "0.65" },
// { id: "101344...", name: "Down", price: "0.35" }]
// Force fresh API call (bypass cache)
const fresh = await watcher.outcomes("btc-updown-5m-17xx91", false);| Param | Type | Default | Description |
| ------- | --------- | -------- | --------------------------------------------- |
| slug | string | required | Market slug (e.g. btc-updown-5m-1774599000) |
| cache | boolean | true | Use cached result if available |
watcher.subscribe(wallet, options?)
Create a subscription for one or more wallets. Triggers lazy WebSocket connection on first call.
// Single wallet, default (trade events only)
const sub = watcher.subscribe("0xAddress");
// Multiple wallets with full filtering
const sub = watcher.subscribe(["0xAddr1", "0xAddr2"], {
outcomes: [{ ...outcomes[0], side: Side.Buy, size: 10 }],
events: ["trade", "split", "merge", "redeem"],
});
// Skip proxy derivation (when address is already a proxy wallet)
const sub = watcher.subscribe("0xProxyAddress", {
events: ["trade"],
skipProxy: true,
});SubscribeOptions:
| Field | Type | Default | Description |
| ----------- | ----------------- | ----------- | ------------------------------------------------------------------------------------ |
| outcomes | OutcomeFilter[] | undefined | Filter by specific outcomes. Omit to receive all. |
| events | EventKind[] | ['trade'] | Event types to subscribe to (see Event Types) |
| skipProxy | boolean | false | Skip proxy address derivation. Use when the input address is already a proxy wallet. |
OutcomeFilter extends OutcomeInfo with optional client-side filters:
| Field | Type | Required | Description |
| ------- | -------- | -------- | ------------------------------------ |
| id | string | yes | CLOB token ID (from outcomes()) |
| name | string | yes | Outcome name (e.g. "Up", "Down") |
| price | string | yes | Current price |
| side | Side | no | Filter by Side.Buy or Side.Sell |
| size | number | no | Minimum USDC amount threshold |
Filtering behavior:
- Server-side: Address filters (maker/taker/stakeholder/redeemer) are always applied on the server. If all outcome filters have a
sizethreshold, the minimum is sent as a server-sideusdc_amount >= Nfilter. - Client-side: Outcome token matching, side filtering, and per-outcome size filtering are applied by the SDK's
EventRouter.
Typed Event Callbacks
Each method registers a typed handler and auto-subscribes to the event type. Returns a disposer function. Multiple handlers per event are supported (additive). One method per event type (15 total).
| Method | Event Type |
| --------------------------------------- | ------------------------------------ |
| sub.traded(cb) | TradeEvent |
| sub.splitted(cb) | SplitEvent |
| sub.merged(cb) | MergeEvent |
| sub.redeemed(cb) | RedeemEvent |
| sub.matched(cb) | MatchEvent |
| sub.cancelled(cb) | CancelEvent |
| sub.fee(cb) | FeeEvent |
| sub.converted(cb) | ConvertEvent |
| sub.prepared(cb) | PrepareEvent |
| sub.resolved(cb) | ResolveEvent |
| sub.transferred(cb) | TransferEvent |
| sub.transferredBatch(cb) | TransferBatchEvent |
| sub.tokenRegistered(cb) | TokenRegisteredEvent |
| sub.tradingPaused(cb) | TradingPausedEvent |
| sub.tradingUnpaused(cb) | TradingUnpausedEvent |
| sub.orderPreapproved(cb) | OrderPreapprovedEvent |
| sub.orderPreapprovalInvalidated(cb) | OrderPreapprovalInvalidatedEvent |
| sub.userPaused(cb) | UserPausedEvent |
| sub.userUnpaused(cb) | UserUnpausedEvent |
| sub.userPauseBlockIntervalUpdated(cb) | UserPauseBlockIntervalUpdatedEvent |
| sub.feeReceiverUpdated(cb) | FeeReceiverUpdatedEvent |
| sub.maxFeeRateUpdated(cb) | MaxFeeRateUpdatedEvent |
// Auto-subscribes to 'trade' event type on the wire
const offTrade = sub.traded(async (event) => {
// event is TradeEvent — TypeScript knows all fields
const label = event.side === "Buy" ? "BUY" : "SELL";
console.log(`${label} ${event.outcome.name} $${event.size} @ ${event.price}`);
});
// Multiple handlers for same event type — both fire
sub.traded(async (event) => {
await sendAlert(event);
});
// Dispose a specific handler
offTrade();Auto-subscribe: calling sub.traded() automatically adds 'trade' to the wire subscription. You don't need to pass events: ['trade'] in SubscribeOptions. When the disposer is called and no handlers remain for that event type, it is automatically removed from the subscription.
subscription.watch(callback)
Catch-all callback. Receives all WatcherEvent types — use a switch statement to narrow. Coexists with typed callbacks: watch() fires first, then typed handlers.
sub.watch(async (event) => {
switch (event.type) {
case "trade":
console.log(`${event.side} ${event.outcome.name} $${event.size}`);
break;
default:
console.log(`${event.type} | tx ${event.tx}`);
break;
}
});The callback can be sync or async. Errors are caught and logged via console.warn.
subscription.unwatch()
Stop receiving events. Removes the catch-all callback and all typed handlers. Updates the upstream WebSocket subscription.
subscription.id
Unique subscription identifier (UUID v4).
subscription.wallets
Normalized wallet addresses being watched.
subscription.proxyWallets
Derived proxy wallet addresses (empty when skipProxy: true).
watcher.on(event, listener) / watcher.off(event, listener)
Register or remove lifecycle event listeners.
watcher.close()
Close the WebSocket connection and clean up all timers and state. Emits a disconnected event.
Event Types
The SDK surfaces 22 typed event variants, each with a dedicated subscription
helper (sub.traded(cb), sub.splitted(cb), …) and a discriminated union
member (event.type === 'trade'). The table below is an at-a-glance index;
detailed per-event field docs follow it.
| Group | event.type | Helper | What it represents |
| --- | --- | --- | --- |
| Trading | 'trade' | sub.traded() | A fill on the user's order — buy or sell, single fill or one slice of a matchOrders call. Carries direction, side, price, size, outcome, and the isIntent flag for taker-side authoritative rollups. |
| Trading | 'match' | sub.matched() | Per-pair match log emitted by the V1 exchange's OrdersMatched. Less common in V2 (use 'trade' instead). |
| Trading | 'cancel' | sub.cancelled() | An order was cancelled on-chain (admin / user). |
| Trading | 'fee' | sub.fee() | A fee accrual log — exchange fee receiver and amount. |
| Position | 'split' | sub.splitted() | User split USDC into a YES+NO pair via splitPosition (CT or NRA). Carries conditionId, collateralAmount, negRisk, and the on-chain stakeholder. |
| Position | 'merge' | sub.merged() | Inverse of split — burned the YES+NO pair to get USDC back. |
| Position | 'redeem' | sub.redeemed() | Redeemed payout after a market resolved. |
| Position | 'convert' | sub.converted() | NegRisk conversion across an index set (multi-outcome → single position). |
| Resolution | 'prepare' | sub.prepared() | A new condition was prepared on the CT framework (market created upstream). |
| Resolution | 'resolve' | sub.resolved() | A condition was resolved with payout numerators set. |
| Transfer | 'transfer' | sub.transferred() | ERC-1155 TransferSingle involving the watched wallet. |
| Transfer | 'transfer_batch' | sub.transferredBatch() | ERC-1155 TransferBatch involving the watched wallet. |
| Admin | 'token_registered' | sub.tokenRegistered() | A new outcome token was registered on the exchange. |
| Admin | 'trading_paused' | sub.tradingPaused() | Global trading paused. |
| Admin | 'trading_unpaused' | sub.tradingUnpaused() | Global trading resumed. |
| V2 admin | 'order_preapproved' | sub.orderPreapproved() | A V2 order was preapproved off the wallet's signer. |
| V2 admin | 'order_preapproval_invalidated' | sub.orderPreapprovalInvalidated() | A previously preapproved order was invalidated. |
| V2 admin | 'user_paused' | sub.userPaused() | A specific wallet was paused (signer-level circuit breaker). |
| V2 admin | 'user_unpaused' | sub.userUnpaused() | A specific wallet was un-paused. |
| V2 admin | 'user_pause_block_interval_updated' | sub.userPauseBlockIntervalUpdated() | The user-pause cooldown interval changed. |
| V2 admin | 'fee_receiver_updated' | sub.feeReceiverUpdated() | Exchange fee receiver address changed. |
| V2 admin | 'max_fee_rate_updated' | sub.maxFeeRateUpdated() | Max protocol fee rate changed. |
sub.watch(cb) is the catch-all that fires for any event type; use it
when you want to write a single discriminated-union handler.
Every event carries the following common fields:
{
tx: string // Transaction hash
block: number | null // null when stage === 'pending'
timestamp: number // Local Date.now() at receipt
stage: 'pending' | 'confirmed' | 'reverted' // Mempool predictor envelope
eventId: string // 32-byte deterministic content hash
gamma?: GammaEnrichment | null // Market metadata
clob?: ClobEnrichment | null // Real-time pricing
raw: object // Full upstream event
}Stage semantics (server-side mempool predictor):
'pending'— observed in the mempool but not yet mined;blockisnull'confirmed'— mined into a confirmed block;blockis set'reverted'— was pending but the transaction reverted or was replaced
For optimistic UI, render 'pending' immediately and reconcile when the matching eventId arrives with 'confirmed' or 'reverted'. The server emits each eventId only once per stage.
Trading Events
TradeEvent ('trade')
Emitted when a watched wallet buys or sells an outcome token (order_filled).
{
type: 'trade'
wallet: string // Address that matched (maker or taker)
market: string // Market slug (e.g. btc-updown-5m-1777723200)
outcome: OutcomeInfo // { id, name, price } — best-effort label on binary markets
side: 'Buy' | 'Sell' // best-effort wallet-perspective; see `direction`
size: number // raw collateral_amount (USDC/pUSD)
collateralAmount: number // same as size
price: number // raw on-chain price
direction: // PRIMARY copy-trade signal — decidable from any fill
| 'UP'
| 'DOWN'
| 'COLLATERAL'
| null
isIntent: boolean // true when counterparty is a known exchange contract
// (carries exact (side, outcome) label, confirmed-only)
canonicalized: boolean // true only when caller passed `canonicalize: true`
// AND the trade was on the binary complement outcome
negRisk: boolean // true if fill from NegRisk CTF Exchange
tokenId: string // gamma-canonical id (matches activity API .asset)
// raw chain id available via event.raw.token_id
builder: string // V2 builder address (zero when not built via builder)
metadata: string // V2 metadata field
tx: string
block: number | null
timestamp: number
stage: 'pending' | 'confirmed' | 'reverted'
eventId: string
}Direction: derived from asset flow (which clob token id the wallet gives
vs. receives). On any binary market with two outcomes, UP means the
wallet's exposure is moving toward gamma.outcomes[0]. This is the only
label that's 100% reliable on a pending fill — side and outcome have
complement-pair ambiguity (SELL Up and BUY Down produce identical
fills).
Intent events (isIntent: true): occur when taker is one of the four
canonical Polymarket exchange contracts (CTF v1/v2, NegRisk v1/v2). These
are the leader's order-setup events; they carry the exact (side, outcome)
label authoritatively, but only at the confirmed stage (~2s after pending),
so they are useful for reconciliation rather than live execution.
Counterparty: not exposed. Use event.tx against a chain-data API
(Polymarket activity API or Polygonscan) if you need it.
Opt-in canonicalization: passing canonicalize: true to subscribe()
rewrites side/outcome/price/size
into a single canonical-outcome perspective when the trade was on the
complement, and canonicalized: true flags those rows. Useful for
dashboards that want one stream per market; never use this for
copy-trading.
MatchEvent ('match')
Summary of a matchOrders call.
{
type: "match";
wallet: string; // taker_order_maker
takerOrderHash: string;
makerAssetId: string;
takerAssetId: string;
makerAmountFilled: string;
takerAmountFilled: string;
tokenId: string; // Outcome token id (V2 OrdersMatched)
side: "Buy" | "Sell"; // Taker side (V2 OrdersMatched)
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}CancelEvent ('cancel')
Maker cancelled their order on-chain. Broadcast to all subscriptions (no wallet-address filter).
{
type: "cancel";
orderHash: string;
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}FeeEvent ('fee')
Fee collected from a trade.
{
type: "fee";
receiver: string;
tokenId: string;
amount: string;
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}Position Events
SplitEvent ('split')
Wallet locked USDC to mint YES+NO outcome tokens (entering a market).
{
type: "split";
wallet: string; // actual user EOA (from server enrichment)
stakeholder: string; // raw stakeholder from event (NRA = exchange addr)
conditionId: string;
amount: number; // USDC locked (raw / 1e6)
collateralAmount: number; // same as amount (canonical name)
source: string; // contract address (CT or NRA)
negRisk: boolean; // true if from NegRisk Adapter
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}MergeEvent ('merge')
Wallet burned YES+NO outcome tokens to release USDC (exiting a market).
{
type: "merge";
wallet: string; // actual user EOA (from server enrichment)
stakeholder: string; // raw stakeholder from event (NRA = exchange addr)
conditionId: string;
amount: number; // USDC released (raw / 1e6)
collateralAmount: number; // same as amount (canonical name)
source: string; // contract address (CT or NRA)
negRisk: boolean; // true if from NegRisk Adapter
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}RedeemEvent ('redeem')
Wallet claimed payout after market resolution.
{
type: "redeem";
wallet: string; // actual user EOA (from server enrichment)
conditionId: string;
payout: number; // USDC claimed (raw / 1e6)
collateralAmount: number; // same as payout (canonical name)
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}ConvertEvent ('convert')
NO tokens converted to YES tokens in multi-outcome (neg-risk) markets.
{
type: "convert";
wallet: string;
marketId: string;
indexSet: string;
amount: number; // USDC value (raw / 1e6)
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}Resolution Events
PrepareEvent ('prepare')
New condition created. Broadcast to all subscriptions.
{
type: "prepare";
conditionId: string;
oracle: string;
questionId: string;
outcomeSlotCount: string;
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}ResolveEvent ('resolve')
Condition resolved with payout numerators. Broadcast to all subscriptions.
{
type: 'resolve'
conditionId: string
oracle: string
questionId: string
outcomeSlotCount: string
payoutNumerators: string[]
tx: string
block: number | null
timestamp: number
stage: 'pending' | 'confirmed' | 'reverted'
eventId: string
}Transfer Events
TransferEvent ('transfer')
Single ERC-1155 token transfer. Matched if from or to is a watched wallet.
{
type: "transfer";
operator: string;
from: string;
to: string;
tokenId: string;
value: string;
tx: string;
block: number | null;
timestamp: number;
stage: "pending" | "confirmed" | "reverted";
eventId: string;
}TransferBatchEvent ('transfer_batch')
Batch ERC-1155 token transfer. Matched if from or to is a watched wallet.
{
type: 'transfer_batch'
operator: string
from: string
to: string
ids: string[]
values: string[]
tx: string
block: number | null
timestamp: number
stage: 'pending' | 'confirmed' | 'reverted'
eventId: string
}Admin Events
These are broadcast to all subscriptions (no wallet-address filter).
TokenRegisteredEvent ('token_registered')
{ type: 'token_registered', token0: string, token1: string, conditionId: string, tx, block, timestamp }TradingPausedEvent ('trading_paused')
{ type: 'trading_paused', pauser: string, tx, block, timestamp }TradingUnpausedEvent ('trading_unpaused')
{ type: 'trading_unpaused', unpauser: string, tx, block, timestamp }V2 Admin Events
Polymarket V2 exchange admin events. All include the common fields (tx, block, timestamp, stage, eventId).
OrderPreapprovedEvent ('order_preapproved')
Broadcast — emitted when an order hash is preapproved on the V2 exchange.
{ type: 'order_preapproved', orderHash: string }OrderPreapprovalInvalidatedEvent ('order_preapproval_invalidated')
Broadcast — emitted when a previously preapproved order is invalidated.
{ type: 'order_preapproval_invalidated', orderHash: string }UserPausedEvent ('user_paused')
Address-routed (matched on user).
{ type: 'user_paused', user: string, effectivePauseBlock: string }UserUnpausedEvent ('user_unpaused')
Address-routed (matched on user).
{ type: 'user_unpaused', user: string }UserPauseBlockIntervalUpdatedEvent ('user_pause_block_interval_updated')
Broadcast.
{ type: 'user_pause_block_interval_updated', oldInterval: string, newInterval: string }FeeReceiverUpdatedEvent ('fee_receiver_updated')
Broadcast.
{ type: 'fee_receiver_updated', feeReceiver: string }MaxFeeRateUpdatedEvent ('max_fee_rate_updated')
Broadcast.
{ type: 'max_fee_rate_updated', maxFeeRate: string }Server Configuration
The polymarket-stream server has feature flags that affect what the SDK receives:
WALLET_RESOLUTION(defaultfalse) — when enabled, the server resolves the actual user EOA viaeth_getTransactionByHashfor split/merge/redeem events and injects it as thewalletenrichment field. The SDK falls back tostakeholder/redeemerwhen this is off.MEMPOOL_DISABLED(defaultfalse) — whenfalse, the server's mempool predictor emits'pending'stage events ahead of confirmation. Set totrueto receive only'confirmed'events.
Optimistic UI Pattern
const pending = new Map<string, TradeEvent>();
sub.traded((event) => {
if (event.stage === "pending") {
pending.set(event.eventId, event);
renderOptimistic(event);
return;
}
if (event.stage === "reverted") {
pending.delete(event.eventId);
rollback(event.eventId);
return;
}
// confirmed
pending.delete(event.eventId);
renderConfirmed(event);
});Lifecycle Events
watcher.on("connected", () => {
console.log("WebSocket connected");
});
watcher.on("disconnected", (code: number, reason: string) => {
console.log(`Disconnected: ${code} ${reason}`);
});
watcher.on("reconnecting", (attempt: number, delay: number) => {
console.log(`Reconnecting #${attempt} in ${delay}ms`);
});
watcher.on("error", (err: Error) => {
// err is one of: ConnectionError, ReconnectError, ProtocolError, ServerError
console.error(err.name, err.message);
});
// Debug event — useful for development diagnostics
watcher.on("debug", (label: string, data: string) => {
console.log(`[debug:${label}]`, data);
});Wallet Resolution
Polymarket uses Gnosis Safe proxy wallets. Users have two addresses:
- EOA -- the user's externally owned account, shown on Polymarket profiles
- Proxy -- the Gnosis Safe proxy that appears in on-chain events, derived via CREATE2
By default, the SDK derives the proxy address from any input and subscribes to both forms, so you don't need to know which type of address you have.
import {
deriveProxyAddress,
normalizeAddress,
} from "polymarket-wallet-listener";
const proxy = deriveProxyAddress("0x25d76e8eaF02494c31Cda797E58364874e598333");
// '0xdbCb463dB35Ad1a011B45e40154fc939CCDD665E'
const norm = normalizeAddress("0x25D76E8EAF02494C31CDA797E58364874E598333");
// '0x25d76e8eaf02494c31cda797e58364874e598333'Important: If the address you're watching is already a proxy wallet (e.g. from the proxyWallet field in the Polymarket data API), use skipProxy: true to avoid deriving a proxy-of-a-proxy:
// Address from data-api proxyWallet field -- already a proxy
const sub = watcher.subscribe("0xProxyWallet", {
skipProxy: true,
events: ["trade"],
});How to tell if an address is a proxy: If you obtained the address from the Polymarket Data API's proxyWallet field or from on-chain maker/taker fields, it is a proxy. If you got it from a Polymarket profile URL or user lookup, it is likely an EOA.
Error Handling
The SDK exports a typed error hierarchy:
import {
WatcherError, // Base class
ConnectionError, // WebSocket creation or pong timeout failures
ReconnectError, // Max reconnect attempts exceeded
ProtocolError, // Failed to parse server message
ServerError, // Server returned an error message
} from "polymarket-wallet-listener";
watcher.on("error", (err) => {
if (err instanceof ReconnectError) {
console.error(`Gave up after ${err.attempt} attempts`);
} else if (err instanceof ConnectionError) {
console.error("Connection issue:", err.message);
}
});Enrichment Types
Every event carries optional gamma and clob fields populated by the stream server's enrichment system:
GammaEnrichment
Market metadata from the Gamma API (30+ fields). Key fields:
| Field | Type | Description |
| ------------------- | ----------------- | ------------------------------------- |
| question | string \| null | Market question text |
| slug | string \| null | URL slug |
| outcomes | string[] | Outcome labels (e.g. ["Yes", "No"]) |
| outcome_prices | string[] | Current prices |
| clob_token_ids | string[] | CLOB token IDs |
| condition_id | string \| null | On-chain condition ID |
| volume_24hr | number \| null | 24-hour volume |
| liquidity | number \| null | Current liquidity |
| active / closed | boolean \| null | Market status |
| event_title | string \| null | Parent event title |
See GammaEnrichment type for the full list.
ClobEnrichment
Real-time order book pricing:
| Field | Type | Description |
| ------------------ | ----------------- | ----------------------- |
| token_id | string | CLOB token ID |
| best_bid | string \| null | Best bid price |
| best_ask | string \| null | Best ask price |
| midpoint | string \| null | Midpoint price |
| last_trade_price | string \| null | Last trade price |
| tick_size | string \| null | Minimum price increment |
| neg_risk | boolean \| null | Negative risk flag |
Exports
// Classes
export { Watcher, Subscription };
// Enums & Constants
export { Side }; // Side.Buy | Side.Sell
export { EVENT_KIND_TO_WIRE }; // EventKind → server wire type mapping
// Utilities
export { deriveProxyAddress, normalizeAddress };
// Errors
export {
WatcherError,
ConnectionError,
ReconnectError,
ProtocolError,
ServerError,
};
// Types
export type {
// Enrichment
GammaEnrichment,
ClobEnrichment,
GammaSeries,
GammaTag,
// Events
WatcherEvent,
WatcherEventMap,
TypedHandler,
TradeEvent,
MatchEvent,
CancelEvent,
FeeEvent,
SplitEvent,
MergeEvent,
RedeemEvent,
ConvertEvent,
PrepareEvent,
ResolveEvent,
TransferEvent,
TransferBatchEvent,
TokenRegisteredEvent,
TradingPausedEvent,
TradingUnpausedEvent,
// Protocol
ExtendMessage,
ExcludeMessage,
ProtocolMessage,
// Configuration
OutcomeInfo,
OutcomeFilter,
SubscribeOptions,
WatcherOptions,
EventKind,
ReconnectOptions,
KeepaliveOptions,
LifecycleEvent,
LifecycleEventMap,
};Examples
The repo ships six runnable example scripts under examples/. Each is
wired to an npm run entry so the script can be invoked without a
tsx install dance.
| Script | Source | Purpose |
| --- | --- | --- |
| example:watch | examples/watch.ts | Subscribe to a single wallet and print every event. Default entry point for kicking the tyres. Set DEBUG=1 for raw frame logs. |
| example:multiple | examples/multiple.ts | Subscribe several wallets to one market slug at once. Demonstrates per-outcome filtering and slug-resolution. |
| example:incremental | examples/incremental.ts | Demonstrates the incremental subscribe protocol — adding/removing wallets from a live subscription without dropping the connection. |
| example:verify | examples/verify.ts | Capture trade events for a wallet over a window, then cross-check against the Polymarket activity API by tx hash. Grades direction + tokenId + wallet + price + shares. Snapshot/replay supported. |
| example:verify-loop | examples/verify-loop.ts | Continuous verify runner — re-captures every cycle so a wallet can be soak-tested indefinitely. Useful for catching SDK regressions against a live wallet. |
| example:verify-split | examples/verify-split.ts | Counterpart to verify for split / merge events. Grades kind + wallet + conditionId + collateralAmount against the activity API's SPLIT / MERGE entries. |
Run the example watcher
cd sdk
# Watch all markets for a wallet (no slug needed)
npm run example:watch -- 0xWalletAddress
# With debug logging
DEBUG=1 npm run example:watch -- 0xWalletAddress
# Multiple wallets on a specific market (slug-based filtering)
npm run example:multiple -- 0xWallet1 0xWallet2 market-slug
# Demonstrate incremental wallet add/remove on a live subscription
npm run example:incremental
# Cross-check captured trade events against the Polymarket activity API
# (captures for --duration seconds, waits 60s, then verifies by tx hash)
npm run example:verify -- --wallet 0xProxyWallet --duration 30
# Same shape, but for split/merge events
npm run example:verify-split -- --wallet 0xProxyWallet --duration 60
# Re-run verify on a saved snapshot (skips capture + indexer wait)
npm run example:verify -- --replay verify-snapshot-0xabc...jsonl
# Continuous verify loop — keeps re-capturing until you Ctrl+C
npm run example:verify-loop -- --wallet 0xProxyWallet --duration 60Copy-trade pattern
const watcher = new Watcher({ wsUrl });
const sub = watcher.subscribe("0xWhale");
sub.traded(async (event) => {
console.log(
`COPY ${event.side} ${event.outcome.name} $${event.size} @ ${event.price}`,
);
await executeTrade(event.outcome.id, event.side, event.size);
});Watch proxy wallet directly
const watcher = new Watcher({ wsUrl });
const sub = watcher.subscribe("0xProxyWallet", {
events: ["trade", "split", "merge", "redeem"],
skipProxy: true,
});
sub.watch((event) => {
if (event.type === "trade") {
console.log(
`${event.market} | ${event.side} ${event.outcome.name} $${event.size.toFixed(2)}`,
);
console.log(` bid: ${event.clob?.best_bid} ask: ${event.clob?.best_ask}`);
}
});Multi-wallet monitoring
const watcher = new Watcher({ wsUrl });
const sub = watcher.subscribe(["0xWhale1", "0xWhale2", "0xWhale3"], {
events: ["trade"],
});
sub.watch((event) => {
if (event.type === "trade") {
console.log(
`[${event.wallet}] ${event.market} | ${event.side} ${event.outcome.name} $${event.size}`,
);
}
});Market resolution alerts
const watcher = new Watcher({ wsUrl });
watcher
.subscribe("0xAddr", {
events: ["trade", "resolve"],
})
.watch((event) => {
if (event.type === "resolve") {
console.log(
`RESOLVED: ${event.gamma?.question} → payouts [${event.payoutNumerators}]`,
);
}
});Whale alert (size filter with slug)
const watcher = new Watcher({ wsUrl, gammaUrl });
const outcomes = await watcher.outcomes("will-trump-win-2024");
const sub = watcher.subscribe("0xWhale", {
outcomes: outcomes.map((o) => ({ ...o, size: 1000 })),
events: ["trade"],
});
sub.watch((event) => {
if (event.type === "trade") {
console.log(
`WHALE ALERT: ${event.side} ${event.outcome.name} $${event.size.toFixed(2)}`,
);
}
});Graceful shutdown
const watcher = new Watcher({ wsUrl });
const sub = watcher.subscribe("0xAddr", { events: ["trade"] });
sub.watch((event) => {
/* ... */
});
process.on("SIGINT", () => {
sub.unwatch();
watcher.close();
process.exit(0);
});Development
cd sdk
# Install dependencies
npm install
# Build (CJS + ESM + .d.ts)
npm run build
# Watch mode
npm run dev
# Type check
npm run lint
# Run examples
DEBUG=1 npm run example:watch -- 0xAddress
npm run example:multiple -- 0xAddr1 0xAddr2 market-slugWire Protocol
The SDK communicates with the upstream stream server over WebSocket using JSON messages.
Subscribe (full state, sent on first connection or reconnect):
{
"action": "subscribe",
"subscriptions": [
{
"event_type": "order_filled",
"filters": [{ "field": "maker", "op": "eq", "value": "0xAddr" }]
},
{ "event_type": "condition_resolution" }
]
}Extend (incremental, sent when adding wallets or event types):
{
"action": "extend",
"subscriptions": [
{
"event_type": "order_filled",
"filters": [{ "field": "taker", "op": "eq", "value": "0xNew" }]
}
]
}Exclude (remove entire event types):
{ "action": "exclude", "event_types": ["condition_resolution"] }The SDK automatically chooses the optimal action: extend for pure additions, exclude when removing entire event types, and full subscribe rebuild for partial removals. Multiple rapid changes are debounced via queueMicrotask.
Server responses:
{"type": "subscribed", "event_types": [...], "subscriptions": [...]}-- acknowledged (for subscribe, extend, and exclude){"type": "event", "data": { ..., "gamma": {...}, "clob": {...} }}-- enriched event{"type": "pong"}-- keepalive response{"type": "error", "message": "..."}-- server error
Supported filter operators: eq, ne, gt, gte, lt, lte
Event type mapping (SDK → wire):
| SDK EventKind | Wire Type |
| ----------------------------------- | ----------------------------------- |
| trade | order_filled |
| match | orders_matched |
| cancel | order_cancelled |
| fee | fee_charged |
| split | position_split |
| merge | positions_merge |
| redeem | payout_redemption |
| convert | positions_converted |
| prepare | condition_preparation |
| resolve | condition_resolution |
| transfer | transfer_single |
| transfer_batch | transfer_batch |
| token_registered | token_registered |
| trading_paused | trading_paused |
| trading_unpaused | trading_unpaused |
| order_preapproved | order_preapproved |
| order_preapproval_invalidated | order_preapproval_invalidated |
| user_paused | user_paused |
| user_unpaused | user_unpaused |
| user_pause_block_interval_updated | user_pause_block_interval_updated |
| fee_receiver_updated | fee_receiver_updated |
| max_fee_rate_updated | max_fee_rate_updated |
License
MIT
