# 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.4:** [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.4"
tokio = { version = "1", features = ["rt", "macros"] }
# For local cache (optional):
# polynode = { version = "0.4", 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(())
}
```
## 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.4", 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