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

polynode-sdk

v0.14.2

Published

TypeScript SDK for the Polynode real-time Polymarket API

Readme

polynode-sdk

TypeScript SDK for the Polynode real-time Polymarket API.

Stream settlements, trades, positions, deposits, oracle events, orderbook updates, and more through a single WebSocket connection. All events enriched with full market metadata.

New in v0.14.0: Normal web applications can now take a connected wallet from user-owned onboarding through an exact V2 order with a backend-vault signing bridge or the dedicated browser-memory entry point. The SDK provides canonical preview confirmation, wallet/chain lifecycle checks, one-time keyed backend state, zero builder attribution, collateral preflight, and a stable cross-SDK browser contract.

New in v0.14.1: The browser session now accepts viem's standard EIP-1193 provider type directly, reports both USDC.e and pUSD balances, wraps funding-address USDC.e into pUSD with wallet-confirmed EOA or deposit-wallet transactions, and exposes authenticated open orders for conservative timeout reconciliation.

New in v0.14.2: Prepared backend orders expose the canonical exchange orderHash, and browser-memory orders provide an awaited, credential-free beforeSubmit hook. Platforms can durably record the exact CLOB/open-order and on-chain fill identity before the SDK sends the order.

New in v0.13.1: V2 user-owned onboarding defaults new users to the order-capable deposit wallet, preserves existing deterministic Safe makers, and exposes authoritative per-order status and outcome-balance reads.

New in v0.13.0: Trading adds explicit user_owned execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.

New in v0.12.0: TypeScript now provides the same core capabilities as the Python and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal strings remain exact, and any local queue eviction is reported.

New in v0.11.0: Current-production parity. Trading now defaults to CLOB V2 on clob.polymarket.com, uses Polynode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour market streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket types, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.

New in v0.10.19: POLY_1271 V2 order signatures now normalize the ERC-7739 TypedDataSign recovery byte to Ethereum v=27/28 for on-chain ERC-1271 validation.

In v0.10.18: Polymarket V2 deposit-wallet trading fixes. ensureReady() detects deployed POLY_1271 wallets correctly, V2 type-3 orders use the deposit wallet as both maker and signer, and existing local credentials can be repaired by rerunning ensureReady().

In v0.10.17: WebSocket subscription resilience — event subscriptions preserve frames that arrive before the subscribe ack, orderbook subscribe() waits for the server ack, reconnects replay subscriptions in place, and orderbook clients can partially unsubscribe without closing the stream.

In v0.10.13: V3 historical data and Polymarket profile helpers — use pn.v3 for wallet P&L, enriched positions, builder analytics, market search, and username setup while existing top-level methods keep their legacy behavior.

In v0.9: V2 order flow introduced pUSD-collateralized orders with optional builder attribution.

In v0.4: Local Cache — SQLite-backed local storage. Backfill wallet history in seconds, query trades and positions instantly with zero API calls.

Install

npm install polynode-sdk ws

# For local cache (optional):
npm install better-sqlite3

Requires Node.js 18+.

Quick Start

import { PolyNode } from 'polynode-sdk';

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

// Fetch top markets
const markets = await pn.markets({ count: 10 });
console.log(`${markets.count} markets, ${markets.total} total`);

// Search
const results = await pn.search('bitcoin');
console.log(results.results[0].question);

REST API

// System
await pn.healthz();
await pn.status();
await pn.connections();
await pn.createKey('my-bot');

// Markets
await pn.markets({ count: 10 });
await pn.market(tokenId);
await pn.marketBySlug('bitcoin-100k');
await pn.marketByCondition(conditionId);
await pn.marketsList({ count: 20, sort: 'volume' });
await pn.search('ethereum', { limit: 5 });

// Pricing
await pn.candles(tokenId, { resolution: '1h', limit: 100 });
await pn.stats(tokenId);

// Settlements
await pn.recentSettlements({ count: 20 });
await pn.tokenSettlements(tokenId, { count: 10 });
await pn.walletSettlements(address, { count: 10 });

// Wallets
await pn.wallet(address);
await pn.resolve('Fredi9999');
await pn.walletPositions(address, { redeemable: true, conditionId });
await pn.multiWalletPositions([address, secondAddress], { limit: 100 });
await pn.marketPositions(conditionId, { sortBy: 'CURRENT_VALUE', minSize: 0.01 });
await pn.walletOnchainPositions(address, { tagSlug: 'Crypto' });

// V3 historical data
await pn.v3.wallet(address);
await pn.v3.walletTrades(address, { groupBy: 'user_trade', limit: 100 });
await pn.v3.walletPositions(address, { status: 'open', sort: 'size' });
await pn.v3.searchMarkets({ query: 'bitcoin', limit: 10 });
await pn.v3.builderTrades(builderCode, { eventSlug: 'who-will-win-the-2026-world-cup' });

// V3 Polymarket profiles
await pn.v3.polymarketUsernameAvailable('alice123');
const challenge = await pn.v3.createPolymarketUsernameChallenge({
  address: userEoa,
  username: 'alice123',
});
await pn.v3.completePolymarketUsername({
  challenge_id: challenge.challenge_id,
  address: userEoa,
  username: 'alice123',
  polymarket_signature: '0x...',
  consent_signature: '0x...',
});

// RPC (rpc.polynode.dev)
await pn.rpc('eth_blockNumber');
await pn.rpc('eth_getBlockByNumber', ['latest', false]);

Complete V3 route access

V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. Named helpers cover common TypeScript calls, while execute() gives you access to all 120 current V3 operations without waiting for a new helper method.

console.log(pn.v3.operations.length); // 120

const comboActivity = await pn.v3.execute('GET /v3/combos/activity', {
  query: { limit: 25 },
});

const walletRewards = await pn.v3.execute(
  'GET /v3/wallets/{address}/rewards',
  { pathParams: { address }, query: { limit: 100 } },
);

The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor Retry-After; requests that change data are never retried automatically. ApiError exposes the status, request ID, retry details, and a request URL with credentials removed.

WebSocket Streaming

const sub = await pn.ws.subscribe('settlements')
  .minSize(100)
  .status('pending')
  .snapshotCount(20)
  .send();

sub.on('settlement', (event) => {
  console.log(`${event.taker_side} $${event.taker_size} on ${event.market_title}`);
});

sub.on('status_update', (event) => {
  console.log(`Confirmed in ${event.latency_ms}ms`);
});

// Or use async iterator
for await (const event of sub) {
  if (event.event_type === 'settlement') {
    console.log(event.taker_wallet, event.taker_size);
  }
}

Subscription Types

pn.ws.subscribe('settlements');   // pending + confirmed settlements
pn.ws.subscribe('trades');        // all trade activity
pn.ws.subscribe('prices');        // price-moving events
pn.ws.subscribe('dome');          // flat Dome-compatible fills
pn.ws.subscribe('fills');         // alias for flat fill events
pn.ws.subscribe('combos');        // PM2 combo executions + status updates
pn.ws.subscribe('redemptions');   // redemption events
pn.ws.subscribe('deposits');      // deposit/withdrawal events
pn.ws.subscribe('blocks');        // new Polygon blocks
pn.ws.subscribe('wallets');       // all wallet activity
pn.ws.subscribe('markets');       // all market activity
pn.ws.subscribe('large_trades');  // $1K+ trades
pn.ws.subscribe('oracle');        // UMA resolution events
pn.ws.subscribe('chainlink');     // real-time price feeds

dome and fills change settlement delivery into a flat, per-fill wire format. Use one of those presets on a dedicated PolyNodeWS connection when also consuming non-fill events; the server deduplicates delivery per connection and cannot deliver both wire formats for the same settlement.

Subscription Filters

pn.ws.subscribe('settlements')
  .wallets(['0xabc...'])
  .tokens(['21742633...'])
  .slugs(['bitcoin-100k'])
  .conditionIds(['0xabc...'])
  .side('BUY')
  .status('pending')
  .minSize(100)
  .maxSize(10000)
  .eventTypes(['settlement'])
  .snapshotCount(50)
  .since(Date.now() - 30_000)
  .send();

Current PM2 filters are also available through comboConditionIds(), legPositionIds(), eventIds(), moduleIds(), action(), and direction().

Reconnect and delivery behavior

Why: a reconnect can overlap the last event or exceed the server's retained history. The SDK resubscribes with the latest accepted timestamp, deduplicates the overlap, preserves unknown additive events, and reports replay state. Replay is best effort, not an unbounded gapless guarantee.

pn.ws.onReplay((notice) => {
  console.log(notice.phase, notice.since, notice.guaranteed, notice.warning);
});

sub.onOverflow((overflow) => {
  console.warn('local iterator queue evicted events', overflow.droppedEvents);
});

Chainlink TWAP and short-form markets

The TWAP values are lookback windows, not update cadence: 5-minute markets use 30 seconds; 15-minute and 4-hour markets use 60 seconds.

const prices = await pn.ws.subscribe('chainlink')
  .feeds(['BTC/USD', 'ETH/USD'])
  .twapWindows([30])
  .send();

console.log(prices.priceSource, prices.twapWindows, prices.warnings);

prices.on('price_feed', (event) => {
  if (event.is_twap) console.log(event.feed, event.twap_window_seconds, event.price);
});

const stream = pn.ws.shortForm('5m', { coins: ['btc', 'eth'] });
stream.on('rotation', ({ markets }) => console.log(markets.map((market) => market.slug)));
stream.on('price_feed', (event) => console.log(event.feed, event.price));
stream.on('settlement', (event) => console.log(event.market_slug, event.status));

A Chainlink selection is scoped to its WebSocket connection, so combine feeds and windows into one Chainlink subscription per connection. The resolved subscription exposes the server acknowledgement through priceSource, twapWindows, and warnings. shortForm() handles rotation safely with its own socket. At each market boundary it closes that socket, discovers the new slugs, reconnects, and subscribes to the exact settlement and TWAP filters again.

V3 perps WebSocket

The V3 perps WebSocket streams tickers, best bid/offer, full books, trades, statistics, and klines. The managed client confirms which channels were accepted, reconnects and resubscribes, and emits an explicit gap notice because the perps stream cannot replay missed messages.

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

const pn = new PolyNode({ apiKey: 'pn_live_...' });
const perps = pn.configurePerps({ queueCapacity: 4096 });

const hello = await perps.connect();
const ack = await perps.subscribe([
  perpsChannels.tickers,
  perpsChannels.book('BTC-USD'),
  perpsChannels.trades('BTC-USD'),
]);

console.log(hello.max_subscriptions, ack.channels, ack.rejected);

perps.onMessage((message) => {
  if (message.type === 'event' && message.channel === 'perps_tickers') {
    // Prices, quantities, funding, and equity values stay exact strings.
    console.log(message.data.mark_price, message.data.funding_rate);
  }
  if (message.type === 'lag_warning') console.warn(message.dropped_events);
  if (message.type === 'reconnect') console.warn(message.message);
});

perps.onOverflow((overflow) => console.warn(overflow.droppedMessages));

Use perpsChannels.bbo(), .book(), .trades(), and .klines(instrument, '1m' | '1h') for scoped channels. Each perps_book event is a complete replacement snapshot; perps.book('BTC-USD') returns the latest complete book. Authentication (4401) and connection-cap (4429) closes are terminal. Call perps.disconnect() during shutdown.

Trading and fees

PolyNodeTrader defaults to current CLOB V2. V2 fees are determined at match time and are not signed into the order; the V2 wire payload therefore omits feeRateBps, nonce, and taker. The explicit legacy V1 mode still signs feeRateBps, so the SDK fetches /fee-rate for the token and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.

import { PolyNodeTrader } from 'polynode-sdk';

const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',
  // exchangeVersion: 'v2' is the default
  // builderCode: null disables default public Polynode attribution
});

Optional user-owned execution

Set executionMode: 'user_owned' when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.

const signer = process.env.POLYMARKET_PRIVATE_KEY!;
const trader = new PolyNodeTrader({
  polynodeKey: process.env.POLYNODE_API_KEY!,
  executionMode: 'user_owned',
});

const ready = await trader.ensureReady(signer); // one ownership signature on first use
console.log(ready.executionMode, ready.userRelayerAuthorized);

For V2 user-owned execution, ensureReady(signer) defaults new onboarding to the signing wallet's deterministic POLY_1271 deposit wallet. Existing deterministic Safe users remain supported: set defaultSignatureType: SignatureType.POLY_GNOSIS_SAFE (or import a valid type-2 vault bundle), and the order uses the Safe as maker/funder and its controlling EOA as the EIP-712 signer. Explicit EOA order selection also remains supported. The SDK verifies the deterministic funder binding for every supported wallet type.

User-owned mode rejects builder credentials, nonzero builder codes, credentials owned by another wallet, and fee escrow (feeConfig.feeBps > 0) before signing. Its onboarding grants only the base order permissions and never approves FeeEscrow. Split and merge transactions add the selected collateral-adapter permission immediately before the operation in the same atomic batch, so an old local approvalsSet value cannot bypass the current prerequisite. It supports EOA signers and EOA-controlled Safe or V2 deposit wallets; POLY_1271 is rejected with legacy V1, and legacy POLY_PROXY and Magic/DID signers are not supported in the first release. Gasless split, merge, and convert operations require a Safe or deposit wallet; an EOA is never silently routed through a derived Safe. Normal CLOB authentication and Polymarket rate limits still apply.

CLOB requests go directly to Polymarket by default. Integrations can explicitly opt in to Polynode regional egress with userOwnedClobTransport: 'polynode_proxy'. The selected path is fixed for each request and never fails over automatically.

For long-running services, authorizeUserOwnedExecution(signer) returns a wallet-owned credential that can be kept in a server-side secret manager and supplied later as userRelayerCredentials. Never log it or commit it. Backend custody is recommended; the explicitly memory-only browser option below is intended for platforms that accept the additional exposure of live credentials to their own frontend runtime.

Platforms should keep one credential record per signing-wallet address and keep the user's signer in the platform's existing wallet layer. The SDK never takes custody of the signer or private key. A browser only needs the short-lived challenge message and returns its wallet signature; the platform backend can keep the resulting wallet-bound credential encrypted and reuse it for that wallet.

Browser integrations can keep the Polynode API key entirely on their backend with trader instance methods. The trader supplies its configured service URL and key; there is no separate cosigner URL or product to configure:

const trader = new PolyNodeTrader({
  polynodeKey: process.env.POLYNODE_API_KEY!,
  executionMode: 'user_owned',
  storage: 'memory',
});

const challenge = await trader.beginUserRelayerAuthorization(walletAddress);
// Send only challenge.message to the browser wallet for personal_sign.
const credentials = await trader.completeUserRelayerAuthorization(
  challenge, walletSignature, walletAddress,
);

The SDK validates the canonical wallet-ownership message before returning it and revalidates the challenge, expiration, signature shape, and expected wallet before completion. Completion keeps the credential only in that trader's memory and also returns a defensive copy for your encrypted vault. The original global helper signatures remain available for compatibility.

Connect wallet through an actual user-owned order

For a normal web application, the recommended boundary is:

  1. The backend keeps the Polynode API key, wallet-owned relayer credential, and CLOB credentials in an encrypted per-wallet vault.
  2. The browser receives only authorization messages and exact EIP-712 signing requests.
  3. The connected wallet signs locally. It never sends a private key to the application or Polynode.
  4. The backend validates the wallet address and signature, forces zero builder attribution, authenticates the request with the vaulted CLOB credentials, and submits it.

After wallet onboarding, decrypt the stable browser bundle from your vault and import it into a memory-only backend trader. The helper validates the exact v1 schema, execution mode, credential owner, supported signature type, and deterministic wallet/funder binding without attaching a signer or writing the secrets to persistent SDK storage:

import { PolyNodeTrader, SignatureType } from 'polynode-sdk';

const trader = new PolyNodeTrader({
  polynodeKey: process.env.POLYNODE_API_KEY!,
  executionMode: 'user_owned',
  exchangeVersion: 'v2',
  storage: 'memory',
});

await trader.importUserOwnedBrowserBundle(vaultBundle);

If your platform already has separately vaulted credentials, await trader.linkCredentials(...) remains available. It is asynchronous for a deposit wallet; always await it.

When the user enters an order, prepare it on that backend trader. Only .signingRequest is safe to return to the browser. For a multi-worker service, transfer the exact state into an expiring store with exportPreparedUserOwnedOrderState():

// Backend: POST /api/orders/prepare
const prepared = await trader.prepareUserOwnedOrder({
  tokenId: input.tokenId,
  side: input.side,
  price: input.price,
  size: input.size,
  type: 'GTC',
});

const signingRequest = prepared.signingRequest;
const orderHash = prepared.orderHash;
const backendState = trader.exportPreparedUserOwnedOrderState(prepared);
await pendingOrders.putIfAbsent(signingRequest.requestId, backendState, {
  expiresAt: signingRequest.expiresAt,
});
// This must be a durable database write, not an in-memory log or queue.
await orderAttempts.insertIfAbsent({
  orderHash,
  requestId: signingRequest.requestId,
  order: signingRequest.order,
  status: 'awaiting_signature',
});
return signingRequest;

The backend must have a nonempty Polynode API key for this prepared flow. prepared.orderHash is the canonical STANDARD exchange Order EIP-712 hash: it is the CLOB/open-order ID and the on-chain fill order_hash for both EOA and POLY_1271 orders. For POLY_1271, it deliberately differs from prepared.signingDigest, which is the deposit wallet's TypedDataSign prompt digest. The opaque version-1 prepared-state wire schema is unchanged; orderHash is derived again after a strict import. Persist orderHash and the credential-free order summary before returning the signing request so an ambiguous submission always has an exact reconciliation key.

The SDK uses the fetched canonical tick (0.1, 0.01, 0.001, or 0.0001), checks BUY collateral before returning a signing request, and shows the actual tick-rounded price and two-decimal size. A GTD request expires at the earlier of five minutes or 60 seconds before the order expiration. Prepared browser signing rejects hidden metadata and exchange overrides.

The version-1 signing request is JSON-safe and stable across SDKs: expiresAt is Unix seconds, and order contains exactly tokenId, side, price, size, orderType, postOnly, expiration, maker, signer, makerAmount, and takerAmount. It contains no Polynode key, wallet-owned relayer credential, CLOB credential, HMAC header, tick metadata, or backend URL. Return it from a same-origin authenticated endpoint with Cache-Control: no-store.

The browser verifies the address and visible order summary before requesting the signature:

// Frontend
const signingRequest = await fetch('/api/orders/prepare', {
  method: 'POST',
  cache: 'no-store',
  credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(orderInput),
}).then((response) => response.json());

const chainId = await window.ethereum.request({ method: 'eth_chainId' });
if (BigInt(chainId) !== 137n) throw new Error('Switch the wallet to Polygon');
if (signingRequest.address.toLowerCase() !== connectedAddress.toLowerCase()) {
  throw new Error('Signing request belongs to another wallet');
}
if (signingRequest.expiresAt <= Math.floor(Date.now() / 1000)) {
  throw new Error('Signing request expired');
}

// Display signingRequest.order for final user confirmation.
// typedData is the complete eth_signTypedData_v4 JSON shape.
const signature = await window.ethereum.request({
  method: 'eth_signTypedData_v4',
  params: [connectedAddress, JSON.stringify(signingRequest.typedData)],
});

const result = await fetch('/api/orders/submit', {
  method: 'POST',
  cache: 'no-store',
  credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    requestId: signingRequest.requestId,
    address: connectedAddress,
    signature,
  }),
}).then((response) => response.json());

The backend atomically takes the opaque server state so only one worker can submit it, strictly reimports it, and gives the SDK only the three browser reply fields:

// Backend: POST /api/orders/submit
const backendState = await pendingOrders.take(input.requestId); // atomic read + delete
if (!backendState) throw new Error('Unknown or expired order');
const prepared = await trader.importPreparedUserOwnedOrderState(backendState);

return trader.submitPreparedUserOwnedOrder(prepared, {
  requestId: input.requestId,
  address: input.address,
  signature: input.signature,
});

PreparedUserOwnedOrderState is backend-only even though it is serializable; never send it to the browser. Use an atomic take/delete operation in Redis, SQL, or an equivalent store and expire the row at signingRequest.expiresAt. Export invalidates the original in-process object. The state carries a domain-separated HMAC integrity tag keyed by the backend-held Polynode key; the key is never serialized. Import and submission verify that tag and then revalidate every field, canonical tick-rounded amount, typed-data digest, wallet/funder binding, expiry, and current credential record. Coherent tampering with the order and its unkeyed digest is therefore rejected. The SDK then checks the browser reply and recovered signer, and consumes the restored intent before authenticated network submission. If a response is ambiguous, reconcile order state instead of preparing and submitting a duplicate. A single-process service may instead retain the exact unexported PreparedUserOwnedOrder object in memory.

Optional browser-memory onboarding

polynode-sdk/trading/browser is a dedicated ESM entry for platforms that want the connected wallet to complete onboarding in the browser. It forces user_owned, V2, direct transport, and in-memory storage. It accepts no Polynode key, builder credentials, builder code, fee settings, proxy transport, or persistent storage.

import { BrowserUserOwnedSession } from 'polynode-sdk/trading/browser';

// Your backend completed the two-step authorization above and returned this
// wallet-bound credential once over an authenticated, no-store response.
const credentialResponse = await fetch('/api/user-owned/session-credential', {
  cache: 'no-store',
  credentials: 'same-origin',
});
if (!credentialResponse.ok) throw new Error('Credential request failed');
const { userRelayerCredentials } = await credentialResponse.json();

const session = await BrowserUserOwnedSession.open({
  wallet: {
    provider: window.ethereum,
    address: connectedAddress,
  },
  userRelayerCredentials,
});

// open() establishes identity and order permissions; it does not create or
// move collateral. Put USDC.e at status.funderAddress, then let the connected
// wallet confirm the conversion to pUSD. Amounts use raw six-decimal units.
console.log(session.status.funderAddress, await session.getUsdcBalance());
if (await session.getPolyUsdBalance() < 5_200_000n) {
  await session.wrapToPolyUsd(5_200_000n);
}

// Option A: keep trading only for this tab/session.
const order = await session.order(
  { tokenId, side: 'BUY', price: 0.52, size: 10 },
  {
    // Strongly recommended: show this exact canonical 11-field summary.
    confirm: async (preview) => showFinalOrderConfirmation(preview),
    // Await a durable application-owned write before the SDK can POST /order.
    beforeSubmit: async ({ orderHash, order }) => {
      await persistOrderAttempt({ orderHash, order, status: 'submitting' });
    },
  },
);

// Option B (recommended for continued use): move the bound credentials once
// into the platform's encrypted backend vault, then close the browser session.
const vaultBundle = session.exportForBackendVault();
const vaultResponse = await fetch('/api/user-owned/vault', {
  method: 'POST',
  cache: 'no-store',
  credentials: 'same-origin',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(vaultBundle),
});
if (!vaultResponse.ok) throw new Error('Backend vault did not confirm receipt');
session.close();

open() requires the injected wallet to be on Polygon (0x89) and verifies the active account before readiness work. The session closes on accountsChanged, chainChanged, or pagehide, and it rechecks chain and account immediately before every order signature and submission, including after both callbacks. confirm receives the exact 11-key credential-free preview used by the backend signing bridge; returning false or throwing prevents signing and submission. beforeSubmit runs only after the wallet signature has been finalized, receives a frozen, credential-free { orderHash, order }, and is awaited before the first order-submission HTTP request. If it throws or rejects, no /order request occurs. Its orderHash has the same STANDARD-hash semantics for EOA and POLY_1271 orders described above.

The session never obtains collateral for the user: wrapToPolyUsd() converts only USDC.e already held by status.funderAddress, shows every required wallet prompt, and waits for confirmation. A BUY also compares its exact raw maker amount with getPolyUsdBalance() and fails before confirmation or wallet prompting if the funding address lacks enough pUSD. If submission becomes ambiguous, use the durably stored orderHash: match it exactly against id from await session.getOpenOrders({ assetId: tokenId }) and against order_hash in fill history. Absence from open orders alone is not evidence of failure because an order may already be filled or canceled; never submit the same intent blindly.

The one-time vault bundle has a stable camel-case schema:

{
  version: '1',
  executionMode: 'user_owned',
  wallet: { address, funderAddress, signatureType },
  clobCredentials: { apiKey, apiSecret, apiPassphrase },
  userRelayerCredentials: { key, address },
}

The portable schema admits signature types 0 (EOA), 2 (existing Safe), and 3 (deposit wallet). This browser session creates only type 0 or 3 bundles; backend SDKs can import an existing, correctly bound type 2 bundle. A backend can restore any valid bundle with await trader.importUserOwnedBrowserBundle(bundle).

Treat that entire bundle as a secret. Send it only to an authenticated same-origin HTTPS endpoint, exclude the request and response from logs and analytics, return Cache-Control: no-store, encrypt it at rest under the wallet address, and close the session only after the vault confirms receipt. Trader/session JSON representations and Node trader inspection are credential-free, but that is only accidental-logging protection. The session clears its in-memory maps, wallet/provider references, and credentials on close(). JavaScript strings cannot be cryptographically zeroized, and devtools, an XSS payload, or a browser extension can inspect live page memory, so memory-only mode is not equivalent to backend custody.

There are therefore three supported integration choices:

  • Backend vault plus browser signing bridge: recommended for normal platforms and long-running sessions.
  • Browser-memory session: simplest temporary mode, with higher frontend-secret exposure.
  • Caller-controlled service signer: suitable when the platform already operates a secure signer or HSM workflow.

Long-running order loops can reconcile each accepted order by ID without inferring a fill from its absence in the open-order list:

const order = await trader.getOrder(orderId);
console.log(order.status, order.sizeMatched, order.originalSize);

const balances = await trader.getPositionBalances(upTokenId, downTokenId);
console.log(balances.up, balances.down); // raw 6-decimal share amounts

Deposit-wallet addresses are factory-version dependent. Resolve the current address with live factory context before binding or displaying a fresh funder:

import { createPublicClient, http } from 'viem';
import { polygon } from 'viem/chains';
import { resolveDepositWalletAddress } from 'polynode-sdk';

const polygonClient = createPublicClient({ chain: polygon, transport: http(rpcUrl) });
const funder = await resolveDepositWalletAddress(polygonClient, walletAddress);

The older synchronous deriveDepositWalletAddress(walletAddress) export is deprecated and derives only the legacy UUPS address. Do not use it to bind a new deposit wallet when the factory beacon is active. ensureReady() and position execution always use the asynchronous current resolver. User-owned link/import methods verify the deterministic funder before storage; always await linkCredentials(...) and await importWallet(...) for deposit wallets because current beacon resolution is asynchronous.

Orderbook Streaming

await pn.orderbook.subscribe(['token_id_1', 'token_id_2']);

pn.orderbook.on('snapshot', (snap) => {
  console.log(snap.asset_id, snap.bids.length, 'bids', snap.asks.length, 'asks');
});

pn.orderbook.on('update', (delta) => {
  console.log(delta.asset_id, delta.bids.length, 'bid changes');
});

pn.orderbook.on('price', (change) => {
  for (const asset of change.assets) {
    console.log(asset.outcome, asset.price);
  }
});

subscribe() resolves after the orderbook server acknowledges the subscription. In v0.10.17+, reconnects reuse the same handlers and replay the active token list automatically.

To remove only some tokens, pass them to unsubscribe():

pn.orderbook.unsubscribe(['token_id_1']); // remove one token
pn.orderbook.unsubscribe();               // remove all tokens

LocalOrderbook

import { LocalOrderbook } from 'polynode-sdk';

const book = new LocalOrderbook();

pn.orderbook.on('snapshot', (snap) => book.applySnapshot(snap));
pn.orderbook.on('update', (delta) => book.applyUpdate(delta));

const fullBook = book.getBook(tokenId);
const bestBid = book.getBestBid(tokenId);
const bestAsk = book.getBestAsk(tokenId);
const spread = book.getSpread(tokenId);

OrderbookEngine

Higher-level orderbook client. One connection, shared state, filtered views for different parts of your app.

import { OrderbookEngine } from 'polynode-sdk';

const engine = new OrderbookEngine({ apiKey: 'pn_live_...' });

// Subscribe with token IDs, slugs, or condition IDs
await engine.subscribe([tokenA, tokenB, tokenC]);

engine.on('ready', () => {
  // Query computed values from local state
  engine.midpoint(tokenA);  // 0.465
  engine.spread(tokenA);    // 0.01
  engine.bestBid(tokenA);   // { price: '0.46', size: '226.29' }
  engine.book(tokenA);      // { bids: [...], asks: [...] }

  // Create filtered views for different components
  const view = engine.view([tokenA]);
  view.on('update', (u) => console.log(u.asset_id, 'updated'));
  view.midpoint(tokenA);    // reads from shared state

  // Swap tokens or destroy views at any time
  view.setTokens([tokenD, tokenE]);
  view.destroy();
});

engine.close();

PN1 orderbook integrity

Why: reconnects and dropped frames can leave a locally maintained book looking valid when it is not. Enable PN1 to validate sequence continuity and deterministic checksums. The engine fails stale or invalid books closed by default and requests a fresh anchor before making them readable again.

const verified = new OrderbookEngine({
  apiKey: 'pn_live_...',
  integrity: true,
});

verified.on('integrity_error', (error) => {
  console.error(error.token, error.code);
});

await verified.subscribe([tokenA, tokenB]); // explicit markets only in PN1 mode

Wildcard subscriptions are intentionally unavailable in integrity mode. Set allowStaleReads: true only when your application explicitly prefers availability over verified state.

Local Cache

Store trades and positions in a local SQLite database. Backfills recent history on startup, streams live updates, and serves all queries locally with zero API calls.

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',
  onBackfillProgress: (p) => console.log(`${p.label}: ${p.fetched} trades`),
});

await cache.start();

// Query locally — instant, no API calls
const trades = cache.walletTrades('0xabc...', { limit: 50, side: 'BUY' });
const positions = cache.walletPositions('0xabc...');
const multiPos = cache.multiWalletPositions(['0xabc...', '0xdef...']);
const marketTrades = cache.marketTrades('0xcondition...');

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

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

await cache.stop();

Watchlist (polynode.watch.json):

{
  "version": 1,
  "wallets": [
    { "address": "0xabc...", "label": "trader-1", "backfill": true }
  ],
  "settings": { "ttl_days": 30 }
}

Backfill timing: 1 request per wallet at 1 req/s. 10 wallets = 10 seconds. Up to 500 trades per wallet (configurable with backfillPages).

See full documentation for all query methods, configuration options, and examples.

Compression & Reconnection

Zlib compression is enabled by default (~50% bandwidth savings). All connections auto-reconnect with exponential backoff.

// Compression is automatic — no config needed
// To disable (not recommended):
const ws = pn.configureWs({ compress: false });
const orderbook = pn.configureOrderbook({ subscribeTimeoutMs: 30000 });

ws.onConnect(() => console.log('connected'));
ws.onDisconnect((reason) => console.log('disconnected:', reason));
ws.onReconnect((attempt) => console.log('reconnected, attempt', attempt));
ws.onError((err) => console.error(err));

Configuration

const pn = new PolyNode({
  apiKey: 'pn_live_...',
  baseUrl: 'https://api.polynode.dev',
  v3BaseUrl: 'https://api.polynode.dev',
  perpsWsUrl: 'wss://perps.polynode.dev/ws',
  wsUrl: 'wss://ws.polynode.dev/ws',
  obUrl: 'wss://ob.polynode.dev/ws',
  rpcUrl: 'https://rpc.polynode.dev',
  timeout: 10000,
});

Error Handling

import { PolyNode, ApiError, WsError } from 'polynode-sdk';

try {
  await pn.market('invalid-id');
} catch (err) {
  if (err instanceof ApiError) {
    console.log(err.status);
    console.log(err.message);
  }
}

Cleanup

sub.unsubscribe();           // remove one subscription
pn.ws.unsubscribeAll();      // remove all
pn.ws.disconnect();          // close event stream
pn.orderbook.unsubscribe();  // unsubscribe orderbook
pn.orderbook.disconnect();   // close orderbook stream
pn.perps.disconnect();       // close perps stream

Testing Utilities

The SDK includes network-free helpers that return stable public sample wallet addresses for examples, integration tests, and local development. They do not imply current wallet activity; validate or supply your own fixture when a test depends on wallet state.

import { getActiveTestWallet, getActiveTestWallets } from 'polynode-sdk';

// Get one stable public sample address
const wallet = await getActiveTestWallet();

// Get multiple stable public sample addresses
const wallets = await getActiveTestWallets(5);

// `fresh` remains accepted for compatibility, but is deprecated and ignored.
// These helpers never make a network request.
const sameStableSample = await getActiveTestWallet({ fresh: true });

Combine with the cache for a zero-config quickstart:

const wallet = await getActiveTestWallet();
cache.addToWatchlist([{ type: 'wallet', id: wallet, label: 'test' }]);

Links

License

MIT