# 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.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.10"
tokio = { version = "1", features = ["rt", "macros"] }
# For local cache (optional):
# polynode = { version = "0.13.10", 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.
Existing Safe user:
```rust
use polynode::trading::{
BuilderCredentials, EnsureReadyOpts, ExchangeVersion, PolyNodeTrader,
PrivateKeySigner, SignatureType, TraderConfig,
};
let signer = PrivateKeySigner::from_hex(&private_key)?;
let mut trader = PolyNodeTrader::new(TraderConfig {
exchange_version: ExchangeVersion::V2,
builder_credentials: Some(BuilderCredentials {
key: builder_key,
secret: builder_secret,
passphrase: builder_passphrase,
}),
..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, BuilderCredentials, 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 {
exchange_version: ExchangeVersion::V2,
default_signature_type: SignatureType::Poly1271,
builder_credentials: Some(BuilderCredentials {
key: builder_key,
secret: builder_secret,
passphrase: builder_passphrase,
}),
..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 builder credentials are available for smart-wallet execution.
## 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.10", 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