polyoxide-clob
Rust client library for Polymarket CLOB (Central Limit Order Book) API.
Provides authenticated order creation, EIP-712 signing, and submission, plus read-only market data and order book access. Part of the polyoxide workspace.
More information about this crate can be found in the crate documentation.
Features
- Order Management: Create, sign, and post limit and market orders with EIP-712
- Market Data: Order books, prices, midpoints, spreads, last trade prices, and price history
- Account Management: Balances, allowances, trade history, and session heartbeats
- API Key Management: Create, list, and delete standard, read-only, and builder API keys
- Liquidity Rewards: Query earnings, percentages, and reward markets
- Notifications: List and dismiss user notifications
- WebSocket: Real-time market data and user order/trade updates (feature-gated)
Installation
cargo add polyoxide-clob
Feature Flags
| Feature |
Default |
Description |
gamma |
Yes |
Enables the polyoxide-gamma dependency, used to auto-resolve proxy wallet addresses for proxy signature types |
ws |
No |
Enables WebSocket support (tokio-tungstenite, futures-util) for real-time streaming |
keychain |
No |
Enables OS keychain storage for credentials via keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service) |
cargo add polyoxide-clob --features ws
cargo add polyoxide-clob --no-default-features
Authentication
The CLOB API uses two authentication layers:
- L1 (EIP-712): On-chain signing with a private key via
alloy. Used for API key creation/derivation.
- L2 (HMAC-SHA256): Signing with API credentials. Used for order management, account operations, and all authenticated endpoints.
Both layers are managed through the Account type.
Environment Variables
POLYMARKET_PRIVATE_KEY # Hex-encoded private key
POLYMARKET_API_KEY # L2 API key
POLYMARKET_API_SECRET # L2 API secret (base64)
POLYMARKET_API_PASSPHRASE # L2 API passphrase
Usage
Client Construction
# async fn doctest(credentials: polyoxide_clob::Credentials) -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::{Account, Chain, Clob, ClobBuilder, Credentials};
let clob = Clob::public();
let account = Account::from_env()?;
let clob = Clob::from_account(account)?;
let clob = Clob::new("0xprivate_key", credentials)?;
# let account = Account::from_env()?;
let clob = ClobBuilder::new()
.with_account(account)
.chain(Chain::PolygonMainnet)
.base_url("https://clob.polymarket.com")
.timeout_ms(30_000)
.pool_size(10)
.max_concurrent(16) .build()?;
# Ok(())
# }
Account Configuration
# fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::{Account, Credentials};
let account = Account::from_env()?;
let account = Account::from_file("config/account.json")?;
let account = Account::from_keychain()?;
let credentials = Credentials {
key: "api_key".to_string(),
secret: "api_secret".to_string(),
passphrase: "passphrase".to_string(),
};
let account = Account::new("0x...", credentials)?;
# Ok(())
# }
API Namespaces
The client organizes endpoints into namespaces. Public namespaces are always available; authenticated namespaces return Result<_, ClobError> and require an Account.
| Namespace |
Access |
Method |
markets() |
Public |
clob.markets() |
health() |
Public |
clob.health() |
orders() |
Authenticated |
clob.orders()? |
account_api() |
Authenticated |
clob.account_api()? |
auth() |
Authenticated |
clob.auth()? |
rewards() |
Authenticated |
clob.rewards()? |
notifications() |
Authenticated |
clob.notifications()? |
Market Data (public)
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::{Clob, OrderSide};
let clob = Clob::public();
let markets = clob.markets().list().send().await?;
let market = clob.markets().get("0xcondition_id").send().await?;
let book = clob.markets().order_book("token_id").send().await?;
println!("{} bids, {} asks", book.bids.len(), book.asks.len());
let price = clob.markets().price("token_id", OrderSide::Buy).send().await?;
let mid = clob.markets().midpoint("token_id").send().await?;
let spread = clob.markets().spread("token_id").send().await?;
let last = clob.markets().last_trade_price("token_id").send().await?;
let history = clob.markets().prices_history("token_id").send().await?;
use polyoxide_clob::BookParams;
let params = vec![BookParams { token_id: "t1".into(), side: None }];
let books = clob.markets().order_books(¶ms).await?;
# Ok(())
# }
Placing Orders (authenticated)
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::{Account, Clob, CreateOrderParams, OrderKind, OrderSide};
let account = Account::from_env()?;
let clob = Clob::from_account(account)?;
let params = CreateOrderParams {
token_id: "token_id".to_string(),
price: 0.52,
size: 100.0,
side: OrderSide::Buy,
order_type: OrderKind::Gtc,
post_only: false,
expiration: None,
funder: None,
signature_type: None,
};
let response = clob.place_order(¶ms, None).await?;
if response.success {
println!("Order placed: {:?}", response.order_id);
}
let order = clob.create_order(¶ms, None).await?;
let signed = clob.sign_order(&order).await?;
let response = clob.post_order(&signed, OrderKind::Gtc, false).await?;
# Ok(())
# }
Order Management (authenticated)
# use polyoxide_clob::Clob;
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
# let clob = Clob::public();
let orders = clob.orders()?.list().send().await?;
let order = clob.orders()?.get("order_id").send().await?;
let result = clob.orders()?.cancel("order_id").send().await?;
let result = clob.orders()?.cancel_many(vec!["id1".into(), "id2".into()]).await?;
let result = clob.orders()?.cancel_all().await?;
let scoring = clob.orders()?.is_scoring("order_id").send().await?;
# Ok(())
# }
Account Operations (authenticated)
# use polyoxide_clob::Clob;
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
# let clob = Clob::public();
# let builder_code = "my_builder_code";
let bal = clob.account_api()?.balance_allowance("token_id").send().await?;
let usdc = clob.account_api()?.usdc_balance().send().await?;
let trades = clob.account_api()?.trades("0xmaker_address")
.market("0xcondition_id")
.after("1700000000")
.send()
.await?;
let builder_trades = clob.account_api()?.builder_trades(builder_code).send().await?;
# Ok(())
# }
Health and Latency
# use polyoxide_clob::Clob;
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
let clob = Clob::public();
let latency = clob.health().ping().await?;
println!("Latency: {}ms", latency.as_millis());
let time = clob.health().server_time().send().await?;
# Ok(())
# }
WebSocket (feature ws)
Market Channel
Subscribe to real-time order book and price updates (no authentication required):
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::ws::{WebSocket, Channel, MarketMessage};
use futures_util::StreamExt;
let mut ws = WebSocket::connect_market(vec![
"asset_id".to_string(),
]).await?;
while let Some(msg) = ws.next().await {
match msg? {
Channel::Market(MarketMessage::Book(book)) => {
println!("Order book: {} bids, {} asks", book.bids.len(), book.asks.len());
}
Channel::Market(MarketMessage::PriceChange(pc)) => {
println!("Price change: {:?}", pc.price_changes);
}
_ => {}
}
}
# Ok(())
# }
User Channel
Subscribe to authenticated order and trade updates:
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::ws::{ApiCredentials, WebSocket, Channel, UserMessage};
use futures_util::StreamExt;
let credentials = ApiCredentials::from_env()?;
let mut ws = WebSocket::connect_user(
vec!["condition_id".to_string()],
credentials,
).await?;
while let Some(msg) = ws.next().await {
match msg? {
Channel::User(UserMessage::Order(order)) => {
println!("Order update: {} {:?}", order.id, order.order_type);
}
Channel::User(UserMessage::Trade(trade)) => {
println!("Trade: {} @ {}", trade.size, trade.price);
}
_ => {}
}
}
# Ok(())
# }
Auto-Ping with WebSocketBuilder
For long-running connections with automatic keep-alive:
# async fn doctest() -> Result<(), Box<dyn std::error::Error>> {
use polyoxide_clob::ws::{WebSocketBuilder, Channel};
use std::time::Duration;
let ws = WebSocketBuilder::new()
.ping_interval(Duration::from_secs(10))
.connect_market(vec!["asset_id".to_string()])
.await?;
ws.run(|msg| async move {
println!("Received: {:?}", msg);
Ok(())
}).await?;
# Ok(())
# }
License
Licensed under either of MIT or Apache-2.0 at your option.