# polynode
Rust SDK for the [PolyNode](https://polynode.dev) 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.13.11:** Rust V2 trading now separates user CLOB credentials, public builder attribution, and relayer auth. The default path uses PolyNode's public builder code plus PolyNode managed relay with a per-user relayer key; users can still bring their own builder code, direct Polymarket builder credentials, or direct RPC submission for Safe/proxy calls.
**New in v0.13.10:** Published docs now include explicit Rust examples for both supported V2 wallet identities: existing Safe users (`POLY_GNOSIS_SAFE`) and new deposit-wallet users (`POLY_1271`).
**New in v0.13.9:** Rust V2 trading keeps Safe and deposit-wallet identities separate: pass an explicit `signature_type` plus `funder_address` to route existing Safe users through `POLY_GNOSIS_SAFE`, while new deposit-wallet users continue through `POLY_1271`. Safe V2 onboarding can now relay pUSD/CTF approvals, and stored credentials are reused without silently flipping wallet type.
**New in v0.13.8:** Rust deposit-wallet V2 trading now self-heals deployed-wallet onboarding, includes USDC.e collateral-onramp approval, uses the deposit wallet as both order maker and signer for `POLY_1271`, wraps available USDC.e into pUSD before buy orders, and matches Polymarket's current V2 `POLY_1271` order body and wrapped signature format.
**New in v0.12:** V2 order flow — place orders on the Polymarket V2 CLOB with pUSD collateral and builder attribution. See [`src/trading/V2_ORDER_FLOW.md`](src/trading/V2_ORDER_FLOW.md) for the full reference.
**In v0.5:** [Local Cache](#local-cache) — SQLite-backed local storage. Backfill wallet history in seconds, query trades and positions instantly with zero API calls.
## Install
```toml
[dependencies]
polynode = "0.13.11"
tokio = { version = "1", features = ["rt", "macros"] }
# For local cache (optional):
# polynode = { version = "0.13.11", features = ["cache"] }
```
## Quick Start
```rust
use polynode::PolyNodeClient;
#[tokio::main]
async fn main() -> polynode::Result<()> {
let client = PolyNodeClient::new("pn_live_...")?;
// Fetch top markets
let markets = client.markets(Some(10)).await?;
println!("{} markets, {} total", markets.count, markets.total);
// Search
let results = client.search("bitcoin", Some(5), None).await?;
for r in &results.results {
println!("{}", r.question.as_deref().unwrap_or("?"));
}
Ok(())
}
```
## Rust V2 Trading Wallet Modes
Polymarket V2 supports two smart-wallet identities. They are not interchangeable:
- Existing Safe users: `SignatureType::PolyGnosisSafe`, with the Safe address as `funder_address`.
- New deposit-wallet users: `SignatureType::Poly1271`, with the deposit wallet address as `funder_address`.
Pass both values explicitly whenever you know the user's Polymarket wallet type. This prevents the SDK from deriving a different wallet and sending orders with a maker address that the CLOB does not allow.
The SDK also separates three credential paths:
- User CLOB API credentials are created or loaded by `ensure_ready()` and always authenticate order placement, cancels, open orders, and balance/allowance checks.
- V2 builder codes are public bytes32 attribution values. `TraderConfig::default()` uses PolyNode's builder code; users can override it with `TraderConfig.builder_code` or per order with `OrderParams.builder`.
- Relayer auth is used only for gasless smart-wallet calls such as deploy, approve, wrap, and unwrap. `RelayerMode::Auto` uses PolyNode managed relay when `polynode_key` and `cosigner_url` are configured, then falls back to direct builder credentials when supplied. `RelayerMode::DirectRpc` bypasses the relayer for Safe/proxy calls and pays gas from the signer.
Existing Safe user:
```rust
use polynode::trading::{
EnsureReadyOpts, ExchangeVersion, PolyNodeTrader, PrivateKeySigner,
SignatureType, TraderConfig,
};
let signer = PrivateKeySigner::from_hex(&private_key)?;
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
..Default::default()
})?;
trader.ensure_ready(
Box::new(signer),
Some(EnsureReadyOpts {
signature_type: Some(SignatureType::PolyGnosisSafe),
funder_address: Some(safe_address),
}),
).await?;
```
Deposit-wallet user:
```rust
use polynode::trading::{
onboarding::derive_deposit_wallet_address, EnsureReadyOpts, ExchangeVersion,
PolyNodeTrader, PrivateKeySigner, SignatureType, TraderConfig, TradingSigner,
};
let signer = PrivateKeySigner::from_hex(&private_key)?;
let deposit_wallet = format!("{}", derive_deposit_wallet_address(signer.address()));
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
default_signature_type: SignatureType::Poly1271,
..Default::default()
})?;
trader.ensure_ready(
Box::new(signer),
Some(EnsureReadyOpts {
signature_type: Some(SignatureType::Poly1271),
funder_address: Some(deposit_wallet),
}),
).await?;
```
Both paths use pUSD on V2. The SDK can wrap available USDC.e into pUSD before BUY orders when the configured wallet has the required onramp approval and a relayer path is configured.
Bring your own builder attribution while keeping PolyNode managed relay:
```rust
use polynode::trading::{ExchangeVersion, PolyNodeTrader, TraderConfig};
let mut trader = PolyNodeTrader::new(TraderConfig {
polynode_key: "pn_live_...".into(),
exchange_version: ExchangeVersion::V2,
builder_code: Some("0xYourBuilderCodeBytes32".into()),
..Default::default()
})?;
```
Use direct Polymarket builder credentials for relayer auth instead:
```rust
use polynode::trading::{
BuilderCredentials, ExchangeVersion, PolyNodeTrader, RelayerMode, TraderConfig,
};
let mut trader = PolyNodeTrader::new(TraderConfig {
exchange_version: ExchangeVersion::V2,
relayer_mode: RelayerMode::BuilderCredentials,
builder_code: Some("0xYourBuilderCodeBytes32".into()),
builder_credentials: Some(BuilderCredentials {
key: builder_key,
secret: builder_secret,
passphrase: builder_passphrase,
}),
..Default::default()
})?;
```
Bypass the relayer for Safe/proxy calls:
```rust
use polynode::trading::{ExchangeVersion, PolyNodeTrader, RelayerMode, TraderConfig};
let mut trader = PolyNodeTrader::new(TraderConfig {
exchange_version: ExchangeVersion::V2,
relayer_mode: RelayerMode::DirectRpc,
rpc_url: "https://polygon-bor-rpc.publicnode.com".into(),
..Default::default()
})?;
```
`DirectRpc` signs Safe `execTransaction` calls and broadcasts through `rpc_url`. It requires a signer with `TradingSigner::sign_hash` support, such as `PrivateKeySigner`; the built-in `PrivySigner` does not currently sign raw transactions. It is not used for deposit-wallet factory calls.
## REST API
```rust
// System
client.healthz().await?;
client.status().await?;
client.create_key(Some("my-bot")).await?;
// Markets
client.markets(Some(10)).await?;
client.market("token_id").await?;
client.market_by_slug("bitcoin-100k").await?;
client.market_by_condition("0xabc...").await?;
client.list_markets(&ListMarketsParams {
count: Some(20),
sort: Some("volume".into()),
..Default::default()
}).await?;
client.search("ethereum", Some(5), None).await?;
// Pricing
client.candles("token_id", Some(CandleResolution::OneHour), Some(100)).await?;
client.stats("token_id").await?;
// Settlements
client.recent_settlements(Some(20)).await?;
client.token_settlements("token_id", Some(10)).await?;
client.wallet_settlements("0xabc...", Some(10)).await?;
// Wallets
client.wallet("0xabc...").await?;
// RPC (rpc.polynode.dev)
client.rpc_call("eth_blockNumber", serde_json::json!([])).await?;
```
## WebSocket Streaming
```rust
use polynode::ws::{Subscription, SubscriptionType, StreamOptions};
use polynode::WsMessage;
let mut stream = client.stream(StreamOptions {
compress: true,
auto_reconnect: true,
..Default::default()
}).await?;
stream.subscribe(
Subscription::new(SubscriptionType::Settlements)
.min_size(100.0)
.status("pending")
.snapshot_count(20)
).await?;
while let Some(msg) = stream.next().await {
match msg? {
WsMessage::Event(event) => {
match event {
polynode::PolyNodeEvent::Settlement(s) => {
println!("{} ${:.2} on {}",
s.taker_side, s.taker_size,
s.market_title.as_deref().unwrap_or("unknown"));
}
polynode::PolyNodeEvent::StatusUpdate(u) => {
println!("Confirmed in {}ms", u.latency_ms);
}
_ => {}
}
}
WsMessage::Snapshot(events) => {
println!("Snapshot: {} events", events.len());
}
_ => {}
}
}
```
### Subscription Types
```rust
SubscriptionType::Settlements // pending + confirmed settlements
SubscriptionType::Trades // all trade activity
SubscriptionType::Prices // price-moving events
SubscriptionType::Blocks // new Polygon blocks
SubscriptionType::Wallets // all wallet activity
SubscriptionType::Markets // all market activity
SubscriptionType::LargeTrades // $1K+ trades
SubscriptionType::Oracle // UMA resolution events
SubscriptionType::Chainlink // real-time price feeds
```
### Subscription Filters
```rust
Subscription::new(SubscriptionType::Settlements)
.wallets(vec!["0xabc...".into()])
.tokens(vec!["21742633...".into()])
.slugs(vec!["bitcoin-100k".into()])
.condition_ids(vec!["0xabc...".into()])
.side("BUY")
.status("pending")
.min_size(100.0)
.max_size(10000.0)
.event_types(vec!["settlement".into()])
.snapshot_count(50)
.feeds(vec!["BTC/USD".into()])
```
## Orderbook Streaming
```rust
use polynode::{ObStreamOptions, ObMessage, OrderbookUpdate, LocalOrderbook};
let mut stream = client.orderbook_stream(ObStreamOptions::default()).await?;
stream.subscribe(vec!["token_id_1".into(), "token_id_2".into()]).await?;
let mut book = LocalOrderbook::new();
while let Some(msg) = stream.next().await {
match msg? {
ObMessage::Update(OrderbookUpdate::Snapshot(snap)) => {
book.apply_snapshot(&snap);
println!("{}: {} bids, {} asks", snap.asset_id, snap.bids.len(), snap.asks.len());
}
ObMessage::Update(OrderbookUpdate::Update(delta)) => {
book.apply_update(&delta);
}
ObMessage::Update(OrderbookUpdate::PriceChange(change)) => {
for asset in &change.assets {
println!("{} {}: {}", change.market, asset.outcome, asset.price);
}
}
_ => {}
}
}
// Query local state
let best_bid = book.best_bid("token_id");
let best_ask = book.best_ask("token_id");
let spread = book.spread("token_id");
```
## OrderbookEngine
Higher-level orderbook client. One connection, shared state, filtered views for different parts of your app.
```rust
use polynode::orderbook::engine::{OrderbookEngine, EngineOptions};
let engine = OrderbookEngine::connect("pn_live_...", EngineOptions::default()).await?;
// Subscribe with token IDs, slugs, or condition IDs
engine.subscribe(vec![token_a.into(), token_b.into()]).await?;
// Query computed values from local state
engine.midpoint(&token_a).await; // Some(0.465)
engine.spread(&token_a).await; // Some(0.01)
engine.best_bid(&token_a).await; // Some(OrderbookLevel { price: "0.46", size: "226.29" })
engine.book(&token_a).await; // Some((bids, asks))
// Create filtered views for different components
let mut view = engine.view(vec![token_a.into()]);
view.midpoint(&token_a).await; // reads from shared state
// Receive only this view's updates
while let Some(update) = view.next().await {
// only token_a updates arrive here
}
engine.close().await?;
```
Zlib compression is enabled by default (~50% bandwidth savings). All connections auto-reconnect with exponential backoff.
## 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.
Enable the `cache` feature in your `Cargo.toml`:
```toml
polynode = { version = "0.13.11", features = ["cache"] }
```
```rust
use polynode::{PolyNodeClient, cache::{PolyNodeCache, QueryOptions}};
use std::sync::Arc;
let client = Arc::new(PolyNodeClient::new("pn_live_...")?);
let mut cache = PolyNodeCache::builder(client)
.db_path("./cache.db")
.watchlist_path("./polynode.watch.json")
.on_backfill_progress(|p| println!("{}: {} trades", p.label, p.fetched))
.build()?;
cache.start().await?;
// Query locally — instant, no API calls
let trades = cache.wallet_trades("0xabc...", &QueryOptions { limit: Some(50), ..Default::default() })?;
let positions = cache.wallet_positions("0xabc...")?;
let stats = cache.stats()?;
// Add wallets at runtime
use polynode::cache::EntityType;
cache.add_to_watchlist(&[(EntityType::Wallet, "0xnew...".into(), "whale".into(), true)])?;
cache.stop().await?;
```
**Watchlist** (`polynode.watch.json`):
```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. See [full documentation](https://docs.polynode.dev/sdks/local-cache).
## Configuration
```rust
let client = PolyNodeClient::builder("pn_live_...")
.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(10))
.build()?;
```
## Error Handling
```rust
use polynode::Error;
match client.market("invalid-id").await {
Ok(detail) => println!("{:?}", detail),
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(e) => println!("Other error: {}", e),
}
```
## Links
- [Documentation](https://docs.polynode.dev)
- [API Reference](https://docs.polynode.dev/api-reference)
- [Get an API Key](https://polynode.dev)
- [TypeScript SDK](https://www.npmjs.com/package/polynode-sdk)
## License
MIT