Unofficial Rust SDK for the Polymarket US Retail API.
- Resource-based API — Organized into focused clients (
client.markets(),client.orders(),client.events(), etc.) - Ed25519 request signing — Automatic X-PM-* authentication headers
- Typed async REST client — Markets, events, orders, portfolio, account, and search endpoints
- Async WebSocket streaming — Market data and order updates with automatic reconnect
- Order book & pricing data — Get order books, best bid/offer, settlement prices
- Builder-based configuration — Base URLs, timeouts, custom HTTP client
- Automatic retries — Exponential backoff with jitter on idempotent requests, honouring
Retry-After;POSTis never retried, so orders can't be duplicated - rustls throughout — No OpenSSL dependency
cargo add polymarket-us tokio --features tokio/macros,tokio/rt-multi-threadOr in Cargo.toml:
[dependencies]
polymarket-us = "0.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Requires Rust 1.86 or newer. TLS is provided by rustls, so no OpenSSL installation is needed.
Authenticated endpoints require:
POLYMARKET_US_KEY_IDPOLYMARKET_US_SECRET_KEY
POLYMARKET_US_SECRET_KEY must be Base64 that decodes to either:
- 64 bytes (keypair format, first 32 bytes are used as signing seed), or
- 32 bytes (raw Ed25519 seed).
Example:
export POLYMARKET_US_KEY_ID="your-key-id"
export POLYMARKET_US_SECRET_KEY="your-base64-secret"use polymarket_us::{PolymarketUsClient, PolymarketUsError};
#[tokio::main]
async fn main() -> Result<(), PolymarketUsError> {
let client = PolymarketUsClient::builder().build()?;
// Health check
let health = client.health().await?;
println!("status: {}", health.status);
// List markets
let markets = client.markets().list().await?;
println!("markets: {}", markets.markets.len());
// Get order book for a market
let book = client.markets().order_book("BTC-USD").await?;
println!("bid/ask: {} orders", book.bids.len() + book.asks.len());
Ok(())
}The SDK is organized into focused resource clients for better discoverability and maintainability:
Market discovery, order books, and pricing data.
// List markets
let markets = client.markets().list().await?;
// List with filters
let query = [("limit", "10"), ("category", "politics")];
let page = client.markets().list_with_query(&query).await?;
// Order book and pricing
let book = client.markets().order_book("BTC-USD").await?;
let bbo = client.markets().bbo("BTC-USD").await?; // Best bid/offer
let settlement = client.markets().settlement_price("BTC-USD").await?;Event-level metadata and context.
// List all events
let events = client.events().list().await?;
// Get event by ID or slug
let event = client.events().retrieve("event-123").await?;
let event = client.events().retrieve_by_slug("2024-us-election").await?;Complete order lifecycle management. All operations are authenticated.
use polymarket_us::types;
let order_req = types::PlaceOrderRequest {
symbol: "BTC-USD".to_string(),
action: types::OrderAction::Buy,
outcome_side: types::OrderSide::Long,
order_type: types::OrderType::Limit,
price: types::Money { value: "0.50".to_string(), currency: "USD".to_string() },
quantity: 100,
tif: types::TimeInForce::GoodTillCancel,
client_order_id: Some("my-order-1".to_string()),
post_only: false,
expires_at: None,
};
// Place order
let order = client.orders().create(&order_req).await?;
// Get open orders
let open = client.orders().open(None::<&()>).await?;
// Modify, cancel, preview
client.orders().modify(&order.order_id, &modify_req).await?;
client.orders().cancel(&order.order_id, &types::CancelOrderParams { quantity: None }).await?;
let estimate = client.orders().preview(&preview_req).await?;
// Close position
client.orders().close_position(&types::ClosePositionRequest {
symbol: "BTC-USD".to_string(),
quantity: 50,
}).await?;Account balances and buying power (authenticated).
let balances = client.account().balances().await?;
for balance in balances.balances {
println!("{}; balance={}, buying_power={}",
balance.currency,
balance.current_balance,
balance.buying_power
);
}Holdings and activity history (authenticated).
// Get positions
let positions = client.portfolio().positions().await?;
// Get activity with pagination
let query = [("limit", "50")];
let activities = client.portfolio().activities(&query).await?;Full-text search across markets and events.
let query = [("q", "bitcoin")];
let results = client.search().search(&query).await?;
// Search specific resource
let markets = client.search().markets(&query).await?;
let events = client.search().events(&query).await?;Use list_with_query() for filters, cursors, and pagination:
use polymarket_us::{PolymarketUsClient, PolymarketUsError};
use serde::Serialize;
#[derive(Serialize)]
struct MarketsQuery<'a> {
category: Option<&'a str>,
limit: Option<u32>,
cursor: Option<&'a str>,
}
async fn load_filtered_markets(client: &PolymarketUsClient) -> Result<(), PolymarketUsError> {
let query = MarketsQuery {
category: Some("politics"),
limit: Some(25),
cursor: None,
};
let page = client.markets().list_with_query(&query).await?;
println!("filtered markets: {}", page.markets.len());
Ok(())
}If your account tier requires authenticated access for some filters, use
list_authenticated_with_query(), which takes the same query argument.
The SDK exposes an async WebSocket client via client.streaming(). It supports reconnects,
typed subscription helpers, and dynamic subscribe/unsubscribe while connected.
Connections that go silent are torn down and reconnected after
StreamConnectConfig::idle_timeout (60s by default). This matters because a TCP
connection can die without a FIN or RST — common behind NAT and load balancers —
in which case the socket never reports an error and the stream would otherwise
wait forever. Subscribing to StreamSubscription::heartbeat() guarantees regular
traffic to feed the check. Pass None to disable it:
use std::time::Duration;
let config = StreamConnectConfig::default()
.with_idle_timeout(Some(Duration::from_secs(30)));use polymarket_us::{
PolymarketUsClient, PolymarketUsError, StreamConnectConfig, StreamDataEvent,
StreamMessageKind, StreamSubscription,
};
async fn watch_market(client: &PolymarketUsClient) -> Result<(), PolymarketUsError> {
let stream_client = client.streaming();
let config = StreamConnectConfig::default().with_responses_debounced(true);
let mut stream = stream_client
.connect_with_config(
vec![
StreamSubscription::market_data_lite("BTC-USD"),
StreamSubscription::trades("BTC-USD"),
StreamSubscription::heartbeat(),
],
config,
)
.await?;
// Add/remove subscriptions at runtime.
let dynamic_sub = StreamSubscription::market_data("BTC-USD");
let dynamic_tracking_id = dynamic_sub.tracking_id.clone();
stream.subscribe(dynamic_sub).await?;
stream.unsubscribe(&dynamic_tracking_id).await?;
while let Some(message) = stream.next().await {
match message.kind {
StreamMessageKind::Data(StreamDataEvent::Trade(payload)) => {
println!("trade: {payload}");
}
StreamMessageKind::Data(StreamDataEvent::Heartbeat) => {
println!("heartbeat");
}
_ => {}
}
}
Ok(())
}Supported event families include:
- Market:
market_data,market_data_lite,order_book_delta,trade,heartbeat - Private:
order_snapshot,order_update,position_snapshot,position_update,balance_snapshot,balance_update
Markets (client.markets()):
list()— List all marketslist_with_query(q)— List markets with filters/paginationlist_authenticated()— Authenticated market listinglist_authenticated_with_query(q)— Authenticated with filtersorder_book(symbol)— Get market order bookbbo(symbol)— Get best bid/offersettlement_price(symbol)— Get settlement price
Events (client.events()):
list()— List all eventslist_with_query(q)— List events with filtersretrieve(id)— Get event by IDretrieve_by_slug(slug)— Get event by slug
Orders (client.orders()):
create(req)— Create orderplace(req)— Place order (alternative endpoint)place_batch(req)— Place multiple orders atomicallyopen(q)— Get open ordersretrieve(id)— Get order by IDcancel(id, params)— Cancel ordercancel_trading(id)— Cancel via trading endpointcancel_all(params)— Cancel all ordersmodify(id, req)— Modify open orderpreview(req)— Preview order estimateclose_position(req)— Close position
Account (client.account()):
balances()— Get account balances and buying power
Portfolio (client.portfolio()):
positions()— Get positionsactivities(q)— Get activity with pagination
Search (client.search()):
search(q)— Full-text search across markets/eventsmarkets(q)— Search marketsevents(q)— Search events
Streaming (client.streaming()):
- Typed channels via
SubscriptionChannel - Subscription helpers on
StreamSubscription - Dynamic
subscribe(...)/unsubscribe(...) - Async WebSocket client with automatic reconnect and subscription replay
The flat legacy methods deprecated in 0.3.0 have been removed. Each maps to a resource client:
// Removed in 0.4
let markets = client.markets_list().await?;
let balances = client.account_balances().await?;
let order = client.place_order(&req).await?;
// Use instead
let markets = client.markets().list().await?;
let balances = client.account().balances().await?;
let order = client.orders().place(&req).await?;The general rule: client.<resource>_<verb>() becomes client.<resource>().<verb>().
Three other breaking changes:
UsAuthreturnsPolymarketUsError, notanyhow::Error.UsAuth::from_env()andUsAuth::from_parts()now returnResult<UsAuth, PolymarketUsError>, so credential failures can be matched on like every other SDK error. Bad Base64 or a wrong key length surfaces asPolymarketUsError::InvalidCredentials. If you were using?inside ananyhow::Resultfunction, no change is needed.UsMarket::market_sidesisVec<MarketSide>, previouslyVec<serde_json::Value>. Unmodelled keys are preserved inMarketSide::extra.LeagueandTeamwere removed. They were unreachable placeholders that no endpoint returned; they will return with the endpoints that populate them.
use polymarket_us::{PolymarketUsClient, UsAuth};
use std::time::Duration;
fn build_client(auth: UsAuth) -> Result<PolymarketUsClient, polymarket_us::PolymarketUsError> {
PolymarketUsClient::builder()
.auth(auth)
.gateway_base_url("https://gateway.polymarket.us")
.api_base_url("https://api.polymarket.us")
.timeout(Duration::from_secs(30))
.build()
}use polymarket_us::{PolymarketUsClient, PolymarketUsError};
async fn check_health(client: &PolymarketUsClient) {
match client.health().await {
Ok(h) => println!("ok: {}", h.status),
Err(PolymarketUsError::RateLimited { message, retry_after }) => {
if let Some(d) = retry_after {
eprintln!("rate limited (retry in {}s): {message}", d.as_secs());
} else {
eprintln!("rate limited: {message}");
}
}
Err(PolymarketUsError::Authentication(msg)) => eprintln!("auth failed: {msg}"),
Err(e) => eprintln!("request failed: {e}"),
}
}GET and DELETE requests are automatically retried with exponential backoff and jitter.
POST requests (order creation, placement, etc.) are never retried automatically to
prevent duplicate submissions.
use polymarket_us::{PolymarketUsClient, RetryConfig};
use std::time::Duration;
// Default: 3 retries, 200ms initial backoff, 10s cap, 25% jitter
let client = PolymarketUsClient::builder().build()?;
// Aggressive retry for high-availability workflows
let client = PolymarketUsClient::builder()
.retry(RetryConfig::aggressive())
.build()?;
// Disable retries entirely
let client = PolymarketUsClient::builder()
.retry(RetryConfig::none())
.build()?;
// Fine-grained control
let client = PolymarketUsClient::builder()
.retry(RetryConfig {
max_retries: 5,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(30),
jitter_factor: 0.3,
})
.build()?;Retries occur on:
- HTTP 429 (respects
Retry-Afterheader if present) - HTTP 500, 502, 503, 504
- Transport-level errors (connection refused, timeout)
Every request automatically includes an X-Correlation-ID header (pmrs-{uuid_v4}) for
tracing requests across your logs and Polymarket support conversations.
// Custom prefix — useful to distinguish SDK requests by service/environment
let client = PolymarketUsClient::builder()
.correlation_id_prefix("my-service-prod")
.build()?;
// Sends: X-Correlation-ID: my-service-prod-550e8400-e29b-41d4-a716-446655440000When Polymarket returns a 429, the Retry-After header is parsed and surfaced in the
RateLimited error variant so your application can react precisely:
match client.markets().list().await {
Err(PolymarketUsError::RateLimited { retry_after: Some(d), .. }) => {
println!("backing off for {}s", d.as_secs());
tokio::time::sleep(d).await;
}
_ => {}
}For idempotent endpoints, the SDK already honors this automatically — the Retry-After
duration is used directly instead of the configured backoff.
The SDK includes comprehensive unit tests for all resource clients and type serialization/deserialization:
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test module
cargo test resources::tests
# Run a single test
cargo test resources::tests::place_order_request_serializesCurrent test coverage includes:
- ✅ Resource client creation and type checking (6 resources × 2 tests = 12 tests)
- ✅ Request/Response serialization for all order types (typed enums + wire compatibility)
- ✅ Type deserialization for markets, events, positions, balances
- ✅ Streaming event parsing and subscription helper coverage
- ✅ Retry/backoff policy tests and builder configuration tests
Total: 55 tests, all passing
Initial implementation originated in the DRADIS project and was extracted into this crate.
- Project link:
https://github.com/mbordash/DRADIS - Attribution is kept for provenance and maintenance history.