Skip to main content

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.

Polymarket V2 cutover: April 28, 2026 at ~11am UTC. Set exchangeVersion: "v2" (TypeScript) / ExchangeVersion::V2 (Rust) / ExchangeVersion.V2 (Python) in your TraderConfig before cutover. V1 orders submitted after April 28 will be rejected with order_version_mismatch. Pass your V2 builder code (mint at polymarket.com/settings?tab=builder) via the builder field on each order. See the V2 Migration Guide for the full rundown.
Deposit wallets supported. Polymarket is rolling out deposit wallets as a new account type for new users. The SDK handles both Safe and deposit wallet users automatically — no code changes needed. See the Deposit Wallets guide for details.

Overview

The trading module lets you place and manage orders on Polymarket. One function call handles wallet setup, credential creation, approvals, and order placement. You don’t need to understand Polymarket’s wallet types, contract addresses, or signing protocols. The SDK auto-detects whether a user has a Safe proxy, deposit wallet, or no wallet at all, and handles each case transparently.
npm install polynode-sdk viem better-sqlite3 @polymarket/clob-client @polymarket/builder-relayer-client @polymarket/builder-signing-sdk
V2 order placement requires polynode-sdk >= 0.9.0. Deposit wallet support (address derivation, wallet detection) requires >= 0.10.0. Deposit wallet order signing (POLY_1271) requires >= 0.10.5.
Your project must use ESM imports (import syntax). Add "type": "module" to your package.json, or use .mjs file extensions.

Quick Start

Pick the path that matches your situation.

I’m building a backend service (private keys on your server)

Generate wallets, manage them yourself. This is the simplest path.
import { PolyNodeTrader } from 'polynode-sdk';

const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });

// 1. Generate a wallet (or use an existing private key)
const { privateKey, address } = await PolyNodeTrader.generateWallet();
// SAVE THIS KEY. It is not stored anywhere. If you lose it, you lose access.

// 2. One call sets up everything: deploys a Safe, sets approvals, creates credentials.
//    Completely gasless. No MATIC needed. Takes ~10 seconds for a new wallet.
const status = await trader.ensureReady(privateKey);

// 3. Send USDC.e (Polygon) to the funder address. This is your trading wallet.
console.log('Send USDC.e here:', status.funderAddress);

// 4. Place an order
const result = await trader.order({
  tokenId: '...',  // Get token IDs from /v1/events/search or /v1/crypto/active
  side: 'BUY',
  price: 0.55,
  size: 100,
});

// 5. Cancel it
if (result.orderId) {
  await trader.cancelOrder(result.orderId);
}
ensureReady() is idempotent. Call it every time your service starts. If the wallet is already set up, it returns instantly from the local database.

I’m building a platform with Privy (server wallets for each user)

Use createPrivySigner to trade with Privy-managed wallets. Each user gets their own wallet without touching private keys.
import { PolyNodeTrader, createPrivySigner, createPrivyClient } from 'polynode-sdk';

// Set up Privy (one client for your whole app)
// createPrivyClient handles the wallet API config for you.
const privy = createPrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
  authorizationKey: process.env.PRIVY_AUTHORIZATION_KEY!,
});

// Create a signer for a specific user's wallet
const signer = createPrivySigner(privy, user.privyWalletId, user.walletAddress);

const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });

// Set up the wallet — gasless, ~10 seconds for new users
const status = await trader.ensureReady(signer);

// Send USDC.e to status.funderAddress, then trade
const result = await trader.order({ tokenId: '...', side: 'BUY', price: 0.55, size: 100 });
Privy setup requirements:
  1. Create a Privy app at dashboard.privy.io
  2. Enable Server wallets under Wallet infrastructure
  3. Generate an Authorization keypair (same section) — this is PRIVY_AUTHORIZATION_KEY
  4. Install: npm install @privy-io/server-auth (the SDK imports it automatically via createPrivyClient)
If you enable Privy gas sponsorship in your dashboard, all on-chain operations (approvals, deploys) are free for your users.

I already have a Polymarket account (exported key or existing credentials)

If you export your private key from Polymarket, the SDK auto-detects your wallet type.
const trader = new PolyNodeTrader({ polynodeKey: 'pn_live_...' });
const status = await trader.ensureReady('0xYOUR_EXPORTED_KEY');

// Your USDC.e is already in the right place. Start trading.
const balance = await trader.checkBalance();
console.log(`USDC.e: $${balance.usdc}`);
If you already have CLOB credentials (from Polymarket or Dome), import them directly:
trader.linkCredentials({
  wallet: '0xYOUR_EOA',
  apiKey: 'your-clob-api-key',
  apiSecret: 'your-clob-api-secret',
  apiPassphrase: 'your-clob-passphrase',
  signatureType: 2,
  funderAddress: '0xYOUR_SAFE',
});
await trader.linkWallet('0xYOUR_PRIVATE_KEY');

Where to Send USDC.e

Polymarket uses USDC.e (bridged USDC) on Polygon, NOT native USDC. These are different tokens.
  • USDC.e (correct): 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
  • Native USDC (wrong): 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
If you accidentally send native USDC, the tokens are still in your wallet (recoverable), but they won’t work as a trading balance on Polymarket.
After ensureReady(), send USDC.e to status.funderAddress. This is the address that holds your trading balance.
Wallet typefunderAddress is…
Safe (default)The Safe contract address (different from your private key’s address)
EOAThe wallet address itself
Do not send USDC.e to the EOA address if you’re using a Safe wallet. It will sit there unused.

Finding Markets

You need a tokenId to place orders. Here’s how to find one:
import { PolyNode } from 'polynode-sdk';
const pn = new PolyNode({ apiKey: 'pn_live_...' });

// Search for markets
const results = await pn.searchEvents('Bitcoin', { limit: 5 });
for (const event of results.events) {
  for (const market of event.markets) {
    console.log(market.question, '→', market.tokenId);
  }
}
Or use the REST API directly: GET /v1/events/search?q=bitcoin&limit=5 or GET /v1/crypto/active for live crypto markets.

Configuration

const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',         // Your polynode API key
  dbPath: './my-trading.db',          // Local SQLite path (default: ./polynode-trading.db)
  cosignerUrl: 'https://trade.polynode.dev', // Relay URL (default)
  fallbackDirect: true,               // Fall back to direct CLOB if relay is down (default: true)
  rpcUrl: 'https://polygon-bor-rpc.publicnode.com', // RPC for on-chain reads (default)
  builderCredentials: {               // Optional: your own Polymarket builder credentials
    key: process.env.POLY_BUILDER_API_KEY!,
    secret: process.env.POLY_BUILDER_SECRET!,
    passphrase: process.env.POLY_BUILDER_PASSPHRASE!,
  },
  feeConfig: {                        // Optional: charge fees on orders
    feeBps: 50,                       // 0.5% fee (0 = no fee, default)
    affiliate: '0xPartner...',        // Partner wallet for revenue share
    affiliateShareBps: 3000,          // 30% to partner, 70% to treasury
  },
});

Builder Attribution (Your Own Credentials)

By default, orders placed through polynode are attributed to polynode’s builder profile on Polymarket. If you’re running a platform and want volume credited to your own builder account, pass your credentials in builderCredentials.
  1. Go to polymarket.com/settings?tab=builder and create a builder profile
  2. Generate API credentials (key, secret, passphrase)
  3. Pass them in builderCredentials when constructing the trader
const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',
  builderCredentials: {
    key: process.env.POLY_BUILDER_API_KEY!,
    secret: process.env.POLY_BUILDER_SECRET!,
    passphrase: process.env.POLY_BUILDER_PASSPHRASE!,
  },
});
When builderCredentials is set:
  • All orders are attributed to your builder profile on the Builder Leaderboard
  • Gasless Safe deployments and approvals use your builder account
  • polynode never stores your builder credentials. They’re sent per-request and used only for HMAC signing.
When builderCredentials is omitted, polynode’s default builder credentials are used. Your orders still go through, you just don’t get the builder attribution on your own profile.

Polymarket V2 Exchange

The Polymarket V2 exchange uses PolyUSD as collateral instead of USDC.e directly. One config change enables V2 support. Always call ensureReady / ensure_ready before any V2 method — it deploys the Safe (if needed), sets V2 approvals, creates CLOB credentials, and refreshes the V2 CLOB’s balance-allowance cache:
import { PolyNodeTrader } from 'polynode-sdk';

const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',
  exchangeVersion: 'v2',
});

// Required before any wrap/order/cancel call:
await trader.ensureReady('0xYOUR_EOA_PRIVATE_KEY');
Before placing orders on V2 markets, wrap USDC.e into PolyUSD. Amounts are in raw 6-decimal integers (1_000_000 = $1; TS uses bigint):
// Wrap 100 USDC.e → PolyUSD
await trader.wrapToPolyUsd(100_000_000n);

const balanceRaw = await trader.getPolyUsdBalance();
console.log(`PolyUSD: $${Number(balanceRaw) / 1e6}`);
Pass your builder code via the builder field on each V2 order. Mint one at polymarket.com/settings?tab=builder:
await trader.order({
  tokenId: "...",
  side: "BUY",
  price: 0.42,
  size: 5,
  type: "GTC",
  builder: "0x5472fdd7...",  // bytes32 builder code
});
On V2, ensureReady() / ensure_ready() also refreshes the CLOB’s cached balance-allowance view after setting approvals. If you change approvals outside that flow, call trader.refreshBalanceAllowance() / trader.refresh_balance_allowance() before placing orders — otherwise the CLOB can reject with "not enough balance / allowance" until its cache catches up. All other trading methods (order(), cancelOrder(), split(), merge(), etc.) work the same on V2 markets. The order_type accepts "GTC" (default — rest on book), "GTD" (with expiration unix seconds), "FOK" (fill-or-kill, fully match or cancel), or "FAK" (fill-and-kill, match what you can, cancel the rest). size is in shares of the outcome token, price is the per-share USD price between 0 and 1.
See the V2 Migration Guide for full details on the Polymarket V2 exchange upgrade, and the PolyUSD Guide for wrapping and unwrapping mechanics.

Fee Escrow

Charge your platform’s per-order fee via on-chain escrow. Fees are pulled before the order, distributed on fill, and refunded on cancel. This is your fee — independent of Polymarket’s protocol fee and V2 builder rev share (which are charged separately by the CLOB). See the Fee Escrow Guide for the full architecture and the three-fee breakdown.
const trader = new PolyNodeTrader({
  polynodeKey: 'pn_live_...',
  feeConfig: { feeBps: 50 },  // 0.5% platform fee on every order (yours; not Polymarket's)
});

const result = await trader.order({
  tokenId: '...',
  side: 'BUY',
  price: 0.55,
  size: 100,
});

console.log(result.feeEscrowTxHash); // on-chain pullFee TX
console.log(result.feeAmount);       // "0.0275" USDC

// Cancel → fee is automatically refunded
await trader.cancelOrder(result.orderId);
Set feeBps: 0 or omit feeConfig to skip your platform fee entirely. Zero overhead, zero behavior change. Note: this only turns off your fee — Polymarket’s protocol fee and V2 builder rev share are controlled independently by the CLOB.

API Reference

PolyNodeTrader.generateWallet()

Static async method. Generates a fresh wallet. You must await this call.
const { privateKey, address } = await PolyNodeTrader.generateWallet();
// IMPORTANT: Save privateKey securely. It is not stored anywhere by the SDK.

ensureReady(signer, opts?)

One-call onboarding. Detects wallet type (Safe, proxy, or deposit wallet), deploys contracts if needed, sets all approvals, creates CLOB credentials. Signer types accepted:
  • string — hex private key (most common)
  • createPrivySigner(...) — Privy server wallet
  • ethers v5/v6 Signer
  • viem WalletClient
Returns: ReadyStatus with funderAddress (where to send collateral), signatureType, approvalsSet, credentials, and actions (what it did). Wallet types detected automatically:
signatureTypeValueDescription
POLY_GNOSIS_SAFE2Gnosis Safe proxy (most existing Polymarket accounts)
POLY_PROXY1Legacy proxy wallet
POLY_12713Deposit wallet (newer Polymarket accounts, V2 only)
EOA0Direct EOA signing
Calling it again on a ready wallet is instant (loads from local DB). For deposit wallet users (POLY_1271), order signing uses the ERC-7739 TypedDataSign wrapper automatically. No code changes needed on your side — order() detects the wallet type from stored credentials and signs accordingly.

order(params)

Place an order on Polymarket.
const result = await trader.order({
  tokenId: '...',  // Market token ID (from /v1/events/search)
  side: 'BUY',     // 'BUY' or 'SELL'
  price: 0.55,     // Limit price (0 to 1)
  size: 100,       // Number of shares
  type: 'GTC',     // 'GTC' | 'GTD' | 'FOK' | 'FAK' (default: GTC)
});

if (result.success) {
  console.log('Order placed:', result.orderId);
  // When feeConfig is set:
  console.log('Fee TX:', result.feeEscrowTxHash);  // on-chain escrow TX
  console.log('Fee:', result.feeAmount);            // USDC charged
} else {
  console.log('Failed:', result.error);
}

cancelOrder(orderId) / cancelAll(market?)

Cancel a specific order or all orders.

split(params)

Split USDC into YES + NO outcome tokens. Gasless for Safe wallets. Auto-detects neg-risk vs standard markets.
const result = await trader.split({
  conditionId: '0x895e01db...', // market condition ID
  amount: 100,                   // $100 USDC
});
// result: { success: true, txHash: '0x...' }

merge(params)

Merge YES + NO outcome tokens back into USDC. Gasless for Safe wallets.
const result = await trader.merge({
  conditionId: '0x895e01db...',
  amount: 50,
});

convert(params)

Convert NO positions on selected outcomes into USDC + YES on complementary outcomes. Only works on neg-risk multi-outcome markets. Gasless for Safe wallets.
const result = await trader.convert({
  marketId: '0xc7d902c4...', // negRiskMarketID
  outcomeIndices: [0, 1],     // which outcomes to convert
  amount: 100,                // $100 per outcome
});
See the Position Management guide for a full explanation of how conversions work and when to use each operation.

checkBalance(wallet?)

Returns USDC.e and MATIC balance for the funder address.

checkApprovals(wallet?)

Check if all required token approvals are set on-chain (6 Polymarket approvals + 1 fee escrow approval).

getOpenOrders(params?)

Fetch open orders from the CLOB. Filters: market, assetId.

getOrderHistory(params?)

Query local order history from SQLite. Filters: limit, offset, tokenId, side.

linkCredentials(opts) / linkWallet(signer)

Import existing credentials or link a wallet manually.

exportWallet(wallet?) / exportAll() / importWallet(exported)

Export and import wallet state for backup. Private keys are never included.

unlinkWallet(address?) / getLinkedWallets()

Remove or list stored wallets.

close()

Close the SQLite connection. Call this when shutting down.

How It Works

  1. SDK signs the order locally (EIP-712)
  2. SDK sends to polynode’s relay at trade.polynode.dev
  3. Relay adds builder attribution and forwards to Polymarket’s CLOB
  4. Response returned to SDK, logged in local SQLite
If the relay is down and fallbackDirect is true, orders go directly to Polymarket. Your trading never stops.

Wallet Model & Signing

Polymarket uses a two-address model for each trader:
  • EOA (signer) — a plain private key. Signs every order and every on-chain intent. Holds no trading funds.
  • Safe (funder) — a Gnosis Safe smart wallet whose address is deterministically derived from the EOA (CREATE2 via the Polymarket Safe Factory). This is where USDC.e / pUSD / CTF positions live, and it’s the maker on every order.
ensureReady(privateKey) on a fresh EOA does all four things for you, gasless via the Polymarket relayer:
  1. Derives the Safe address from the EOA via CREATE2
  2. Deploys the Safe if it doesn’t exist yet on-chain
  3. Sets all required approvals (USDC.e/pUSD to exchanges + collateral adapters, CTF to exchanges)
  4. Creates CLOB API credentials (L2 HMAC) by signing an EIP-712 ClobAuth message from the EOA
After this, the SDK stores the CLOB API key, secret, and passphrase in its local SQLite DB keyed by the EOA address. You never send them to polynode.

How orders are signed

Every V2 order struct includes maker (the Safe address) and signer (the EOA address). The EOA signs the EIP-712 hash; the CLOB verifies the signature against signer and trusts that the signer is a 1-of-1 owner of the maker Safe (it is, because the Safe’s sole owner is the EOA by construction). At settlement time, the V2 exchange calls the Safe’s execTransaction with your signed payload as authorization.

How wrap/unwrap, approvals, and split/merge are signed

For any on-chain action that needs to move funds out of the Safe (wrapToPolyUsd, unwrapFromPolyUsd, setApprovals, split, merge, convert), the SDK:
  1. Builds the raw calldata for the target contract call
  2. Wraps it in a Safe execTransaction EIP-712 payload with the current Safe nonce
  3. Has your EOA personal_sign that payload (via eth_account / alloy / ethers)
  4. POSTs the signed payload to https://relayer-v2.polymarket.com/submit with your builder HMAC headers
  5. Polls the relayer until the tx is mined on Polygon
The relayer pays the gas. You never need MATIC. This is why wrapToPolyUsd() / unwrapFromPolyUsd() require builderCredentials in TraderConfig for Safe wallets — the HMAC authenticates your submit to the relayer.

Linking credentials vs ensureReady

Three ways to get a wallet ready to trade, depending on what you already have:
SituationCallWhat it does
Fresh EOA (no Safe yet)trader.ensureReady(privateKey)Derives Safe, deploys, approves, creates CLOB creds, stores in local DB
Existing EOA + Safe already set up (you re-start your service)trader.ensureReady(privateKey)No-op if the local DB already has creds; otherwise re-derives and re-fetches
You already have CLOB creds from somewhere else (DB export, Polymarket account)trader.linkCredentials({ wallet, apiKey, apiSecret, apiPassphrase, signatureType, funderAddress }) then trader.linkWallet(privateKey) to attach the signerImports the creds into the local DB and attaches the signer for order signing. Skips approvals + deploys entirely.
Multi-user platform (one SDK instance, many users)trader.linkCredentials(userA)trader.linkWallet(userA.pk) → trade; then switch with trader.linkWallet(userB.pk)Each linkWallet call swaps the active signer. linkCredentials writes into the local DB so you can return to a user later by calling linkWallet again with their key. Consider using one PolyNodeTrader per user for concurrency.

Multi-user / server wallet patterns

For a backend that trades on behalf of many users:
  • Privy / server-managed wallets: use createPrivySigner(privy, walletId, address) instead of passing a raw private key to ensureReady. Same onboarding flow, but Privy holds the key and signs remotely.
  • Your own KMS: implement the RouterSigner interface (or TradingSigner in Rust / a signer callable in Python). The SDK only needs two operations from you: getAddress() and signTypedData(payload). Everything else is local.
  • Per-user DB file: pass a different dbPath per user to keep their credentials isolated on disk. Or use a shared DB and call linkWallet(userPk) to swap active signers.

Credential Custody

Credentials are stored in a local SQLite database. The SDK never sends your private key or CLOB credentials to polynode’s servers. Back up with exportWallet().

Migrating from Dome

The polynode trading module is a drop-in replacement for Dome. See the Dome Migration guide for a line-by-line comparison.