Skip to main content

Documentation Index

Fetch the complete documentation index at: https://polynode.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Install

cargo add polynode
The core crate has zero required feature flags. Optional features unlock additional capabilities:
FeatureWhat it adds
cacheLocal SQLite cache with watchlist, backfill, and P&L computation
tradingOrder placement, wallet management, credential custody
privyPrivy-based signer (requires trading)
Enable features in your Cargo.toml:
[dependencies]
polynode = { version = "0.12.0", features = ["trading", "cache"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Quick Start

use polynode::PolyNodeClient;

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;

    // Fetch top 5 markets by volume
    let markets = client.markets(Some(5)).await?;
    println!("{} markets returned", markets.count);

    // Search for a market
    let results = client.search("bitcoin", Some(3), None).await?;
    for r in &results.results {
        println!("{}", r.question.as_deref().unwrap_or("untitled"));
    }

    Ok(())
}

Client Configuration

Use the builder for full control over endpoints and timeouts:
use polynode::PolyNodeClient;
use std::time::Duration;

# fn example() -> polynode::Result<()> {
let client = PolyNodeClient::builder("pn_live_YOUR_KEY")
    .base_url("https://api.polynode.dev")
    .ws_url("wss://ws.polynode.dev/ws")
    .ob_url("wss://ob.polynode.dev/ws")
    .rpc_url("https://rpc.polynode.dev")
    .timeout(Duration::from_secs(15))
    .build()?;
# Ok(())
# }
All URLs default to the production endpoints shown above.

REST API

Every REST endpoint has a typed async method on PolyNodeClient.

System

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Liveness probe (no auth required)
let health = client.healthz().await?;

// Readiness check (no auth required)
let ready = client.readyz().await?;

// System status with metrics
let status = client.status().await?;
println!("uptime: {}s, ws_subscribers: {}", status.uptime_seconds, status.ws_subscribers);

// Generate a new API key (no auth required)
let key = client.create_key(Some("my-bot")).await?;
println!("key: {}", key.api_key);
# Ok(())
# }

Markets

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
use polynode::rest::ListMarketsParams;

// Top markets by 24h volume
let markets = client.markets(Some(10)).await?;
println!("{} markets, {} total", markets.count, markets.total);

// Single market by token ID
let market = client.market("51037625779056581606819614184446816710505006861008496087735536016411882582167").await?;

// Single market by URL slug
let market = client.market_by_slug("bitcoin-100k").await?;

// Single market by condition ID
let market = client.market_by_condition("0xabc...").await?;

// Filtered, paginated listing
let list = client.list_markets(&ListMarketsParams {
    count: Some(20),
    sort: Some("volume".into()),
    category: Some("crypto".into()),
    min_volume: Some(10000.0),
    active_only: Some(true),
    cursor: None,
}).await?;

// Full-text search
let results = client.search("ethereum", Some(5), Some(false)).await?;

// Search events
let events = client.search_events("election", Some(5)).await?;

// Event detail (includes all markets within the event)
let event = client.event("presidential-election-2028").await?;

// Markets by category
let crypto = client.markets_by_category("crypto").await?;
# Ok(())
# }
Token IDs change as markets open and close. Use client.markets(Some(1)).await? to get a currently active token ID for testing.

Pricing

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
use polynode::common::CandleResolution;

let token_id = "51037625779056581606819614184446816710505006861008496087735536016411882582167";

// OHLCV candles
let candles = client.candles(token_id, Some(CandleResolution::OneHour), Some(100)).await?;
for c in &candles.candles {
    println!("{}: o={} h={} l={} c={} v={}", c.timestamp, c.open, c.high, c.low, c.close, c.volume);
}

// Market statistics
let stats = client.stats(token_id).await?;
# Ok(())
# }

Settlements

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Most recent settlements across all markets
let recent = client.recent_settlements(Some(20)).await?;
for s in &recent.settlements {
    println!("{}: {} ${} on {}", s.status, s.taker_side, s.taker_size, s.market_title);
}

// Settlements for a specific token
let token_settlements = client.token_settlements("21742633...", Some(10)).await?;

// Settlements for a specific wallet
let wallet_settlements = client.wallet_settlements("0xabc...", Some(10)).await?;
# Ok(())
# }

Wallets

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
let address = "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6";

// Wallet activity summary
let wallet = client.wallet(address).await?;

// Wallet positions with P&L
let positions = client.wallet_positions_data(address, Some(50), None).await?;
for p in &positions.positions {
    println!("{:?}", p);
}

// Onchain positions (all open + closed, accurate realized P&L)
let onchain = client.wallet_onchain_positions(address).await?;

// Wallet trade history
let trades = client.wallet_trades(address, Some(100), None).await?;

// Market trade history (by condition ID or slug)
let market_trades = client.market_trades("0xabc...", Some(50), None, Some("BUY"), None).await?;
# Ok(())
# }

Orderbook (REST)

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
let token_id = "51037625779056581606819614184446816710505006861008496087735536016411882582167";

// Full orderbook snapshot
let book = client.orderbook_rest(token_id).await?;
println!("bids: {}, asks: {}", book.bids.len(), book.asks.len());

// Midpoint price
let mid = client.midpoint(token_id).await?;
println!("midpoint: {}", mid.mid);

// Bid-ask spread
let spread = client.spread(token_id).await?;
println!("spread: {}", spread.spread);
# Ok(())
# }

Enriched Data

# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Top traders leaderboard
let leaders = client.leaderboard(Some("daily"), Some("profit")).await?;

// Trending markets (carousel, breaking, hot topics, featured, movers)
let trending = client.trending().await?;

// Recent global activity
let activity = client.activity().await?;

// Biggest 24h price movers
let movers = client.movers().await?;

// Trader profile
let profile = client.trader_profile("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6").await?;
println!("trades: {}, pnl: {}", profile.trades, profile.total_pnl);

// Trader P&L time series
let pnl = client.trader_pnl("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6", Some("30d")).await?;
# Ok(())
# }

RPC

Send JSON-RPC requests through rpc.polynode.dev. Transaction submission is optimized for speed. Standard read methods are supported for latest state.
# use polynode::serde_json;
# async fn example(client: &polynode::PolyNodeClient) -> polynode::Result<()> {
// Get current block number
let block = client.rpc_call("eth_blockNumber", serde_json::json!([])).await?;
println!("block: {}", block);

// Get a block
let block_data = client.rpc_call(
    "eth_getBlockByNumber",
    serde_json::json!(["latest", false]),
).await?;

// Send a raw transaction (optimized delivery)
let tx_hash = client.rpc_call(
    "eth_sendRawTransaction",
    serde_json::json!(["0xf86c..."]),
).await?;

// Gas price (recommended for fast inclusion)
let gas = client.rpc_call("eth_gasPrice", serde_json::json!([])).await?;

// Standard reads (latest state only)
let balance = client.rpc_call(
    "eth_getBalance",
    serde_json::json!(["0xabc...", "latest"]),
).await?;
# Ok(())
# }

WebSocket Streaming

Subscribe to real-time events with 3-5 second pre-confirmation lead time on settlements.

Connect and Subscribe

use polynode::{PolyNodeClient, ws::{StreamOptions, Subscription, SubscriptionType}};
use polynode::ws_messages::WsMessage;

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;

    let mut stream = client.stream(StreamOptions {
        compress: true,
        auto_reconnect: true,
        ..Default::default()
    }).await?;

    // Subscribe to settlements with a minimum size filter
    let sub = Subscription::new(SubscriptionType::Settlements)
        .min_size(100.0)
        .status("pending")
        .snapshot_count(20);
    stream.subscribe(sub).await?;

    // Read events
    while let Some(msg) = stream.next().await {
        match msg {
            Ok(WsMessage::Event(event)) => {
                println!("{:?}", event);
            }
            Ok(WsMessage::Snapshot(events)) => {
                println!("snapshot: {} events", events.len());
            }
            Ok(WsMessage::Subscribed { subscription_id, .. }) => {
                println!("subscribed: {}", subscription_id);
            }
            Ok(WsMessage::PriceFeed(feed)) => {
                // Chainlink price feed update
                println!("{:?}", feed);
            }
            Ok(WsMessage::Heartbeat { ts }) => {
                // Connection alive
            }
            Err(e) => eprintln!("error: {}", e),
            _ => {}
        }
    }

    Ok(())
}

Subscription Types

use polynode::ws::{Subscription, SubscriptionType};

# fn example() {
Subscription::new(SubscriptionType::Settlements);   // pending + confirmed settlements
Subscription::new(SubscriptionType::Trades);         // all trade activity
Subscription::new(SubscriptionType::Prices);         // price-moving events
Subscription::new(SubscriptionType::Blocks);         // new Polygon blocks
Subscription::new(SubscriptionType::Wallets);        // all wallet activity
Subscription::new(SubscriptionType::Markets);        // all market activity
Subscription::new(SubscriptionType::LargeTrades);    // $1K+ trades
Subscription::new(SubscriptionType::Oracle);         // UMA resolution events
Subscription::new(SubscriptionType::Chainlink);      // real-time price feeds
Subscription::new(SubscriptionType::Global);         // everything
# }

Subscription Filters

All filters from the Subscriptions & Filters page are supported via builder methods:
use polynode::ws::{Subscription, SubscriptionType};

# fn example() {
let sub = Subscription::new(SubscriptionType::Settlements)
    .wallets(vec!["0xabc...".into()])          // filter by wallet
    .tokens(vec!["21742633...".into()])         // filter by token ID
    .slugs(vec!["bitcoin-100k".into()])         // filter by market slug
    .condition_ids(vec!["0xabc...".into()])     // filter by condition ID
    .side("BUY")                                // BUY or SELL
    .status("pending")                          // pending, confirmed, or all
    .min_size(100.0)                            // minimum USD size
    .max_size(10000.0)                          // maximum USD size
    .event_types(vec!["settlement".into()])     // override event types
    .snapshot_count(50)                         // initial snapshot (max 200)
    .feeds(vec!["BTC/USD".into()]);             // chainlink feeds
# }

Multiple Subscriptions

Subscriptions stack on the same connection. Events are deduplicated server-side:
# async fn example(stream: &polynode::ws::WsStream) -> polynode::Result<()> {
use polynode::ws::{Subscription, SubscriptionType};

// Whale trades
stream.subscribe(
    Subscription::new(SubscriptionType::LargeTrades).min_size(5000.0)
).await?;

// Specific wallet activity
stream.subscribe(
    Subscription::new(SubscriptionType::Wallets)
        .wallets(vec!["0xabc...".into()])
).await?;
# Ok(())
# }

Auto-Reconnect

Enabled by default. The SDK reconnects with exponential backoff and replays all active subscriptions automatically:
use polynode::ws::StreamOptions;
use std::time::Duration;

# fn example() {
let options = StreamOptions {
    compress: true,
    auto_reconnect: true,
    max_reconnect_attempts: None,          // unlimited by default
    initial_backoff: Duration::from_secs(1),
    max_backoff: Duration::from_secs(30),
};
# }

Cleanup

# async fn example(stream: polynode::ws::WsStream) -> polynode::Result<()> {
// Unsubscribe from a specific subscription
stream.unsubscribe(Some("sub_id_here".into())).await?;

// Unsubscribe from all
stream.unsubscribe(None).await?;

// Close the connection
stream.close().await?;
# Ok(())
# }

Orderbook Streaming

Real-time orderbook data from ob.polynode.dev. Three levels of abstraction: raw stream, local state manager, or the fully managed engine.

Raw Stream

For full control over message processing:
use polynode::{PolyNodeClient, ObStreamOptions};
use polynode::types::orderbook::ObMessage;

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;

    let mut stream = client.orderbook_stream(ObStreamOptions::default()).await?;

    // Subscribe to specific tokens
    stream.subscribe(vec![
        "51037625779056581606819614184446816710505006861008496087735536016411882582167".into(),
    ]).await?;

    while let Some(msg) = stream.next().await {
        match msg {
            Ok(ObMessage::Update(update)) => {
                println!("{:?}", update);
            }
            Ok(ObMessage::Subscribed { markets }) => {
                println!("tracking {} markets", markets);
            }
            Ok(ObMessage::SnapshotsDone { total }) => {
                println!("all {} snapshots loaded", total);
            }
            Err(e) => eprintln!("error: {}", e),
            _ => {}
        }
    }

    Ok(())
}

Local Orderbook State

Apply snapshots and deltas to maintain a sorted local copy of the book:
use polynode::LocalOrderbook;
use polynode::types::orderbook::{ObMessage, OrderbookUpdate};

# async fn example(stream: &mut polynode::ObStream) -> polynode::Result<()> {
let mut book = LocalOrderbook::new();

while let Some(msg) = stream.next().await {
    if let Ok(ObMessage::Update(update)) = msg {
        match &update {
            OrderbookUpdate::Snapshot(snap) => book.apply_snapshot(snap),
            OrderbookUpdate::Update(delta) => book.apply_update(delta),
            OrderbookUpdate::PriceChange(_) => {}
        }
    }

    let token = "21742633...";
    if let Some(bid) = book.best_bid(token) {
        println!("best bid: {} x {}", bid.price, bid.size);
    }
    if let Some(ask) = book.best_ask(token) {
        println!("best ask: {} x {}", ask.price, ask.size);
    }
    if let Some(spread) = book.spread(token) {
        println!("spread: {:.4}", spread);
    }
}
# Ok(())
# }

Orderbook Engine

The highest-level abstraction. One shared WebSocket, automatic state management, and filtered views for different consumers:
use polynode::{OrderbookEngine, EngineOptions};

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let engine = OrderbookEngine::connect(
        "pn_live_YOUR_KEY",
        EngineOptions::default(),
    ).await?;

    // Subscribe to tokens
    engine.subscribe(vec![
        "token_a".into(),
        "token_b".into(),
    ]).await?;

    // Query the shared state directly
    if let Some(mid) = engine.midpoint("token_a").await {
        println!("midpoint: {:.4}", mid);
    }
    if let Some(spread) = engine.spread("token_a").await {
        println!("spread: {:.4}", spread);
    }
    if let Some((bids, asks)) = engine.book("token_a").await {
        println!("bids: {}, asks: {}", bids.len(), asks.len());
    }

    // Create a filtered view for a subset of tokens
    let mut view = engine.view(vec!["token_a".into()]);
    while let Some(update) = view.next().await {
        if let Some(mid) = view.midpoint("token_a").await {
            println!("token_a midpoint: {:.4}", mid);
        }
    }

    engine.close().await?;
    Ok(())
}

Short-Form Markets

Auto-rotating streams for Polymarket’s short-form crypto markets (5m, 15m, 1h windows). The SDK discovers the current market window, subscribes to live events, and automatically rotates to the next window at expiry.
use polynode::{PolyNodeClient, ShortFormInterval, Coin, ShortFormMessage};

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;

    let mut stream = client
        .short_form(ShortFormInterval::FifteenMin)
        .coins(&[Coin::Btc, Coin::Eth, Coin::Sol])
        .rotation_buffer(3)
        .start()
        .await?;

    while let Some(msg) = stream.next().await {
        match msg {
            ShortFormMessage::Event(event) => {
                println!("event: {:?}", event);
            }
            ShortFormMessage::Rotation(info) => {
                println!("--- window rotated ({}) ---", info.interval);
                for m in &info.markets {
                    println!("  {}: beat ${:?} | {:.0}% up | {}s left",
                        m.coin.id(), m.price_to_beat,
                        m.up_odds * 100.0, info.time_remaining);
                }
            }
            ShortFormMessage::Error(e) => {
                eprintln!("non-fatal: {}", e);
            }
        }
    }

    Ok(())
}

Intervals and Coins

use polynode::{ShortFormInterval, Coin};

# fn example() {
// Intervals
let _ = ShortFormInterval::FiveMin;     // 5-minute windows
let _ = ShortFormInterval::FifteenMin;  // 15-minute windows
let _ = ShortFormInterval::Hourly;      // 1-hour windows

// Supported coins
let all = Coin::all(); // BTC, ETH, SOL, XRP, DOGE, HYPE, BNB
# }

Trading Module

Requires the trading feature flag: polynode = { version = "0.12.0", features = ["trading"] }
Place orders on Polymarket with local credential custody and builder attribution. All signing happens locally. Private keys never leave your machine. Supports both the current exchange and the Polymarket V2 exchange. See also: PolyUSD Guide for V2 collateral wrapping.

Generate a Wallet

use polynode::trading::PolyNodeTrader;

# fn example() {
let (private_key, address) = PolyNodeTrader::generate_wallet();
println!("address: {}", address);
println!("key: {}", private_key);
// Fund this address with USDC and POL on Polygon before trading
# }

Onboarding

ensure_ready() handles the full onboarding flow in one call: derives addresses, checks Safe deployment and approvals, creates or loads CLOB credentials, and stores everything in a local SQLite database.
use polynode::trading::{PolyNodeTrader, TraderConfig, PrivateKeySigner};

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let mut trader = PolyNodeTrader::new(TraderConfig {
        polynode_key: "pn_live_YOUR_KEY".into(),
        db_path: "./my-trading.db".into(),
        ..Default::default()
    })?;

    let signer = PrivateKeySigner::from_hex("0xdeadbeef...")?;
    let status = trader.ensure_ready(Box::new(signer), None).await?;

    println!("wallet: {}", status.wallet);
    println!("funder: {}", status.funder_address);
    println!("safe deployed: {}", status.safe_deployed);
    println!("approvals set: {}", status.approvals_set);
    println!("actions taken: {:?}", status.actions);

    trader.close();
    Ok(())
}

Linking an existing wallet (skip onboarding)

If you already have CLOB credentials from somewhere else (exported from a previous session, another SDK, or Polymarket directly), use link_credentials + link_wallet to skip ensure_ready entirely. Nothing gets deployed or re-approved — you just import the creds and attach a signer for order signing.
use polynode::trading::{
    LinkCredentialsOpts, LinkOpts, PolyNodeTrader, PrivateKeySigner, SignatureType, TraderConfig,
};

let mut trader = PolyNodeTrader::new(TraderConfig {
    polynode_key: "pn_live_...".into(),
    ..Default::default()
})?;

// 1. Import existing CLOB creds (they're written to the local SQLite DB)
trader.link_credentials(LinkCredentialsOpts {
    wallet: "0xYOUR_EOA".into(),
    api_key: "clob-api-key-uuid".into(),
    api_secret: "clob-api-secret".into(),
    api_passphrase: "clob-api-passphrase".into(),
    signature_type: Some(SignatureType::PolyGnosisSafe),
    funder_address: Some("0xYOUR_SAFE".into()),
})?;

// 2. Attach the signer (PrivateKeySigner, Privy, or your own TradingSigner impl)
let signer = PrivateKeySigner::from_hex("0xYOUR_EOA_PRIVATE_KEY")?;
trader.link_wallet(
    Box::new(signer),
    Some(LinkOpts { signature_type: Some(SignatureType::PolyGnosisSafe) }),
).await?;

// Now trader.order / trader.wrap_to_polyusd / etc. use the Safe+relayer path.
SignatureType values:
  • SignatureType::Eoa — sign as the EOA directly (pay gas in MATIC, tx from EOA address)
  • SignatureType::PolyProxy — legacy Polymarket proxy wallet (very rare)
  • SignatureType::PolyGnosisSafe — default; EOA signs Safe execTransaction payloads, Polymarket relayer submits gaslessly
For multi-user backends, call link_credentials + link_wallet for each user, or keep a PolyNodeTrader per user with a separate db_path.

Place an Order

use polynode::trading::{PolyNodeTrader, TraderConfig, PrivateKeySigner, OrderParams, OrderSide, OrderType};

# async fn example() -> polynode::Result<()> {
let mut trader = PolyNodeTrader::new(TraderConfig {
    polynode_key: "pn_live_...".into(),
    ..Default::default()
})?;

let signer = PrivateKeySigner::from_hex("0x...")?;
trader.ensure_ready(Box::new(signer), None).await?;

let result = trader.order(OrderParams {
    token_id: "51037625779056581606819614184446816710505006861008496087735536016411882582167".into(),
    side: OrderSide::Buy,
    price: 0.55,
    size: 100.0,
    order_type: OrderType::GTC,
    expiration: None,
    post_only: false,
    fee_config: None,
}).await?;

if result.success {
    println!("order placed: {:?}", result.order_id);
} else {
    println!("order failed: {:?}", result.error);
}

trader.close();
# Ok(())
# }

Order Types

TypeBehavior
GTCGood til canceled (default)
GTDGood til date (set expiration to a Unix timestamp)
FOKFill or kill, entire order fills or nothing
FAKFill and kill, partial fills allowed, remainder canceled

Cancel Orders

# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Cancel a specific order
let result = trader.cancel_order("order_id_here").await?;
println!("canceled: {:?}", result.canceled);

// Cancel all orders
let result = trader.cancel_all(None).await?;

// Cancel all orders for a specific market
let result = trader.cancel_all(Some("condition_id")).await?;
# Ok(())
# }

Query Open Orders

# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// All open orders
let orders = trader.get_open_orders(None).await?;
for o in &orders {
    println!("{}: {} {} @ {} (matched: {}/{})",
        o.id, o.side, o.asset_id, o.price, o.size_matched, o.original_size);
}

// Open orders for a specific market
let orders = trader.get_open_orders(Some("condition_id")).await?;
# Ok(())
# }

Pre-Trade Checks

# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Check token approvals
let approvals = trader.check_approvals(None).await?;
println!("all approved: {}", approvals.all_approved);

// Check USDC and POL balances
let balance = trader.check_balance(None).await?;
println!("USDC: {}, POL: {}", balance.usdc, balance.matic);
# Ok(())
# }

Local Order History

All orders are logged locally in SQLite:
use polynode::trading::HistoryParams;

# fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
let history = trader.get_order_history(Some(HistoryParams {
    limit: Some(50),
    offset: None,
    token_id: None,
    side: None,
}))?;
for row in &history {
    println!("{}: {} {} @ {} — {}", row.token_id, row.side, row.size, row.price, row.status);
}
# Ok(())
# }

Wallet Export and Import

Back up and restore wallet credentials:
# fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Export
let exported = trader.export_wallet(None)?;
if let Some(ref data) = exported {
    let json = serde_json::to_string_pretty(data).unwrap();
    std::fs::write("wallet-backup.json", json).unwrap();
}

// Import
let json = std::fs::read_to_string("wallet-backup.json").unwrap();
let data: polynode::trading::WalletExport = serde_json::from_str(&json).unwrap();
trader.import_wallet(data)?;
# Ok(())
# }

Polymarket V2 Exchange

The SDK supports the Polymarket V2 exchange system. Set exchange_version in your config to target V2. Defaults to V1 — no existing code is affected.
use polynode::trading::{PolyNodeTrader, TraderConfig, ExchangeVersion};

# async fn example() -> polynode::Result<()> {
let mut trader = PolyNodeTrader::new(TraderConfig {
    polynode_key: "pn_live_...".into(),
    exchange_version: ExchangeVersion::V2,
    ..Default::default()
})?;
# Ok(())
# }
V2 uses PolyUSD as collateral instead of USDC.e. The SDK provides helper methods for wrapping and balance checking:
# async fn example(trader: &mut polynode::trading::PolyNodeTrader) -> polynode::Result<()> {
// Wrap USDC.e → PolyUSD (amount in raw units, 6 decimals)
let tx_hash = trader.wrap_to_polyusd(1_000_000).await?; // 1 USDC

// Unwrap PolyUSD → USDC.e
let tx_hash = trader.unwrap_from_polyusd(1_000_000).await?;

// Check balances
let polyusd = trader.get_polyusd_balance().await?;
let usdce = trader.get_usdce_balance().await?;
# Ok(())
# }
Order placement, cancellation, and all other trading methods work identically on V2. See the V2 Migration Guide and PolyUSD Guide for full details.

Custom Signers

Implement the TradingSigner trait for HSM, KMS, or other signing backends:
use polynode::trading::{TradingSigner, Address, async_trait};

struct MyHsmSigner { /* ... */ }

#[async_trait]
impl TradingSigner for MyHsmSigner {
    fn address(&self) -> Address {
        // Return the EOA address
        todo!()
    }

    async fn sign_typed_data(
        &self,
        payload: &polynode::trading::Eip712Payload,
    ) -> polynode::Result<Vec<u8>> {
        // Sign EIP-712 typed data, return 65-byte signature
        todo!()
    }

    async fn sign_message(&self, message: &[u8]) -> polynode::Result<Vec<u8>> {
        // Sign raw message (personal_sign), return 65-byte signature
        todo!()
    }
}

Fee Escrow

Charge per-order fees with on-chain escrow. Fees are pulled before the order, distributed on fill, and refunded on cancel. See the Fee Escrow Guide for the full architecture and security model.
use polynode::trading::{PolyNodeTrader, TraderConfig, FeeConfig, OrderParams, OrderSide, OrderType};

let mut trader = PolyNodeTrader::new(TraderConfig {
    polynode_key: "pn_live_...".into(),
    fee_config: Some(FeeConfig {
        fee_bps: 50,  // 0.5% fee on every order
        affiliate: Some("0xYourWallet...".into()),  // REQUIRED: your wallet receives the fee
        affiliate_share_bps: None,  // default: you keep 100%
    }),
    ..Default::default()
})?;

let result = trader.order(OrderParams {
    token_id: "...".into(),
    side: OrderSide::Buy,
    price: 0.55,
    size: 100.0,
    order_type: OrderType::GTC,
    expiration: None,
    post_only: false,
    fee_config: None,  // uses global fee_config from TraderConfig
}).await?;

println!("Fee TX: {:?}", result.fee_escrow_tx_hash);
println!("Fee: {:?} USDC", result.fee_amount);

// Cancel → fee is automatically refunded
trader.cancel_order("order-id").await?;
Set fee_bps: 0 or omit fee_config to skip the escrow entirely. Per-order overrides via OrderParams.fee_config.

Privy Signer

Requires the privy feature flag: polynode = { version = "0.12.0", features = ["trading", "privy"] }
Sign orders with a Privy server-side wallet. No private key needed on your machine. All signing happens via Privy’s HTTP API.
use polynode::trading::{PolyNodeTrader, TraderConfig};
use polynode::trading::privy::{PrivyConfig, PrivySigner};

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let config = PrivyConfig {
        app_id: "your-privy-app-id".into(),
        app_secret: "your-privy-app-secret".into(),
        authorization_key: "wallet-auth:your-authorization-key".into(),
    };

    // Or load from PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY env vars:
    // let config = PrivyConfig::from_env()?;

    let signer = PrivySigner::new(
        config,
        "privy-wallet-id".into(),
        "0xYourWalletAddress".parse().unwrap(),
    );

    let mut trader = PolyNodeTrader::new(TraderConfig {
        polynode_key: "pn_live_...".into(),
        ..Default::default()
    })?;

    let status = trader.ensure_ready(Box::new(signer), None).await?;
    println!("ready: {}", status.credentials_stored);

    trader.close();
    Ok(())
}
The PrivySigner implements TradingSigner, so it works with ensure_ready(), order(), and all other trading methods. Get your Privy credentials from the Privy Dashboard.

TraderConfig

use polynode::trading::{TraderConfig, SignatureType, ExchangeVersion};

# fn example() {
let config = TraderConfig {
    polynode_key: "pn_live_...".into(),
    db_path: "./my-trading.db".into(),          // local SQLite for credentials + history
    cosigner_url: "https://trade.polynode.dev".into(), // default
    fallback_direct: true,                       // submit directly if cosigner is down
    default_signature_type: SignatureType::PolyGnosisSafe, // default
    rpc_url: "https://polygon-bor-rpc.publicnode.com".into(),  // default; for on-chain reads
    exchange_version: ExchangeVersion::V1,       // default; set to V2 for the Polymarket V2 exchange
};
# }

Redemption Watcher

Monitor wallets for redeemable positions after oracle resolution. The watcher fetches current positions via REST, then listens for real-time oracle events on the WebSocket and emits alerts when a watched wallet holds a position in a resolved market.
use polynode::{PolyNodeClient, RedemptionWatcher, RedemptionWatcherConfig};
use std::sync::Arc;

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = Arc::new(PolyNodeClient::new("pn_live_YOUR_KEY")?);

    let mut watcher = RedemptionWatcher::new(client, RedemptionWatcherConfig {
        track_position_changes: true,
        refresh_interval_secs: 300,
        compress: true,
    });

    // Start watching specific wallets
    watcher.start(&[
        "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6",
        "0xBB39C16C3fc54d3C9B1f9f9E8dF4a09Ee25AB7df",
    ]).await?;

    println!("tracking {} positions", watcher.size());

    // Add more wallets at runtime
    watcher.add_wallets(&["0x7a25dA10f8cA3b67D5fF55e87E2B0C076D3Dd0bD"]).await?;

    // Listen for alerts
    while let Some(alert) = watcher.next_alert().await {
        if alert.is_winner {
            println!("REDEEMABLE: {} holds {} on '{}' — payout: ${:.2}",
                alert.wallet, alert.outcome, alert.market_title, alert.estimated_payout_usd);
        } else {
            println!("RESOLVED (loss): {} on '{}'", alert.wallet, alert.market_title);
        }
    }

    watcher.close();
    Ok(())
}

Local Cache

Requires the cache feature flag: polynode = { version = "0.12.0", features = ["cache"] }
Local SQLite cache that backfills trade history on startup, streams live updates via WebSocket, and serves all queries locally with zero additional API calls after initialization. Driven by a JSON watchlist file that specifies which wallets, markets, and tokens to track.

Setup

Create a polynode.watch.json file:
{
  "version": 1,
  "wallets": [
    { "address": "0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6", "label": "whale-1", "backfill": true },
    { "address": "0xBB39C16C3fc54d3C9B1f9f9E8dF4a09Ee25AB7df", "label": "whale-2", "backfill": true }
  ],
  "markets": [
    { "condition_id": "0xabc...", "label": "bitcoin-100k", "backfill": true }
  ],
  "tokens": [],
  "settings": {
    "ttl_days": 30,
    "backfill_rate": 2.0,
    "purge_on_remove": false
  }
}

Start the Cache

use polynode::{PolyNodeClient, cache::PolyNodeCache};
use std::sync::Arc;

#[tokio::main]
async fn main() -> polynode::Result<()> {
    let client = Arc::new(PolyNodeClient::new("pn_live_YOUR_KEY")?);

    let mut cache = PolyNodeCache::builder(client)
        .db_path("./polynode-cache.db")
        .watchlist_path("./polynode.watch.json")
        .ttl_seconds(30 * 86400)            // 30 days
        .backfill_rate(2.0)                  // 2 requests/sec
        .backfill_pages(3)                   // 3 pages per entity
        .backfill_page_size(500)             // 500 trades per page
        .purge_on_remove(false)              // keep data when removing from watchlist
        .on_backfill_progress(|p| {
            println!("[{}] {}: {} fetched ({})", p.entity_type, p.label, p.fetched, p.status);
        })
        .build()?;

    cache.start().await?;

    // All queries are local, instant, no API calls
    let positions = cache.wallet_positions("0x1a1A27de044faFFCCf68E28F03dCfCf5eB3d3cE6")?;
    for p in &positions {
        println!("{}: {} {} @ {:.4} (pnl: {:?})",
            p.market_title, p.outcome, p.size, p.avg_price, p.cash_pnl);
    }

    cache.stop().await?;
    Ok(())
}

Query Methods

All queries are synchronous and read from local SQLite:
use polynode::cache::{QueryOptions, OrderBy};

# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
// Wallet positions
let positions = cache.wallet_positions("0xabc...")?;

// Multi-wallet positions
let multi = cache.multi_wallet_positions(&[
    "0xabc...".into(),
    "0xdef...".into(),
])?;

// Wallet trades with filters
let trades = cache.wallet_trades("0xabc...", &QueryOptions {
    limit: Some(100),
    offset: None,
    since: Some(1711843200.0),
    until: None,
    side: Some("BUY".into()),
    order_by: Some(OrderBy::TimestampDesc),
})?;

// Market trades
let market_trades = cache.market_trades("condition_id", &QueryOptions::default())?;

// Market positions
let market_pos = cache.market_positions("condition_id")?;

// Token trades
let token_trades = cache.token_trades("token_id", &QueryOptions::default())?;

// Settlements
let settlements = cache.wallet_settlements("0xabc...", &QueryOptions::default())?;

// Lookup by tx hash
let tx = cache.trade_by_tx_hash("0xdeadbeef...")?;

// Realized P&L (weighted average cost basis)
let pnl = cache.wallet_realized_pnl("0xabc...")?;
println!("realized: ${:.2}, unrealized: ${:.2}, confidence: {}",
    pnl.total_realized_pnl, pnl.total_unrealized_pnl, pnl.confidence);

// Cache stats
let stats = cache.stats()?;
println!("{} trades, {} settlements, {:.1}MB",
    stats.trade_count, stats.settlement_count, stats.db_size_bytes as f64 / 1_048_576.0);
# Ok(())
# }

Runtime Watchlist Management

Add or remove entities without restarting:
use polynode::cache::EntityType;

# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
// Add a wallet at runtime (will backfill automatically)
cache.add_to_watchlist(&[
    (EntityType::Wallet, "0xnew...".into(), "new-whale".into(), true),
])?;

// Remove a wallet
cache.remove_from_watchlist(&[
    (EntityType::Wallet, "0xold...".into()),
])?;
# Ok(())
# }
The cache also watches the polynode.watch.json file for changes. Edit the file and the cache picks up additions and removals automatically.

Manual Pruning

# fn example(cache: &polynode::cache::PolyNodeCache) -> polynode::Result<()> {
let pruned = cache.prune()?;
println!("pruned {} expired records", pruned);
# Ok(())
# }

Testing Utilities

Helpers for integration tests that need active wallet addresses:
use polynode::testing;

#[tokio::main]
async fn main() {
    // Get a single active wallet (from leaderboard or fallback list)
    let wallet = testing::get_active_test_wallet(true).await;
    println!("test wallet: {}", wallet);

    // Get multiple
    let wallets = testing::get_active_test_wallets(3, true).await;
    for w in &wallets {
        println!("  {}", w);
    }
}
Set fresh to true to attempt fetching recently active wallets from the Polymarket leaderboard. Falls back to a hardcoded list of known-active addresses.

Error Handling

All SDK methods return polynode::Result<T>, which wraps polynode::Error:
use polynode::{PolyNodeClient, Error};

# async fn example() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_YOUR_KEY")?;

match client.market("invalid-token-id").await {
    Ok(market) => println!("{:?}", market),
    Err(Error::NotFound(msg)) => println!("not found: {}", msg),
    Err(Error::Auth(msg)) => println!("auth failed: {}", msg),
    Err(Error::RateLimited(msg)) => println!("rate limited: {}", msg),
    Err(Error::Api { status, message }) => println!("API error {}: {}", status, message),
    Err(Error::Http(e)) => println!("network error: {}", e),
    Err(Error::Disconnected) => println!("WebSocket disconnected"),
    Err(e) => println!("other: {}", e),
}
# Ok(())
# }

Error Variants

VariantWhen it occurs
HttpNetwork-level request failure
WebSocketWebSocket connection or protocol error
JsonResponse deserialization failure
Api { status, message }Server returned a non-success status
Auth401 or 403 from the API
RateLimited429 from the API
NotFound404 from the API
DisconnectedWebSocket connection lost
Decompressionzlib decompression failure
ConnectionClosedServer closed the WebSocket
UrlURL parse error
TradingTrading-specific error (feature: trading)
SigningEIP-712 signing failure (feature: trading)
SqliteLocal database error (feature: cache or trading)
IoFile I/O error (feature: cache or trading)
CacheCache-specific error (feature: cache)

Source

GitHub | crates.io