(
+ token: &IERC20::IERC20Instance,
+ owner: Address,
+ spender: Address,
+) -> anyhow::Result {
+ let allowance = token.allowance(owner, spender).call().await?;
+ Ok(allowance)
+}
+
+async fn check_approval_for_all(
+ ctf: &IERC1155::IERC1155Instance,
+ account: Address,
+ operator: Address,
+) -> anyhow::Result {
+ let approved = ctf.isApprovedForAll(account, operator).call().await?;
+ Ok(approved)
+}
+
+async fn approve(
+ usdc: &IERC20::IERC20Instance,
+ spender: Address,
+ amount: U256,
+) -> anyhow::Result> {
+ let tx_hash = usdc.approve(spender, amount).send().await?.watch().await?;
+ Ok(tx_hash)
+}
+
+async fn set_approval_for_all(
+ ctf: &IERC1155::IERC1155Instance,
+ operator: Address,
+ approved: bool,
+) -> anyhow::Result> {
+ let tx_hash = ctf
+ .setApprovalForAll(operator, approved)
+ .send()
+ .await?
+ .watch()
+ .await?;
+ Ok(tx_hash)
+}
diff --git a/polymarket-client-sdk/examples/bridge.rs b/polymarket-client-sdk/examples/bridge.rs
new file mode 100644
index 0000000..e42fdfb
--- /dev/null
+++ b/polymarket-client-sdk/examples/bridge.rs
@@ -0,0 +1,90 @@
+//! Bridge API example demonstrating deposit and supported assets endpoints.
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example bridge --features bridge,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=bridge.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example bridge --features bridge,tracing
+//! ```
+
+use std::fs::File;
+
+use polymarket_client_sdk::bridge::Client;
+use polymarket_client_sdk::bridge::types::{DepositRequest, StatusRequest};
+use polymarket_client_sdk::types::address;
+use tracing::{debug, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+
+ match client.supported_assets().await {
+ Ok(response) => {
+ info!(
+ endpoint = "supported_assets",
+ count = response.supported_assets.len()
+ );
+ for asset in &response.supported_assets {
+ info!(
+ endpoint = "supported_assets",
+ name = %asset.token.name,
+ symbol = %asset.token.symbol,
+ chain = %asset.chain_name,
+ chain_id = asset.chain_id,
+ min_usd = %asset.min_checkout_usd
+ );
+ }
+ }
+ Err(e) => debug!(endpoint = "supported_assets", error = %e),
+ }
+
+ let request = DepositRequest::builder()
+ .address(address!("56687bf447db6ffa42ffe2204a05edaa20f55839"))
+ .build();
+
+ match client.deposit(&request).await {
+ Ok(response) => {
+ info!(
+ endpoint = "deposit",
+ evm = %response.address.evm,
+ svm = %response.address.svm,
+ btc = %response.address.btc,
+ note = ?response.note
+ );
+ }
+ Err(e) => debug!(endpoint = "deposit", error = %e),
+ }
+
+ let status_request = StatusRequest::builder()
+ .address("bc1qs82vw5pczv9uj44n4npscldkdjgfjqu7x9mlna")
+ .build();
+
+ match client.status(&status_request).await {
+ Ok(response) => {
+ info!(endpoint = "status", count = response.transactions.len());
+ }
+ Err(e) => debug!(endpoint = "status", error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/check_approvals.rs b/polymarket-client-sdk/examples/check_approvals.rs
new file mode 100644
index 0000000..23daecf
--- /dev/null
+++ b/polymarket-client-sdk/examples/check_approvals.rs
@@ -0,0 +1,164 @@
+#![allow(clippy::exhaustive_enums, reason = "Generated by sol! macro")]
+#![allow(clippy::exhaustive_structs, reason = "Generated by sol! macro")]
+#![allow(clippy::print_stderr, reason = "Usage message to stderr")]
+#![allow(clippy::unwrap_used, reason = "Examples use unwrap for brevity")]
+
+//! Read-only example to check current token approvals for Polymarket CLOB trading.
+//!
+//! This example queries the blockchain to show which contracts are approved
+//! for a given wallet address. No private key or gas required.
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example check_approvals --features tracing --
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=check_approvals.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example check_approvals --features tracing --
+//! ```
+//!
+//! Example:
+//! ```sh
+//! RUST_LOG=info cargo run --example check_approvals --features tracing -- 0x1234...abcd
+//! ```
+
+use std::env;
+use std::fs::File;
+
+use alloy::primitives::U256;
+use alloy::providers::ProviderBuilder;
+use alloy::sol;
+use polymarket_client_sdk::types::{Address, address};
+use polymarket_client_sdk::{POLYGON, contract_config};
+use tracing::{debug, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+const RPC_URL: &str = "https://polygon-rpc.com";
+
+const USDC_ADDRESS: Address = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
+
+sol! {
+ #[sol(rpc)]
+ interface IERC20 {
+ function allowance(address owner, address spender) external view returns (uint256);
+ }
+
+ #[sol(rpc)]
+ interface IERC1155 {
+ function isApprovedForAll(address account, address operator) external view returns (bool);
+ }
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let args: Vec = env::args().collect();
+
+ if args.len() != 2 {
+ debug!(
+ args = args.len(),
+ "invalid arguments - expected wallet address"
+ );
+ eprintln!("Usage: cargo run --example check_approvals -- ");
+ eprintln!(
+ "Example: cargo run --example check_approvals -- 0x1234567890abcdef1234567890abcdef12345678"
+ );
+ std::process::exit(1);
+ }
+
+ let wallet_address: Address = args[1].parse()?;
+
+ info!(wallet = %wallet_address, chain = "Polygon Mainnet (137)", "checking approvals");
+
+ let provider = ProviderBuilder::new().connect(RPC_URL).await?;
+
+ let config = contract_config(POLYGON, false).unwrap();
+ let neg_risk_config = contract_config(POLYGON, true).unwrap();
+
+ let usdc = IERC20::new(USDC_ADDRESS, provider.clone());
+ let ctf = IERC1155::new(config.conditional_tokens, provider.clone());
+
+ // All contracts that need approval for full CLOB trading
+ let mut targets: Vec<(&str, Address)> = vec![
+ ("CTF Exchange", config.exchange),
+ ("Neg Risk CTF Exchange", neg_risk_config.exchange),
+ ];
+
+ if let Some(adapter) = neg_risk_config.neg_risk_adapter {
+ targets.push(("Neg Risk Adapter", adapter));
+ }
+
+ let mut all_approved = true;
+
+ for (name, target) in &targets {
+ let usdc_result = usdc.allowance(wallet_address, *target).call().await;
+ let ctf_result = ctf.isApprovedForAll(wallet_address, *target).call().await;
+
+ match (&usdc_result, &ctf_result) {
+ (Ok(usdc_allowance), Ok(ctf_approved)) => {
+ let usdc_ok = *usdc_allowance > U256::ZERO;
+ let ctf_ok = *ctf_approved;
+
+ if !usdc_ok || !ctf_ok {
+ all_approved = false;
+ }
+
+ info!(
+ contract = name,
+ address = %target,
+ usdc_allowance = %format_allowance(*usdc_allowance),
+ usdc_approved = usdc_ok,
+ ctf_approved = ctf_ok,
+ );
+ }
+ (Err(e), _) => {
+ debug!(contract = name, error = %e, "failed to check USDC allowance");
+ all_approved = false;
+ }
+ (_, Err(e)) => {
+ debug!(contract = name, error = %e, "failed to check CTF approval");
+ all_approved = false;
+ }
+ }
+ }
+
+ if all_approved {
+ info!(status = "ready", "all contracts properly approved");
+ } else {
+ info!(
+ status = "incomplete",
+ "some approvals missing - run: cargo run --example approvals"
+ );
+ }
+
+ Ok(())
+}
+
+fn format_allowance(allowance: U256) -> String {
+ if allowance == U256::MAX {
+ "MAX (unlimited)".to_owned()
+ } else if allowance == U256::ZERO {
+ "0".to_owned()
+ } else {
+ // USDC has 6 decimals
+ let usdc_decimals = U256::from(1_000_000);
+ let whole = allowance / usdc_decimals;
+ format!("{whole} USDC")
+ }
+}
diff --git a/polymarket-client-sdk/examples/clob/async.rs b/polymarket-client-sdk/examples/clob/async.rs
new file mode 100644
index 0000000..f7b68ae
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/async.rs
@@ -0,0 +1,151 @@
+//! Demonstrates async concurrency patterns with the CLOB client.
+//!
+//! This example shows how to:
+//! 1. Run multiple unauthenticated API calls concurrently
+//! 2. Run multiple authenticated API calls concurrently
+//! 3. Spawn background tasks that share the client
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example async --features clob,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=async.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example async --features clob,tracing
+//! ```
+//!
+//! For authenticated endpoints, set the `POLY_PRIVATE_KEY` environment variable.
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::types::U256;
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tokio::join;
+use tracing::{error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let (unauthenticated, authenticated) = join!(unauthenticated(), authenticated());
+ unauthenticated?;
+ authenticated
+}
+
+async fn unauthenticated() -> anyhow::Result<()> {
+ let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ let client_clone = client.clone();
+
+ let token_id = U256::from_str(
+ "42334954850219754195241248003172889699504912694714162671145392673031415571339",
+ )?;
+
+ let thread = tokio::spawn(async move {
+ let (ok_result, tick_result, neg_risk_result) = join!(
+ client_clone.ok(),
+ client_clone.tick_size(token_id),
+ client_clone.neg_risk(token_id)
+ );
+
+ match ok_result {
+ Ok(s) => info!(endpoint = "ok", thread = true, result = %s),
+ Err(e) => error!(endpoint = "ok", thread = true, error = %e),
+ }
+
+ match tick_result {
+ Ok(t) => info!(endpoint = "tick_size", thread = true, tick_size = ?t.minimum_tick_size),
+ Err(e) => error!(endpoint = "tick_size", thread = true, error = %e),
+ }
+
+ match neg_risk_result {
+ Ok(n) => info!(endpoint = "neg_risk", thread = true, neg_risk = n.neg_risk),
+ Err(e) => error!(endpoint = "neg_risk", thread = true, error = %e),
+ }
+
+ anyhow::Ok(())
+ });
+
+ match client.ok().await {
+ Ok(s) => info!(endpoint = "ok", result = %s),
+ Err(e) => error!(endpoint = "ok", error = %e),
+ }
+
+ match client.tick_size(token_id).await {
+ Ok(t) => {
+ info!(endpoint = "tick_size", token_id = %token_id, tick_size = ?t.minimum_tick_size);
+ }
+ Err(e) => error!(endpoint = "tick_size", token_id = %token_id, error = %e),
+ }
+
+ match client.neg_risk(token_id).await {
+ Ok(n) => info!(endpoint = "neg_risk", token_id = %token_id, neg_risk = n.neg_risk),
+ Err(e) => error!(endpoint = "neg_risk", token_id = %token_id, error = %e),
+ }
+
+ thread.await?
+}
+
+async fn authenticated() -> anyhow::Result<()> {
+ let Ok(private_key) = std::env::var(PRIVATE_KEY_VAR) else {
+ info!(
+ endpoint = "authenticated",
+ "skipped - POLY_PRIVATE_KEY not set"
+ );
+ return Ok(());
+ };
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+ let client_clone = client.clone();
+
+ let thread = tokio::spawn(async move {
+ let (ok_result, api_keys_result) = join!(client_clone.ok(), client_clone.api_keys());
+
+ match ok_result {
+ Ok(s) => info!(endpoint = "ok", thread = true, authenticated = true, result = %s),
+ Err(e) => error!(endpoint = "ok", thread = true, authenticated = true, error = %e),
+ }
+
+ match api_keys_result {
+ Ok(keys) => info!(endpoint = "api_keys", thread = true, result = ?keys),
+ Err(e) => error!(endpoint = "api_keys", thread = true, error = %e),
+ }
+
+ anyhow::Ok(())
+ });
+
+ match client.ok().await {
+ Ok(s) => info!(endpoint = "ok", authenticated = true, result = %s),
+ Err(e) => error!(endpoint = "ok", authenticated = true, error = %e),
+ }
+
+ match client.api_keys().await {
+ Ok(keys) => info!(endpoint = "api_keys", result = ?keys),
+ Err(e) => error!(endpoint = "api_keys", error = %e),
+ }
+
+ thread.await?
+}
diff --git a/polymarket-client-sdk/examples/clob/authenticated.rs b/polymarket-client-sdk/examples/clob/authenticated.rs
new file mode 100644
index 0000000..d79396a
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/authenticated.rs
@@ -0,0 +1,216 @@
+//! Comprehensive authenticated CLOB API endpoint explorer.
+//!
+//! This example tests authenticated CLOB API endpoints including:
+//! 1. API key management and account status
+//! 2. Market and limit order creation
+//! 3. Order management (fetch, cancel)
+//! 4. Balance and allowance operations
+//! 5. Trades and rewards queries
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example authenticated --features clob,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=authenticated.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example authenticated --features clob,tracing
+//! ```
+//!
+//! Requires `POLY_PRIVATE_KEY` environment variable to be set.
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use chrono::{TimeDelta, Utc};
+use polymarket_client_sdk::clob::types::request::{
+ BalanceAllowanceRequest, OrdersRequest, TradesRequest, UpdateBalanceAllowanceRequest,
+ UserRewardsEarningRequest,
+};
+use polymarket_client_sdk::clob::types::{Amount, OrderType, Side};
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::types::{Decimal, U256};
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use rust_decimal_macros::dec;
+use tracing::{error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let token_id = U256::from_str(
+ "15871154585880608648532107628464183779895785213830018178010423617714102767076",
+ )?;
+
+ let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need POLY_PRIVATE_KEY");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let config = Config::builder().use_server_time(true).build();
+ let client = Client::new("https://clob.polymarket.com", config)?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ match client.api_keys().await {
+ Ok(keys) => info!(endpoint = "api_keys", result = ?keys),
+ Err(e) => error!(endpoint = "api_keys", error = %e),
+ }
+
+ match client.closed_only_mode().await {
+ Ok(status) => info!(
+ endpoint = "closed_only_mode",
+ closed_only = status.closed_only
+ ),
+ Err(e) => error!(endpoint = "closed_only_mode", error = %e),
+ }
+
+ // Market order
+ let market_order = client
+ .market_order()
+ .token_id(token_id)
+ .amount(Amount::usdc(Decimal::ONE_HUNDRED)?)
+ .side(Side::Buy)
+ .build()
+ .await?;
+ let signed_order = client.sign(&signer, market_order).await?;
+ match client.post_order(signed_order).await {
+ Ok(r) => {
+ info!(endpoint = "post_order", order_type = "market", order_id = %r.order_id, success = r.success);
+ }
+ Err(e) => error!(endpoint = "post_order", order_type = "market", error = %e),
+ }
+
+ // Limit order
+ let limit_order = client
+ .limit_order()
+ .token_id(token_id)
+ .order_type(OrderType::GTD)
+ .expiration(Utc::now() + TimeDelta::days(2))
+ .price(dec!(0.5))
+ .size(Decimal::ONE_HUNDRED)
+ .side(Side::Buy)
+ .build()
+ .await?;
+ let signed_order = client.sign(&signer, limit_order).await?;
+ match client.post_order(signed_order).await {
+ Ok(r) => {
+ info!(endpoint = "post_order", order_type = "limit", order_id = %r.order_id, success = r.success);
+ }
+ Err(e) => error!(endpoint = "post_order", order_type = "limit", error = %e),
+ }
+
+ match client.notifications().await {
+ Ok(n) => info!(endpoint = "notifications", count = n.len()),
+ Err(e) => error!(endpoint = "notifications", error = %e),
+ }
+
+ match client
+ .balance_allowance(BalanceAllowanceRequest::default())
+ .await
+ {
+ Ok(b) => info!(endpoint = "balance_allowance", result = ?b),
+ Err(e) => error!(endpoint = "balance_allowance", error = %e),
+ }
+
+ match client
+ .update_balance_allowance(UpdateBalanceAllowanceRequest::default())
+ .await
+ {
+ Ok(b) => info!(endpoint = "update_balance_allowance", result = ?b),
+ Err(e) => error!(endpoint = "update_balance_allowance", error = %e),
+ }
+
+ let order_id = "0xa1449ec0831c7d62f887c4653d0917f2445783ff30f0ca713d99c667fef17f2c";
+ match client.order(order_id).await {
+ Ok(o) => info!(endpoint = "order", order_id = %order_id, status = ?o.status),
+ Err(e) => error!(endpoint = "order", order_id = %order_id, error = %e),
+ }
+
+ match client.orders(&OrdersRequest::default(), None).await {
+ Ok(orders) => info!(endpoint = "orders", count = orders.data.len()),
+ Err(e) => error!(endpoint = "orders", error = %e),
+ }
+
+ match client.cancel_order(order_id).await {
+ Ok(r) => info!(endpoint = "cancel_order", order_id = %order_id, result = ?r),
+ Err(e) => error!(endpoint = "cancel_order", order_id = %order_id, error = %e),
+ }
+
+ match client.cancel_orders(&[order_id]).await {
+ Ok(r) => info!(endpoint = "cancel_orders", result = ?r),
+ Err(e) => error!(endpoint = "cancel_orders", error = %e),
+ }
+
+ match client.cancel_all_orders().await {
+ Ok(r) => info!(endpoint = "cancel_all_orders", result = ?r),
+ Err(e) => error!(endpoint = "cancel_all_orders", error = %e),
+ }
+
+ match client.orders(&OrdersRequest::default(), None).await {
+ Ok(orders) => info!(
+ endpoint = "orders",
+ after_cancel = true,
+ count = orders.data.len()
+ ),
+ Err(e) => error!(endpoint = "orders", after_cancel = true, error = %e),
+ }
+
+ match client.trades(&TradesRequest::default(), None).await {
+ Ok(trades) => info!(endpoint = "trades", count = trades.data.len()),
+ Err(e) => error!(endpoint = "trades", error = %e),
+ }
+
+ match client
+ .earnings_for_user_for_day(Utc::now().date_naive(), None)
+ .await
+ {
+ Ok(e) => info!(endpoint = "earnings_for_user_for_day", result = ?e),
+ Err(e) => error!(endpoint = "earnings_for_user_for_day", error = %e),
+ }
+
+ let request = UserRewardsEarningRequest::builder()
+ .date(Utc::now().date_naive() - TimeDelta::days(30))
+ .build();
+ match client
+ .user_earnings_and_markets_config(&request, None)
+ .await
+ {
+ Ok(e) => info!(endpoint = "user_earnings_and_markets_config", result = ?e),
+ Err(e) => error!(endpoint = "user_earnings_and_markets_config", error = %e),
+ }
+
+ match client.reward_percentages().await {
+ Ok(r) => info!(endpoint = "reward_percentages", result = ?r),
+ Err(e) => error!(endpoint = "reward_percentages", error = %e),
+ }
+
+ match client.current_rewards(None).await {
+ Ok(r) => info!(endpoint = "current_rewards", result = ?r),
+ Err(e) => error!(endpoint = "current_rewards", error = %e),
+ }
+
+ let market_id = "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1";
+ match client.raw_rewards_for_market(market_id, None).await {
+ Ok(r) => info!(endpoint = "raw_rewards_for_market", market_id = %market_id, result = ?r),
+ Err(e) => error!(endpoint = "raw_rewards_for_market", market_id = %market_id, error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/aws_authenticated.rs b/polymarket-client-sdk/examples/clob/aws_authenticated.rs
new file mode 100644
index 0000000..47ee92e
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/aws_authenticated.rs
@@ -0,0 +1,69 @@
+//! Demonstrates AWS KMS-based authentication with the CLOB client.
+//!
+//! This example shows how to:
+//! 1. Configure AWS SDK and KMS client
+//! 2. Create an `AwsSigner` using a KMS key
+//! 3. Authenticate with the CLOB API using the AWS signer
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example aws_authenticated --features clob,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=aws_authenticated.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example aws_authenticated --features clob,tracing
+//! ```
+//!
+//! Requires AWS credentials configured and a valid KMS key ID.
+
+use std::fs::File;
+
+use alloy::signers::Signer as _;
+use alloy::signers::aws::AwsSigner;
+use aws_config::BehaviorVersion;
+use polymarket_client_sdk::POLYGON;
+use polymarket_client_sdk::clob::{Client, Config};
+use tracing::{error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
+ let kms_client = aws_sdk_kms::Client::new(&config);
+
+ let key_id = "".to_owned();
+ info!(endpoint = "aws_signer", key_id = %key_id, "creating AWS KMS signer");
+
+ let alloy_signer = AwsSigner::new(kms_client, key_id, Some(POLYGON))
+ .await?
+ .with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&alloy_signer)
+ .authenticate()
+ .await?;
+
+ match client.api_keys().await {
+ Ok(keys) => info!(endpoint = "api_keys", result = ?keys),
+ Err(e) => error!(endpoint = "api_keys", error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/builder_authenticated.rs b/polymarket-client-sdk/examples/clob/builder_authenticated.rs
new file mode 100644
index 0000000..8a695a6
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/builder_authenticated.rs
@@ -0,0 +1,92 @@
+//! Demonstrates builder API authentication with the CLOB client.
+//!
+//! This example shows how to:
+//! 1. Authenticate as a regular user
+//! 2. Create builder API credentials
+//! 3. Promote the client to a builder client
+//! 4. Access builder-specific endpoints
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example builder_authenticated --features clob,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=builder_authenticated.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example builder_authenticated --features clob,tracing
+//! ```
+//!
+//! Requires `POLY_PRIVATE_KEY` environment variable to be set.
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use polymarket_client_sdk::auth::builder::Config as BuilderConfig;
+use polymarket_client_sdk::clob::types::request::TradesRequest;
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::types::U256;
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tracing::{error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need POLY_PRIVATE_KEY");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ // Create builder credentials and promote to builder client
+ let builder_credentials = client.create_builder_api_key().await?;
+ info!(
+ endpoint = "create_builder_api_key",
+ "created builder credentials"
+ );
+
+ let config = BuilderConfig::local(builder_credentials);
+ let client = client.promote_to_builder(config).await?;
+ info!(
+ endpoint = "promote_to_builder",
+ "promoted to builder client"
+ );
+
+ match client.builder_api_keys().await {
+ Ok(keys) => info!(endpoint = "builder_api_keys", count = keys.len()),
+ Err(e) => error!(endpoint = "builder_api_keys", error = %e),
+ }
+
+ let token_id = U256::from_str(
+ "15871154585880608648532107628464183779895785213830018178010423617714102767076",
+ )?;
+ let request = TradesRequest::builder().asset_id(token_id).build();
+
+ match client.builder_trades(&request, None).await {
+ Ok(trades) => {
+ info!(endpoint = "builder_trades", token_id = %token_id, count = trades.data.len());
+ }
+ Err(e) => error!(endpoint = "builder_trades", token_id = %token_id, error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/heartbeats.rs b/polymarket-client-sdk/examples/clob/heartbeats.rs
new file mode 100644
index 0000000..6fd1e88
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/heartbeats.rs
@@ -0,0 +1,38 @@
+//! Shows how heartbeats are sent automatically when the corresponding feature flag is enabled.
+//!
+//! Run with:
+//! ```sh
+//! RUST_LOG=debug,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example heartbeats --features heartbeats,tracing
+//! ```
+//!
+use std::str::FromStr as _;
+use std::time::Duration;
+
+use polymarket_client_sdk::auth::{LocalSigner, Signer as _};
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt::init();
+
+ let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need a private key");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let config = Config::builder()
+ .use_server_time(true)
+ .heartbeat_interval(Duration::from_secs(1))
+ .build();
+ let client = Client::new("https://clob.polymarket.com", config)?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ tokio::time::sleep(Duration::from_secs(5)).await;
+
+ drop(client);
+
+ tokio::time::sleep(Duration::from_secs(2)).await;
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/rfq/quotes.rs b/polymarket-client-sdk/examples/clob/rfq/quotes.rs
new file mode 100644
index 0000000..1ed601a
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/rfq/quotes.rs
@@ -0,0 +1,83 @@
+//! Demonstrates fetching RFQ quotes from the CLOB API.
+//!
+//! This example shows how to:
+//! 1. Authenticate with the CLOB API
+//! 2. Build an RFQ quotes request with filters
+//! 3. Fetch and display paginated quote results
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example rfq_quotes --features clob,rfq,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=rfq_quotes.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example rfq_quotes --features clob,rfq,tracing
+//! ```
+//!
+//! Requires `POLY_PRIVATE_KEY` environment variable to be set.
+
+#![cfg(feature = "rfq")]
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use polymarket_client_sdk::clob::types::{RfqQuotesRequest, RfqSortBy, RfqSortDir, RfqState};
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tracing::{debug, error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need POLY_PRIVATE_KEY");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ let request = RfqQuotesRequest::builder()
+ .state(RfqState::Active)
+ .limit(10)
+ .offset("MA==")
+ .sort_by(RfqSortBy::Price)
+ .sort_dir(RfqSortDir::Asc)
+ .build();
+
+ match client.quotes(&request, None).await {
+ Ok(quotes) => {
+ info!(
+ endpoint = "quotes",
+ count = quotes.count,
+ data_len = quotes.data.len(),
+ next_cursor = %quotes.next_cursor
+ );
+ for quote in "es.data {
+ debug!(endpoint = "quotes", quote = ?quote);
+ }
+ }
+ Err(e) => error!(endpoint = "quotes", error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/rfq/requests.rs b/polymarket-client-sdk/examples/clob/rfq/requests.rs
new file mode 100644
index 0000000..99ee329
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/rfq/requests.rs
@@ -0,0 +1,83 @@
+//! Demonstrates fetching RFQ requests from the CLOB API.
+//!
+//! This example shows how to:
+//! 1. Authenticate with the CLOB API
+//! 2. Build an RFQ requests query with filters
+//! 3. Fetch and display paginated request results
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example rfq_requests --features clob,rfq,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=rfq_requests.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example rfq_requests --features clob,rfq,tracing
+//! ```
+//!
+//! Requires `POLY_PRIVATE_KEY` environment variable to be set.
+
+#![cfg(feature = "rfq")]
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use polymarket_client_sdk::clob::types::{RfqRequestsRequest, RfqSortBy, RfqSortDir, RfqState};
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tracing::{debug, error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need POLY_PRIVATE_KEY");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ let request = RfqRequestsRequest::builder()
+ .state(RfqState::Active)
+ .limit(10)
+ .offset("MA==")
+ .sort_by(RfqSortBy::Created)
+ .sort_dir(RfqSortDir::Desc)
+ .build();
+
+ match client.requests(&request, None).await {
+ Ok(requests) => {
+ info!(
+ endpoint = "requests",
+ count = requests.count,
+ data_len = requests.data.len(),
+ next_cursor = %requests.next_cursor
+ );
+ for req in &requests.data {
+ debug!(endpoint = "requests", request = ?req);
+ }
+ }
+ Err(e) => error!(endpoint = "requests", error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/streaming.rs b/polymarket-client-sdk/examples/clob/streaming.rs
new file mode 100644
index 0000000..ee5dc23
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/streaming.rs
@@ -0,0 +1,157 @@
+//! CLOB API streaming endpoint explorer.
+//!
+//! This example demonstrates streaming data from CLOB API endpoints by:
+//! 1. Streaming `sampling_markets` (unauthenticated) to discover market data
+//! 2. Streaming trades (authenticated) if credentials are available
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example streaming --features tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=streaming.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example streaming --features tracing
+//! ```
+//!
+//! For authenticated streaming, set the `POLY_PRIVATE_KEY` environment variable:
+//! ```sh
+//! POLY_PRIVATE_KEY=0x... RUST_LOG=info cargo run --example streaming --features tracing
+//! ```
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use futures::{StreamExt as _, future};
+use polymarket_client_sdk::clob::types::request::TradesRequest;
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tokio::join;
+use tracing::{debug, info, warn};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let (unauthenticated, authenticated) = join!(unauthenticated(), authenticated());
+ unauthenticated?;
+ authenticated
+}
+
+async fn unauthenticated() -> anyhow::Result<()> {
+ let client = Client::new("https://clob.polymarket.com", Config::default())?;
+
+ info!(
+ stream = "sampling_markets",
+ "starting unauthenticated stream"
+ );
+
+ let mut stream = client
+ .stream_data(Client::sampling_markets)
+ .filter_map(|d| future::ready(d.ok()))
+ .boxed();
+
+ let mut count = 0_u32;
+
+ while let Some(market) = stream.next().await {
+ count += 1;
+
+ // Log every 100th market to avoid flooding logs
+ if count % 100 == 1 {
+ if let Some(cid) = &market.condition_id {
+ info!(
+ stream = "sampling_markets",
+ count = count,
+ condition_id = %cid,
+ question = %market.question,
+ active = market.active
+ );
+ } else {
+ info!(
+ stream = "sampling_markets",
+ count = count,
+ question = %market.question,
+ active = market.active
+ );
+ }
+ }
+ }
+
+ info!(
+ stream = "sampling_markets",
+ total_markets = count,
+ "stream completed"
+ );
+
+ Ok(())
+}
+
+async fn authenticated() -> anyhow::Result<()> {
+ let Ok(private_key) = std::env::var(PRIVATE_KEY_VAR) else {
+ warn!(
+ stream = "trades",
+ "skipping authenticated stream - {} not set", PRIVATE_KEY_VAR
+ );
+ return Ok(());
+ };
+
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?
+ .authentication_builder(&signer)
+ .authenticate()
+ .await?;
+
+ info!(stream = "trades", "starting authenticated stream");
+
+ let request = TradesRequest::builder().build();
+ let mut stream = client
+ .stream_data(|c, cursor| c.trades(&request, cursor))
+ .boxed();
+
+ let mut count = 0_u32;
+
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok(trade) => {
+ count += 1;
+
+ // Log every 100th trade to avoid flooding logs
+ if count % 100 == 1 {
+ info!(
+ stream = "trades",
+ count = count,
+ market = %trade.market,
+ side = ?trade.side,
+ size = %trade.size,
+ price = %trade.price
+ );
+ }
+ }
+ Err(e) => {
+ debug!(stream = "trades", error = %e, "stream error");
+ }
+ }
+ }
+
+ info!(stream = "trades", total_trades = count, "stream completed");
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/unauthenticated.rs b/polymarket-client-sdk/examples/clob/unauthenticated.rs
new file mode 100644
index 0000000..3d78f9d
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/unauthenticated.rs
@@ -0,0 +1,306 @@
+//! Comprehensive CLOB API endpoint explorer (unauthenticated).
+//!
+//! This example dynamically tests all unauthenticated CLOB API endpoints by:
+//! 1. Fetching markets to discover real token IDs and condition IDs
+//! 2. Using those IDs for subsequent price, orderbook, and trade queries
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example unauthenticated --features tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=clob.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example unauthenticated --features tracing
+//! ```
+
+use std::collections::HashMap;
+use std::fs::File;
+
+use futures_util::StreamExt as _;
+use polymarket_client_sdk::clob::types::Side;
+use polymarket_client_sdk::clob::types::request::{
+ LastTradePriceRequest, MidpointRequest, OrderBookSummaryRequest, PriceRequest, SpreadRequest,
+};
+use polymarket_client_sdk::clob::{Client, Config};
+use polymarket_client_sdk::types::{B256, Decimal, U256};
+use tracing::{error, info, warn};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+/// Finds a market with an active orderbook by streaming through all markets.
+///
+/// Returns a tuple of (`token_id`, `condition_id`) from a market that:
+/// - Has orderbook enabled (`enable_order_book` = true)
+/// - Is active and not closed
+/// - Is accepting orders
+/// - Has tokens with non-zero prices
+///
+/// This ensures subsequent price/midpoint/orderbook API calls will succeed.
+async fn find_market_with_orderbook(client: &Client) -> anyhow::Result<(U256, B256)> {
+ info!("Searching for a market with an active orderbook...");
+
+ let mut stream = Box::pin(client.stream_data(Client::markets));
+
+ while let Some(maybe_market) = stream.next().await {
+ match maybe_market {
+ Ok(market) => {
+ if market.enable_order_book
+ && market.active
+ && !market.closed
+ && !market.archived
+ && market.accepting_orders
+ && !market.tokens.is_empty()
+ && market.tokens.iter().any(|t| t.price > Decimal::ZERO)
+ {
+ let condition_id = market
+ .condition_id
+ .ok_or_else(|| anyhow::anyhow!("Market missing condition_id"))?;
+ let token_id = market
+ .tokens
+ .first()
+ .map(|t| t.token_id)
+ .ok_or_else(|| anyhow::anyhow!("Market has no tokens"))?;
+
+ let request = MidpointRequest::builder().token_id(token_id).build();
+ if client.midpoint(&request).await.is_ok() {
+ info!(
+ condition_id = %condition_id,
+ token_id = %token_id,
+ question = %market.question,
+ "Found market with active orderbook"
+ );
+
+ return Ok((token_id, condition_id));
+ }
+ }
+ }
+ Err(e) => {
+ error!(error = ?e, "Error fetching market");
+ }
+ }
+ }
+
+ Err(anyhow::anyhow!(
+ "No active markets with orderbooks found after searching all markets"
+ ))
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::new("https://clob.polymarket.com", Config::default())?;
+
+ // Health check endpoints
+ match client.ok().await {
+ Ok(_) => info!(endpoint = "ok", status = "healthy"),
+ Err(e) => error!(endpoint = "ok", error = %e),
+ }
+
+ match client.server_time().await {
+ Ok(time) => info!(endpoint = "server_time", timestamp = %time),
+ Err(e) => error!(endpoint = "server_time", error = %e),
+ }
+
+ let (token_id, condition_id) = match find_market_with_orderbook(&client).await {
+ Ok((tid, cid)) => (Some(tid), Some(cid)),
+ Err(e) => {
+ error!("Failed to find market with orderbook: {}", e);
+ (None, None)
+ }
+ };
+
+ if let Some(cid) = &condition_id {
+ match client.market(&cid.to_string()).await {
+ Ok(market) => info!(
+ endpoint = "market",
+ condition_id = %cid,
+ question = %market.question,
+ active = market.active
+ ),
+ Err(e) => error!(endpoint = "market", condition_id = %cid, error = %e),
+ }
+ }
+
+ match client.sampling_markets(None).await {
+ Ok(page) => info!(
+ endpoint = "sampling_markets",
+ count = page.data.len(),
+ has_next = !page.next_cursor.is_empty()
+ ),
+ Err(e) => error!(endpoint = "sampling_markets", error = %e),
+ }
+
+ match client.simplified_markets(None).await {
+ Ok(page) => info!(
+ endpoint = "simplified_markets",
+ count = page.data.len(),
+ has_next = !page.next_cursor.is_empty()
+ ),
+ Err(e) => error!(endpoint = "simplified_markets", error = %e),
+ }
+
+ match client.sampling_simplified_markets(None).await {
+ Ok(page) => info!(
+ endpoint = "sampling_simplified_markets",
+ count = page.data.len(),
+ has_next = !page.next_cursor.is_empty()
+ ),
+ Err(e) => error!(endpoint = "sampling_simplified_markets", error = %e),
+ }
+
+ if let Some(token_id) = token_id {
+ let midpoint_request = MidpointRequest::builder().token_id(token_id).build();
+ match client.midpoint(&midpoint_request).await {
+ Ok(midpoint) => info!(endpoint = "midpoint", token_id = %token_id, mid = %midpoint.mid),
+ Err(e) => error!(endpoint = "midpoint", token_id = %token_id, error = %e),
+ }
+
+ match client.midpoints(&[midpoint_request]).await {
+ Ok(midpoints) => info!(endpoint = "midpoints", count = midpoints.midpoints.len()),
+ Err(e) => error!(endpoint = "midpoints", error = %e),
+ }
+
+ let buy_price_request = PriceRequest::builder()
+ .token_id(token_id)
+ .side(Side::Buy)
+ .build();
+ match client.price(&buy_price_request).await {
+ Ok(price) => info!(
+ endpoint = "price",
+ token_id = %token_id,
+ side = "buy",
+ price = %price.price
+ ),
+ Err(e) => error!(endpoint = "price", token_id = %token_id, side = "buy", error = %e),
+ }
+
+ let sell_price_request = PriceRequest::builder()
+ .token_id(token_id)
+ .side(Side::Sell)
+ .build();
+ match client.price(&sell_price_request).await {
+ Ok(price) => info!(
+ endpoint = "price",
+ token_id = %token_id,
+ side = "sell",
+ price = %price.price
+ ),
+ Err(e) => error!(endpoint = "price", token_id = %token_id, side = "sell", error = %e),
+ }
+
+ match client
+ .prices(&[buy_price_request, sell_price_request])
+ .await
+ {
+ Ok(prices) => info!(
+ endpoint = "prices",
+ count = prices.prices.as_ref().map_or(0, HashMap::len)
+ ),
+ Err(e) => error!(endpoint = "prices", error = %e),
+ }
+
+ let spread_request = SpreadRequest::builder().token_id(token_id).build();
+ match client.spread(&spread_request).await {
+ Ok(spread) => info!(
+ endpoint = "spread",
+ token_id = %token_id,
+ spread = %spread.spread
+ ),
+ Err(e) => error!(endpoint = "spread", token_id = %token_id, error = %e),
+ }
+
+ match client.spreads(&[spread_request]).await {
+ Ok(spreads) => info!(
+ endpoint = "spreads",
+ count = spreads.spreads.as_ref().map_or(0, HashMap::len)
+ ),
+ Err(e) => error!(endpoint = "spreads", error = %e),
+ }
+
+ match client.tick_size(token_id).await {
+ Ok(tick_size) => info!(
+ endpoint = "tick_size",
+ token_id = %token_id,
+ tick_size = %tick_size.minimum_tick_size
+ ),
+ Err(e) => error!(endpoint = "tick_size", token_id = %token_id, error = %e),
+ }
+
+ match client.neg_risk(token_id).await {
+ Ok(neg_risk) => info!(
+ endpoint = "neg_risk",
+ token_id = %token_id,
+ neg_risk = neg_risk.neg_risk
+ ),
+ Err(e) => error!(endpoint = "neg_risk", token_id = %token_id, error = %e),
+ }
+
+ match client.fee_rate_bps(token_id).await {
+ Ok(fee_rate) => info!(
+ endpoint = "fee_rate_bps",
+ token_id = %token_id,
+ base_fee = fee_rate.base_fee
+ ),
+ Err(e) => error!(endpoint = "fee_rate_bps", token_id = %token_id, error = %e),
+ }
+
+ let order_book_request = OrderBookSummaryRequest::builder()
+ .token_id(token_id)
+ .build();
+ match client.order_book(&order_book_request).await {
+ Ok(book) => {
+ let hash = book.hash().unwrap_or_default();
+ info!(
+ endpoint = "order_book",
+ token_id = %token_id,
+ bids = book.bids.len(),
+ asks = book.asks.len(),
+ hash = %hash
+ );
+ }
+ Err(e) => error!(endpoint = "order_book", token_id = %token_id, error = %e),
+ }
+
+ match client.order_books(&[order_book_request]).await {
+ Ok(books) => info!(endpoint = "order_books", count = books.len()),
+ Err(e) => error!(endpoint = "order_books", error = %e),
+ }
+
+ let last_trade_request = LastTradePriceRequest::builder().token_id(token_id).build();
+ match client.last_trade_price(&last_trade_request).await {
+ Ok(last_trade) => info!(
+ endpoint = "last_trade_price",
+ token_id = %token_id,
+ price = %last_trade.price
+ ),
+ Err(e) => error!(endpoint = "last_trade_price", token_id = %token_id, error = %e),
+ }
+
+ match client.last_trades_prices(&[last_trade_request]).await {
+ Ok(prices) => info!(endpoint = "last_trade_prices", count = prices.len()),
+ Err(e) => error!(endpoint = "last_trade_prices", error = %e),
+ }
+ } else {
+ warn!(
+ endpoint = "price_queries",
+ "skipped - no token_id discovered"
+ );
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/ws/orderbook.rs b/polymarket-client-sdk/examples/clob/ws/orderbook.rs
new file mode 100644
index 0000000..1e07ba2
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/ws/orderbook.rs
@@ -0,0 +1,106 @@
+//! Demonstrates subscribing to real-time orderbook updates via WebSocket.
+//!
+//! This example shows how to:
+//! 1. Connect to the CLOB WebSocket API
+//! 2. Subscribe to orderbook updates for multiple assets
+//! 3. Process and display bid/ask updates in real-time
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_orderbook --features ws,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=websocket_orderbook.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_orderbook --features ws,tracing
+//! ```
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use futures::StreamExt as _;
+use polymarket_client_sdk::clob::ws::Client;
+use polymarket_client_sdk::types::U256;
+use tracing::{debug, error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+ info!(endpoint = "websocket", "connected to CLOB WebSocket API");
+
+ let asset_ids = vec![
+ U256::from_str(
+ "92703761682322480664976766247614127878023988651992837287050266308961660624165",
+ )?,
+ U256::from_str(
+ "34551606549875928972193520396544368029176529083448203019529657908155427866742",
+ )?,
+ ];
+
+ let stream = client.subscribe_orderbook(asset_ids.clone())?;
+ let mut stream = Box::pin(stream);
+ info!(
+ endpoint = "subscribe_orderbook",
+ asset_count = asset_ids.len(),
+ "subscribed to orderbook updates"
+ );
+
+ while let Some(book_result) = stream.next().await {
+ match book_result {
+ Ok(book) => {
+ info!(
+ endpoint = "orderbook",
+ asset_id = %book.asset_id,
+ market = %book.market,
+ timestamp = %book.timestamp,
+ bids = book.bids.len(),
+ asks = book.asks.len()
+ );
+
+ for (i, bid) in book.bids.iter().take(5).enumerate() {
+ debug!(
+ endpoint = "orderbook",
+ side = "bid",
+ rank = i + 1,
+ size = %bid.size,
+ price = %bid.price
+ );
+ }
+
+ for (i, ask) in book.asks.iter().take(5).enumerate() {
+ debug!(
+ endpoint = "orderbook",
+ side = "ask",
+ rank = i + 1,
+ size = %ask.size,
+ price = %ask.price
+ );
+ }
+
+ if let Some(hash) = &book.hash {
+ debug!(endpoint = "orderbook", hash = %hash);
+ }
+ }
+ Err(e) => error!(endpoint = "orderbook", error = %e),
+ }
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/ws/unsubscribe.rs b/polymarket-client-sdk/examples/clob/ws/unsubscribe.rs
new file mode 100644
index 0000000..f0e3bb4
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/ws/unsubscribe.rs
@@ -0,0 +1,168 @@
+//! Demonstrates WebSocket subscribe/unsubscribe and multiplexing behavior.
+//!
+//! This example shows how to:
+//! 1. Subscribe multiple streams to the same asset (multiplexing)
+//! 2. Unsubscribe streams while others remain active
+//! 3. Verify reference counting works correctly
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_unsubscribe --features ws,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=websocket_unsubscribe.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_unsubscribe --features ws,tracing
+//! ```
+//!
+//! With debug level, you can see subscribe/unsubscribe wire messages:
+//! ```sh
+//! RUST_LOG=debug,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_unsubscribe --features ws,tracing
+//! ```
+
+use std::fs::File;
+use std::str::FromStr as _;
+use std::time::Duration;
+
+use futures::StreamExt as _;
+use polymarket_client_sdk::clob::ws::Client;
+use polymarket_client_sdk::types::U256;
+use tokio::time::timeout;
+use tracing::{debug, error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+ info!(endpoint = "websocket", "connected to CLOB WebSocket API");
+
+ let asset_ids = vec![U256::from_str(
+ "92703761682322480664976766247614127878023988651992837287050266308961660624165",
+ )?];
+
+ // === FIRST SUBSCRIPTION ===
+ info!(
+ step = 1,
+ "first subscription - should send 'subscribe' to server"
+ );
+ let stream1 = client.subscribe_orderbook(asset_ids.clone())?;
+ let mut stream1 = Box::pin(stream1);
+
+ match timeout(Duration::from_secs(10), stream1.next()).await {
+ Ok(Some(Ok(book))) => {
+ info!(
+ step = 1,
+ endpoint = "orderbook",
+ bids = book.bids.len(),
+ asks = book.asks.len(),
+ "received update on stream1"
+ );
+ }
+ Ok(Some(Err(e))) => error!(step = 1, error = %e),
+ Ok(None) => error!(step = 1, "stream ended"),
+ Err(_) => error!(step = 1, "timeout"),
+ }
+
+ // === SECOND SUBSCRIPTION (same asset - should multiplex) ===
+ info!(
+ step = 2,
+ "second subscription (same asset) - should NOT send message (multiplexing)"
+ );
+ let stream2 = client.subscribe_orderbook(asset_ids.clone())?;
+ let mut stream2 = Box::pin(stream2);
+
+ match timeout(Duration::from_secs(10), stream2.next()).await {
+ Ok(Some(Ok(book))) => {
+ info!(
+ step = 2,
+ endpoint = "orderbook",
+ bids = book.bids.len(),
+ asks = book.asks.len(),
+ "received update on stream2"
+ );
+ }
+ Ok(Some(Err(e))) => error!(step = 2, error = %e),
+ Ok(None) => error!(step = 2, "stream ended"),
+ Err(_) => error!(step = 2, "timeout"),
+ }
+
+ // === FIRST UNSUBSCRIBE ===
+ info!(
+ step = 3,
+ "first unsubscribe - should NOT send message (refcount still 1)"
+ );
+ client.unsubscribe_orderbook(&asset_ids)?;
+ drop(stream1);
+ info!(step = 3, "stream1 unsubscribed and dropped");
+
+ // stream2 should still work
+ match timeout(Duration::from_secs(10), stream2.next()).await {
+ Ok(Some(Ok(book))) => {
+ info!(
+ step = 3,
+ endpoint = "orderbook",
+ bids = book.bids.len(),
+ asks = book.asks.len(),
+ "stream2 still receiving updates"
+ );
+ }
+ Ok(Some(Err(e))) => error!(step = 3, error = %e),
+ Ok(None) => error!(step = 3, "stream ended"),
+ Err(_) => error!(step = 3, "timeout"),
+ }
+
+ // === SECOND UNSUBSCRIBE ===
+ info!(
+ step = 4,
+ "second unsubscribe - should send 'unsubscribe' (refcount now 0)"
+ );
+ client.unsubscribe_orderbook(&asset_ids)?;
+ drop(stream2);
+ info!(step = 4, "stream2 unsubscribed and dropped");
+
+ // === RE-SUBSCRIBE (proves unsubscribe worked) ===
+ info!(
+ step = 5,
+ "re-subscribe - should send 'subscribe' (proves unsubscribe worked)"
+ );
+ let stream3 = client.subscribe_orderbook(asset_ids)?;
+ let mut stream3 = Box::pin(stream3);
+
+ match timeout(Duration::from_secs(10), stream3.next()).await {
+ Ok(Some(Ok(book))) => {
+ info!(
+ step = 5,
+ endpoint = "orderbook",
+ bids = book.bids.len(),
+ asks = book.asks.len(),
+ "stream3 receiving updates"
+ );
+ }
+ Ok(Some(Err(e))) => error!(step = 5, error = %e),
+ Ok(None) => error!(step = 5, "stream ended"),
+ Err(_) => error!(step = 5, "timeout"),
+ }
+
+ info!("example complete");
+ debug!(
+ "with debug logging, you should see subscribe/unsubscribe wire messages at steps 1, 4, and 5"
+ );
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/clob/ws/user.rs b/polymarket-client-sdk/examples/clob/ws/user.rs
new file mode 100644
index 0000000..771eaa7
--- /dev/null
+++ b/polymarket-client-sdk/examples/clob/ws/user.rs
@@ -0,0 +1,120 @@
+//! Demonstrates subscribing to authenticated user WebSocket channels.
+//!
+//! This example shows how to:
+//! 1. Build credentials for authenticated WebSocket access
+//! 2. Subscribe to user-specific order and trade events
+//! 3. Process real-time order updates and trade notifications
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_user --features ws,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=websocket_user.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example websocket_user --features ws,tracing
+//! ```
+//!
+//! Requires the following environment variables:
+//! - `POLYMARKET_API_KEY`
+//! - `POLYMARKET_API_SECRET`
+//! - `POLYMARKET_API_PASSPHRASE`
+//! - `POLYMARKET_ADDRESS`
+
+use std::fs::File;
+use std::str::FromStr as _;
+
+use futures::StreamExt as _;
+use polymarket_client_sdk::auth::Credentials;
+use polymarket_client_sdk::clob::ws::{Client, WsMessage};
+use polymarket_client_sdk::types::{Address, B256};
+use tracing::{debug, error, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+use uuid::Uuid;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let api_key = Uuid::parse_str(&std::env::var("POLYMARKET_API_KEY")?)?;
+ let api_secret = std::env::var("POLYMARKET_API_SECRET")?;
+ let api_passphrase = std::env::var("POLYMARKET_API_PASSPHRASE")?;
+ let address = Address::from_str(&std::env::var("POLYMARKET_ADDRESS")?)?;
+
+ let credentials = Credentials::new(api_key, api_secret, api_passphrase);
+
+ let client = Client::default().authenticate(credentials, address)?;
+ info!(
+ endpoint = "websocket",
+ authenticated = true,
+ "connected to authenticated WebSocket"
+ );
+
+ // Provide specific market IDs, or leave empty for all events
+ let markets: Vec = Vec::new();
+ let mut stream = std::pin::pin!(client.subscribe_user_events(markets)?);
+ info!(
+ endpoint = "subscribe_user_events",
+ "subscribed to user events"
+ );
+
+ while let Some(event) = stream.next().await {
+ match event {
+ Ok(WsMessage::Order(order)) => {
+ info!(
+ endpoint = "user_events",
+ event_type = "order",
+ order_id = %order.id,
+ market = %order.market,
+ msg_type = ?order.msg_type,
+ side = ?order.side,
+ price = %order.price
+ );
+ if let Some(size) = &order.original_size {
+ debug!(endpoint = "user_events", original_size = %size);
+ }
+ if let Some(matched) = &order.size_matched {
+ debug!(endpoint = "user_events", size_matched = %matched);
+ }
+ }
+ Ok(WsMessage::Trade(trade)) => {
+ info!(
+ endpoint = "user_events",
+ event_type = "trade",
+ trade_id = %trade.id,
+ market = %trade.market,
+ status = ?trade.status,
+ side = ?trade.side,
+ size = %trade.size,
+ price = %trade.price
+ );
+ if let Some(trader_side) = &trade.trader_side {
+ debug!(endpoint = "user_events", trader_side = ?trader_side);
+ }
+ }
+ Ok(other) => {
+ debug!(endpoint = "user_events", event = ?other);
+ }
+ Err(e) => {
+ error!(endpoint = "user_events", error = %e);
+ break;
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/ctf.rs b/polymarket-client-sdk/examples/ctf.rs
new file mode 100644
index 0000000..7ea1352
--- /dev/null
+++ b/polymarket-client-sdk/examples/ctf.rs
@@ -0,0 +1,225 @@
+#![allow(clippy::exhaustive_enums, reason = "Fine for examples")]
+#![allow(clippy::exhaustive_structs, reason = "Fine for examples")]
+
+//! CTF (Conditional Token Framework) example.
+//!
+//! This example demonstrates how to interact with the CTF contract to:
+//! - Calculate condition IDs, collection IDs, and position IDs
+//! - Split USDC collateral into outcome tokens (YES/NO)
+//! - Merge outcome tokens back into USDC
+//! - Redeem winning tokens after market resolution
+//!
+//! ## Usage
+//!
+//! For read-only operations (ID calculations):
+//! ```sh
+//! cargo run --example ctf --features ctf
+//! ```
+//!
+//! For write operations (split, merge, redeem), you need a private key:
+//! ```sh
+//! export POLYMARKET_PRIVATE_KEY="your_private_key"
+//! cargo run --example ctf --features ctf -- --write
+//! ```
+
+use std::env;
+use std::str::FromStr as _;
+
+use alloy::primitives::{B256, U256};
+use alloy::providers::ProviderBuilder;
+use alloy::signers::Signer as _;
+use alloy::signers::local::LocalSigner;
+use anyhow::Result;
+use polymarket_client_sdk::ctf::Client;
+use polymarket_client_sdk::ctf::types::{
+ CollectionIdRequest, ConditionIdRequest, MergePositionsRequest, PositionIdRequest,
+ RedeemPositionsRequest, SplitPositionRequest,
+};
+use polymarket_client_sdk::types::address;
+use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+use tracing::{error, info};
+
+const RPC_URL: &str = "https://polygon-rpc.com";
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ tracing_subscriber::fmt::init();
+
+ let args: Vec = env::args().collect();
+ let write_mode = args.iter().any(|arg| arg == "--write");
+
+ let chain = POLYGON;
+ info!("=== CTF (Conditional Token Framework) Example ===");
+
+ // For read-only operations, we don't need a wallet
+ let provider = ProviderBuilder::new().connect(RPC_URL).await?;
+ let client = Client::new(provider, chain)?;
+
+ info!("Connected to Polygon {chain}");
+ info!("CTF contract: 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
+
+ // Example: Calculate a condition ID
+ info!("--- Calculating Condition ID ---");
+ let oracle = address!("0x0000000000000000000000000000000000000001");
+ let question_id = B256::ZERO;
+ let outcome_slot_count = U256::from(2);
+
+ let condition_req = ConditionIdRequest::builder()
+ .oracle(oracle)
+ .question_id(question_id)
+ .outcome_slot_count(outcome_slot_count)
+ .build();
+
+ let condition_resp = client.condition_id(&condition_req).await?;
+ info!("Oracle: {oracle}");
+ info!("Question ID: {question_id}");
+ info!("Outcome Slots: {outcome_slot_count}");
+ info!("→ Condition ID: {}", condition_resp.condition_id);
+
+ // Example: Calculate collection IDs for YES and NO tokens
+ info!("--- Calculating Collection IDs ---");
+ let parent_collection_id = B256::ZERO;
+
+ // Collection ID for YES token (index set = 0b01 = 1)
+ let yes_collection_req = CollectionIdRequest::builder()
+ .parent_collection_id(parent_collection_id)
+ .condition_id(condition_resp.condition_id)
+ .index_set(U256::from(1))
+ .build();
+
+ let yes_collection_resp = client.collection_id(&yes_collection_req).await?;
+ info!("YES token (index set = 1):");
+ info!("→ Collection ID: {}", yes_collection_resp.collection_id);
+
+ // Collection ID for NO token (index set = 0b10 = 2)
+ let no_collection_req = CollectionIdRequest::builder()
+ .parent_collection_id(parent_collection_id)
+ .condition_id(condition_resp.condition_id)
+ .index_set(U256::from(2))
+ .build();
+
+ let no_collection_resp = client.collection_id(&no_collection_req).await?;
+ info!("NO token (index set = 2):");
+ info!("→ Collection ID: {}", no_collection_resp.collection_id);
+
+ // Example: Calculate position IDs (ERC1155 token IDs)
+ info!("--- Calculating Position IDs ---");
+ let usdc = address!("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
+
+ let yes_position_req = PositionIdRequest::builder()
+ .collateral_token(usdc)
+ .collection_id(yes_collection_resp.collection_id)
+ .build();
+
+ let yes_position_resp = client.position_id(&yes_position_req).await?;
+ info!(
+ "YES position (ERC1155 token ID): {}",
+ yes_position_resp.position_id
+ );
+
+ let no_position_req = PositionIdRequest::builder()
+ .collateral_token(usdc)
+ .collection_id(no_collection_resp.collection_id)
+ .build();
+
+ let no_position_resp = client.position_id(&no_position_req).await?;
+ info!(
+ "NO position (ERC1155 token ID): {}",
+ no_position_resp.position_id
+ );
+
+ // Write operations require a wallet
+ if write_mode {
+ info!("--- Write Operations (requires wallet) ---");
+
+ let private_key =
+ env::var(PRIVATE_KEY_VAR).expect("Need a private key for write operations");
+ let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(chain));
+
+ let provider = ProviderBuilder::new()
+ .wallet(signer.clone())
+ .connect(RPC_URL)
+ .await?;
+
+ let client = Client::new(provider, chain)?;
+ let wallet_address = signer.address();
+
+ info!("Using wallet: {wallet_address:?}");
+
+ // Example: Split 1 USDC into YES and NO tokens (using convenience method)
+ info!("--- Splitting Position (Binary Market) ---");
+ info!("This will split 1 USDC into 1 YES and 1 NO token");
+ info!("Note: You must approve the CTF contract to spend your USDC first!");
+
+ // Using the convenience method for binary markets
+ let split_req = SplitPositionRequest::for_binary_market(
+ usdc,
+ condition_resp.condition_id,
+ U256::from(1_000_000), // 1 USDC (6 decimals)
+ );
+
+ match client.split_position(&split_req).await {
+ Ok(split_resp) => {
+ info!("✓ Split transaction successful!");
+ info!(" Transaction hash: {}", split_resp.transaction_hash);
+ info!(" Block number: {}", split_resp.block_number);
+ }
+ Err(e) => {
+ error!("✗ Split failed: {e}");
+ error!(" Make sure you have approved the CTF contract and have sufficient USDC");
+ }
+ }
+
+ // Example: Merge YES and NO tokens back into USDC (using convenience method)
+ info!("--- Merging Positions (Binary Market) ---");
+ info!("This will merge 1 YES and 1 NO token back into 1 USDC");
+
+ // Using the convenience method for binary markets
+ let merge_req = MergePositionsRequest::for_binary_market(
+ usdc,
+ condition_resp.condition_id,
+ U256::from(1_000_000), // 1 full set
+ );
+
+ match client.merge_positions(&merge_req).await {
+ Ok(merge_resp) => {
+ info!("✓ Merge transaction successful!");
+ info!(" Transaction hash: {}", merge_resp.transaction_hash);
+ info!(" Block number: {}", merge_resp.block_number);
+ }
+ Err(e) => {
+ error!("✗ Merge failed: {e}");
+ error!(" Make sure you have sufficient YES and NO tokens");
+ }
+ }
+
+ // Example: Redeem winning tokens
+ info!("--- Redeeming Positions ---");
+ info!("This redeems winning tokens after market resolution");
+
+ // Using the convenience method for binary markets (redeems both YES and NO tokens)
+ let redeem_req =
+ RedeemPositionsRequest::for_binary_market(usdc, condition_resp.condition_id);
+
+ match client.redeem_positions(&redeem_req).await {
+ Ok(redeem_resp) => {
+ info!("✓ Redeem transaction successful!");
+ info!(" Transaction hash: {}", redeem_resp.transaction_hash);
+ info!(" Block number: {}", redeem_resp.block_number);
+ }
+ Err(e) => {
+ error!("✗ Redeem failed: {e}");
+ error!(" Make sure the condition is resolved and you have winning tokens");
+ }
+ }
+ } else {
+ info!("--- Write Operations ---");
+ info!("To test write operations (split, merge, redeem), run with --write flag:");
+ info!(" export POLYMARKET_PRIVATE_KEY=\"your_private_key\"");
+ info!(" cargo run --example ctf --features ctf -- --write");
+ }
+
+ info!("=== Example Complete ===");
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/data.rs b/polymarket-client-sdk/examples/data.rs
new file mode 100644
index 0000000..206b31b
--- /dev/null
+++ b/polymarket-client-sdk/examples/data.rs
@@ -0,0 +1,337 @@
+//! Comprehensive Data API endpoint explorer.
+//!
+//! This example dynamically tests all Data API endpoints by:
+//! 1. Fetching leaderboard data to discover real trader addresses
+//! 2. Using those addresses for user-specific queries
+//! 3. Extracting market IDs from positions for holder lookups
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example data --features data,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=data.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example data --features data,tracing
+//! ```
+
+use std::fs::File;
+
+use polymarket_client_sdk::data::Client;
+use polymarket_client_sdk::data::types::request::{
+ ActivityRequest, BuilderLeaderboardRequest, BuilderVolumeRequest, ClosedPositionsRequest,
+ HoldersRequest, LiveVolumeRequest, OpenInterestRequest, PositionsRequest, TradedRequest,
+ TraderLeaderboardRequest, TradesRequest, ValueRequest,
+};
+use polymarket_client_sdk::data::types::{LeaderboardCategory, TimePeriod};
+use polymarket_client_sdk::types::{Address, B256, address, b256};
+use tracing::{debug, error, info, warn};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+
+ // Fallback test data when dynamic discovery fails
+ let fallback_user = address!("56687bf447db6ffa42ffe2204a05edaa20f55839");
+ let fallback_market = b256!("dd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917");
+
+ // Health check
+ match client.health().await {
+ Ok(status) => info!(endpoint = "health", status = %status.data),
+ Err(e) => error!(endpoint = "health", error = %e),
+ }
+
+ // Fetch leaderboard to get real trader addresses
+ let leaderboard_result = client
+ .leaderboard(
+ &TraderLeaderboardRequest::builder()
+ .category(LeaderboardCategory::Overall)
+ .time_period(TimePeriod::Week)
+ .limit(10)?
+ .build(),
+ )
+ .await;
+
+ let user: Option = match &leaderboard_result {
+ Ok(traders) => {
+ info!(endpoint = "leaderboard", count = traders.len());
+ if let Some(trader) = traders.first() {
+ info!(
+ endpoint = "leaderboard",
+ rank = %trader.rank,
+ address = %trader.proxy_wallet,
+ pnl = %trader.pnl,
+ volume = %trader.vol
+ );
+ Some(trader.proxy_wallet)
+ } else {
+ None
+ }
+ }
+ Err(e) => {
+ warn!(endpoint = "leaderboard", error = %e, "using fallback user");
+ Some(fallback_user)
+ }
+ };
+
+ // Fetch positions for the discovered user
+ let market_id: Option = if let Some(user) = user {
+ let positions_result = client
+ .positions(&PositionsRequest::builder().user(user).limit(10)?.build())
+ .await;
+
+ match &positions_result {
+ Ok(positions) => {
+ info!(endpoint = "positions", user = %user, count = positions.len());
+ if let Some(pos) = positions.first() {
+ info!(
+ endpoint = "positions",
+ market = %pos.condition_id,
+ size = %pos.size,
+ value = %pos.current_value
+ );
+ Some(pos.condition_id)
+ } else {
+ // No positions found, use fallback market
+ warn!(
+ endpoint = "positions",
+ "no positions, using fallback market"
+ );
+ Some(fallback_market)
+ }
+ }
+ Err(e) => {
+ warn!(endpoint = "positions", user = %user, error = %e, "using fallback market");
+ Some(fallback_market)
+ }
+ }
+ } else {
+ debug!(endpoint = "positions", "skipped - no user address found");
+ Some(fallback_market)
+ };
+
+ // Fetch holders for the discovered market
+ if let Some(market) = market_id {
+ match client
+ .holders(
+ &HoldersRequest::builder()
+ .markets(vec![market])
+ .limit(5)?
+ .build(),
+ )
+ .await
+ {
+ Ok(meta_holders) => {
+ info!(endpoint = "holders", market = %market, tokens = meta_holders.len());
+ if let Some(meta) = meta_holders.first() {
+ info!(
+ endpoint = "holders",
+ token = %meta.token,
+ holders_count = meta.holders.len()
+ );
+ if let Some(holder) = meta.holders.first() {
+ info!(
+ endpoint = "holders",
+ address = %holder.proxy_wallet,
+ amount = %holder.amount
+ );
+ }
+ }
+ }
+ Err(e) => error!(endpoint = "holders", market = %market, error = %e),
+ }
+ }
+
+ // User activity, value, closed positions, and traded count
+ if let Some(user) = user {
+ match client
+ .activity(&ActivityRequest::builder().user(user).limit(5)?.build())
+ .await
+ {
+ Ok(activities) => {
+ info!(endpoint = "activity", user = %user, count = activities.len());
+ if let Some(act) = activities.first() {
+ info!(
+ endpoint = "activity",
+ activity_type = ?act.activity_type,
+ transaction = %act.transaction_hash
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "activity", user = %user, error = %e),
+ }
+
+ match client
+ .value(&ValueRequest::builder().user(user).build())
+ .await
+ {
+ Ok(values) => {
+ info!(endpoint = "value", user = %user, count = values.len());
+ if let Some(value) = values.first() {
+ info!(
+ endpoint = "value",
+ user = %value.user,
+ total = %value.value
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "value", user = %user, error = %e),
+ }
+
+ match client
+ .closed_positions(
+ &ClosedPositionsRequest::builder()
+ .user(user)
+ .limit(5)?
+ .build(),
+ )
+ .await
+ {
+ Ok(positions) => {
+ info!(endpoint = "closed_positions", user = %user, count = positions.len());
+ if let Some(pos) = positions.first() {
+ info!(
+ endpoint = "closed_positions",
+ market = %pos.condition_id,
+ realized_pnl = %pos.realized_pnl
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "closed_positions", user = %user, error = %e),
+ }
+
+ match client
+ .traded(&TradedRequest::builder().user(user).build())
+ .await
+ {
+ Ok(traded) => {
+ info!(
+ endpoint = "traded",
+ user = %user,
+ markets_traded = traded.traded
+ );
+ }
+ Err(e) => error!(endpoint = "traded", user = %user, error = %e),
+ }
+ }
+
+ // Trades - global trade feed
+ match client.trades(&TradesRequest::default()).await {
+ Ok(trades) => {
+ info!(endpoint = "trades", count = trades.len());
+ if let Some(trade) = trades.first() {
+ info!(
+ endpoint = "trades",
+ market = %trade.condition_id,
+ side = ?trade.side,
+ size = %trade.size,
+ price = %trade.price
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "trades", error = %e),
+ }
+
+ // Open interest
+ match client.open_interest(&OpenInterestRequest::default()).await {
+ Ok(oi_list) => {
+ info!(endpoint = "open_interest", count = oi_list.len());
+ if let Some(oi) = oi_list.first() {
+ info!(
+ endpoint = "open_interest",
+ market = ?oi.market,
+ value = %oi.value
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "open_interest", error = %e),
+ }
+
+ // Live volume (using event ID 1 as example)
+ match client
+ .live_volume(&LiveVolumeRequest::builder().id(1).build())
+ .await
+ {
+ Ok(volumes) => {
+ info!(
+ endpoint = "live_volume",
+ event_id = 1,
+ count = volumes.len()
+ );
+ if let Some(vol) = volumes.first() {
+ info!(
+ endpoint = "live_volume",
+ total = %vol.total,
+ markets = vol.markets.len()
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "live_volume", event_id = 1, error = %e),
+ }
+
+ // Builder leaderboard
+ match client
+ .builder_leaderboard(
+ &BuilderLeaderboardRequest::builder()
+ .time_period(TimePeriod::Week)
+ .limit(5)?
+ .build(),
+ )
+ .await
+ {
+ Ok(builders) => {
+ info!(endpoint = "builder_leaderboard", count = builders.len());
+ if let Some(builder) = builders.first() {
+ info!(
+ endpoint = "builder_leaderboard",
+ name = %builder.builder,
+ volume = %builder.volume,
+ rank = %builder.rank
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "builder_leaderboard", error = %e),
+ }
+
+ // Builder volume time series
+ match client
+ .builder_volume(
+ &BuilderVolumeRequest::builder()
+ .time_period(TimePeriod::Week)
+ .build(),
+ )
+ .await
+ {
+ Ok(volumes) => {
+ info!(endpoint = "builder_volume", count = volumes.len());
+ if let Some(vol) = volumes.first() {
+ info!(
+ endpoint = "builder_volume",
+ builder = %vol.builder,
+ date = %vol.dt,
+ volume = %vol.volume
+ );
+ }
+ }
+ Err(e) => error!(endpoint = "builder_volume", error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/gamma/client.rs b/polymarket-client-sdk/examples/gamma/client.rs
new file mode 100644
index 0000000..f2de57d
--- /dev/null
+++ b/polymarket-client-sdk/examples/gamma/client.rs
@@ -0,0 +1,403 @@
+//! Comprehensive Gamma API endpoint explorer.
+//!
+//! This example dynamically tests all Gamma API endpoints by:
+//! 1. Fetching lists first (events, markets, tags, etc.)
+//! 2. Extracting real IDs/slugs from responses
+//! 3. Using those IDs for subsequent lookups
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example gamma --features gamma,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=gamma.log RUST_LOG=info,hyper_util=off,hyper=off,reqwest=off,h2=off,rustls=off cargo run --example gamma --features gamma,tracing
+//! ```
+
+use std::fs::File;
+
+use polymarket_client_sdk::gamma::Client;
+use polymarket_client_sdk::gamma::types::ParentEntityType;
+use polymarket_client_sdk::gamma::types::request::{
+ CommentsByIdRequest, CommentsByUserAddressRequest, CommentsRequest, EventByIdRequest,
+ EventBySlugRequest, EventTagsRequest, EventsRequest, MarketByIdRequest, MarketBySlugRequest,
+ MarketTagsRequest, MarketsRequest, PublicProfileRequest, RelatedTagsByIdRequest,
+ RelatedTagsBySlugRequest, SearchRequest, SeriesByIdRequest, SeriesListRequest, TagByIdRequest,
+ TagBySlugRequest, TagsRequest, TeamsRequest,
+};
+use tracing::{debug, info};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+
+ match client.status().await {
+ Ok(s) => info!(endpoint = "status", result = %s),
+ Err(e) => debug!(endpoint = "status", error = %e),
+ }
+
+ match client.sports().await {
+ Ok(v) => info!(endpoint = "sports", count = v.len()),
+ Err(e) => debug!(endpoint = "sports", error = %e),
+ }
+
+ match client.sports_market_types().await {
+ Ok(v) => info!(
+ endpoint = "sports_market_types",
+ count = v.market_types.len()
+ ),
+ Err(e) => debug!(endpoint = "sports_market_types", error = %e),
+ }
+
+ match client
+ .teams(&TeamsRequest::builder().limit(5).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "teams", count = v.len()),
+ Err(e) => debug!(endpoint = "teams", error = %e),
+ }
+
+ let tags_result = client.tags(&TagsRequest::builder().limit(10).build()).await;
+ match &tags_result {
+ Ok(v) => info!(endpoint = "tags", count = v.len()),
+ Err(e) => debug!(endpoint = "tags", error = %e),
+ }
+
+ // Use "politics" tag - known to have related tags
+ let tag_slug = "politics";
+ let tag_result = client
+ .tag_by_slug(&TagBySlugRequest::builder().slug(tag_slug).build())
+ .await;
+ let tag_id = match &tag_result {
+ Ok(tag) => {
+ info!(endpoint = "tag_by_slug", slug = tag_slug, id = %tag.id);
+ Some(tag.id.clone())
+ }
+ Err(e) => {
+ debug!(endpoint = "tag_by_slug", slug = tag_slug, error = %e);
+ None
+ }
+ };
+
+ if let Some(id) = &tag_id {
+ match client
+ .tag_by_id(&TagByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "tag_by_id", id = %id),
+ Err(e) => debug!(endpoint = "tag_by_id", id = %id, error = %e),
+ }
+
+ match client
+ .related_tags_by_id(&RelatedTagsByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "related_tags_by_id", id = %id, count = v.len()),
+ Err(e) => debug!(endpoint = "related_tags_by_id", id = %id, error = %e),
+ }
+
+ match client
+ .tags_related_to_tag_by_id(&RelatedTagsByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "tags_related_to_tag_by_id", id = %id, count = v.len()),
+ Err(e) => debug!(endpoint = "tags_related_to_tag_by_id", id = %id, error = %e),
+ }
+ }
+
+ match client
+ .related_tags_by_slug(&RelatedTagsBySlugRequest::builder().slug(tag_slug).build())
+ .await
+ {
+ Ok(v) => info!(
+ endpoint = "related_tags_by_slug",
+ slug = tag_slug,
+ count = v.len()
+ ),
+ Err(e) => debug!(endpoint = "related_tags_by_slug", slug = tag_slug, error = %e),
+ }
+
+ match client
+ .tags_related_to_tag_by_slug(&RelatedTagsBySlugRequest::builder().slug(tag_slug).build())
+ .await
+ {
+ Ok(v) => info!(
+ endpoint = "tags_related_to_tag_by_slug",
+ slug = tag_slug,
+ count = v.len()
+ ),
+ Err(e) => debug!(endpoint = "tags_related_to_tag_by_slug", slug = tag_slug, error = %e),
+ }
+
+ let events_result = client
+ .events(
+ &EventsRequest::builder()
+ .active(true)
+ .limit(20)
+ .order(vec!["volume".to_owned()])
+ .ascending(false)
+ .build(),
+ )
+ .await;
+
+ // Find an event with comments
+ let (event_with_comments, any_event) = match &events_result {
+ Ok(events) => {
+ info!(endpoint = "events", count = events.len());
+ let with_comments = events
+ .iter()
+ .find(|e| e.comment_count.unwrap_or(0) > 0)
+ .map(|e| (e.id.clone(), e.slug.clone(), e.comment_count.unwrap_or(0)));
+ let any = events.first().map(|e| (e.id.clone(), e.slug.clone()));
+ (with_comments, any)
+ }
+ Err(e) => {
+ debug!(endpoint = "events", error = %e);
+ (None, None)
+ }
+ };
+
+ if let Some((event_id, event_slug)) = &any_event {
+ match client
+ .event_by_id(&EventByIdRequest::builder().id(event_id).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "event_by_id", id = %event_id),
+ Err(e) => debug!(endpoint = "event_by_id", id = %event_id, error = %e),
+ }
+
+ match client
+ .event_tags(&EventTagsRequest::builder().id(event_id).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "event_tags", id = %event_id, count = v.len()),
+ Err(e) => debug!(endpoint = "event_tags", id = %event_id, error = %e),
+ }
+
+ if let Some(slug) = event_slug {
+ match client
+ .event_by_slug(&EventBySlugRequest::builder().slug(slug).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "event_by_slug", slug = %slug),
+ Err(e) => debug!(endpoint = "event_by_slug", slug = %slug, error = %e),
+ }
+ }
+ }
+
+ let markets_result = client
+ .markets(&MarketsRequest::builder().closed(false).limit(10).build())
+ .await;
+
+ let (market_id, market_slug) = match &markets_result {
+ Ok(markets) => {
+ info!(endpoint = "markets", count = markets.len());
+ markets
+ .first()
+ .map_or((None, None), |m| (Some(m.id.clone()), m.slug.clone()))
+ }
+ Err(e) => {
+ debug!(endpoint = "markets", error = %e);
+ (None, None)
+ }
+ };
+
+ // Test multiple slugs - verifies repeated query params work (issue #147)
+ if let Ok(markets) = &markets_result {
+ let slugs: Vec = markets
+ .iter()
+ .filter_map(|m| m.slug.clone())
+ .take(3)
+ .collect();
+
+ if slugs.len() >= 2 {
+ match client
+ .markets(&MarketsRequest::builder().slug(slugs.clone()).build())
+ .await
+ {
+ Ok(v) => info!(
+ endpoint = "markets_multiple_slugs",
+ slugs = ?slugs,
+ count = v.len(),
+ "verified repeated query params work"
+ ),
+ Err(e) => debug!(endpoint = "markets_multiple_slugs", slugs = ?slugs, error = %e),
+ }
+ }
+ }
+
+ if let Some(id) = &market_id {
+ match client
+ .market_by_id(&MarketByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "market_by_id", id = %id),
+ Err(e) => debug!(endpoint = "market_by_id", id = %id, error = %e),
+ }
+
+ match client
+ .market_tags(&MarketTagsRequest::builder().id(id).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "market_tags", id = %id, count = v.len()),
+ Err(e) => debug!(endpoint = "market_tags", id = %id, error = %e),
+ }
+ }
+
+ if let Some(slug) = &market_slug {
+ match client
+ .market_by_slug(&MarketBySlugRequest::builder().slug(slug).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "market_by_slug", slug = %slug),
+ Err(e) => debug!(endpoint = "market_by_slug", slug = %slug, error = %e),
+ }
+ }
+
+ let series_result = client
+ .series(
+ &SeriesListRequest::builder()
+ .limit(10)
+ .order("volume".to_owned())
+ .ascending(false)
+ .build(),
+ )
+ .await;
+
+ let series_id = match &series_result {
+ Ok(series) => {
+ info!(endpoint = "series", count = series.len());
+ series.first().map(|s| s.id.clone())
+ }
+ Err(e) => {
+ debug!(endpoint = "series", error = %e);
+ None
+ }
+ };
+
+ if let Some(id) = &series_id {
+ match client
+ .series_by_id(&SeriesByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(_) => info!(endpoint = "series_by_id", id = %id),
+ Err(e) => debug!(endpoint = "series_by_id", id = %id, error = %e),
+ }
+ }
+
+ let (comment_id, user_address) = if let Some((event_id, _, comment_count)) =
+ &event_with_comments
+ {
+ let comments_result = client
+ .comments(
+ &CommentsRequest::builder()
+ .parent_entity_type(ParentEntityType::Event)
+ .parent_entity_id(event_id)
+ .limit(10)
+ .build(),
+ )
+ .await;
+
+ match &comments_result {
+ Ok(comments) => {
+ info!(endpoint = "comments", event_id = %event_id, expected = comment_count, count = comments.len());
+ comments
+ .first()
+ .map_or((None, None), |c| (Some(c.id.clone()), c.user_address))
+ }
+ Err(e) => {
+ debug!(endpoint = "comments", event_id = %event_id, error = %e);
+ (None, None)
+ }
+ }
+ } else {
+ debug!(
+ endpoint = "comments",
+ "skipped - no event with comments found"
+ );
+ (None, None)
+ };
+
+ if let Some(id) = &comment_id {
+ match client
+ .comments_by_id(&CommentsByIdRequest::builder().id(id).build())
+ .await
+ {
+ Ok(v) => info!(endpoint = "comments_by_id", id = %id, count = v.len()),
+ Err(e) => debug!(endpoint = "comments_by_id", id = %id, error = %e),
+ }
+ }
+
+ if let Some(addr) = user_address {
+ match client
+ .comments_by_user_address(
+ &CommentsByUserAddressRequest::builder()
+ .user_address(addr)
+ .limit(5)
+ .build(),
+ )
+ .await
+ {
+ Ok(v) => info!(endpoint = "comments_by_user_address", address = %addr, count = v.len()),
+ Err(e) => debug!(endpoint = "comments_by_user_address", address = %addr, error = %e),
+ }
+ }
+
+ // Use the user_address from comments if available
+ if let Some(profile_address) = user_address {
+ match client
+ .public_profile(
+ &PublicProfileRequest::builder()
+ .address(profile_address)
+ .build(),
+ )
+ .await
+ {
+ Ok(p) => {
+ let name = p.pseudonym.as_deref().unwrap_or("anonymous");
+ info!(endpoint = "public_profile", address = %profile_address, name = %name);
+ }
+ Err(e) => debug!(endpoint = "public_profile", address = %profile_address, error = %e),
+ }
+ }
+
+ let query = "trump";
+ match client
+ .search(&SearchRequest::builder().q(query).build())
+ .await
+ {
+ Ok(r) => {
+ let events = r.events.map_or(0, |e| e.len());
+ let tags = r.tags.map_or(0, |t| t.len());
+ let profiles = r.profiles.map_or(0, |p| p.len());
+ info!(
+ endpoint = "search",
+ query = query,
+ events = events,
+ tags = tags,
+ profiles = profiles
+ );
+ }
+ Err(e) => debug!(endpoint = "search", query = query, error = %e),
+ }
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/gamma/streaming.rs b/polymarket-client-sdk/examples/gamma/streaming.rs
new file mode 100644
index 0000000..641ea64
--- /dev/null
+++ b/polymarket-client-sdk/examples/gamma/streaming.rs
@@ -0,0 +1,126 @@
+//! Gamma API streaming endpoint explorer.
+//!
+//! This example demonstrates streaming data from Gamma API endpoints using offset-based
+//! pagination and single-call endpoints. It covers all response types:
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info cargo run --example gamma_streaming --features gamma,tracing
+//! ```
+//!
+//! Optionally log to a file:
+//! ```sh
+//! LOG_FILE=gamma_streaming.log RUST_LOG=info cargo run --example gamma_streaming --features gamma,tracing
+//! ```
+
+use std::fs::File;
+
+use futures::StreamExt as _;
+use polymarket_client_sdk::gamma::{
+ Client,
+ types::request::{EventsRequest, MarketsRequest},
+};
+use tracing::{info, warn};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::layer::SubscriberExt as _;
+use tracing_subscriber::util::SubscriberInitExt as _;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ if let Ok(path) = std::env::var("LOG_FILE") {
+ let file = File::create(path)?;
+ tracing_subscriber::registry()
+ .with(EnvFilter::from_default_env())
+ .with(
+ tracing_subscriber::fmt::layer()
+ .with_writer(file)
+ .with_ansi(false),
+ )
+ .init();
+ } else {
+ tracing_subscriber::fmt::init();
+ }
+
+ let client = Client::default();
+
+ stream_events(&client).await?;
+ stream_markets(&client).await?;
+
+ Ok(())
+}
+
+/// Streams events from the Gamma API.
+async fn stream_events(client: &Client) -> anyhow::Result<()> {
+ info!(stream = "events", "starting stream");
+
+ let mut stream = client
+ .stream_data(
+ |c, limit, offset| {
+ let request = EventsRequest::builder()
+ .active(true)
+ .limit(limit)
+ .offset(offset)
+ .build();
+ async move { c.events(&request).await }
+ },
+ 100,
+ )
+ .take(100)
+ .boxed();
+
+ let mut count = 0_u32;
+
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok(event) => {
+ count += 1;
+ info!(stream = "events", count, "{event:?}");
+ }
+ Err(e) => {
+ warn!(stream = "events", error = %e, "stream error");
+ break;
+ }
+ }
+ }
+
+ info!(stream = "events", total = count, "stream completed");
+ Ok(())
+}
+
+/// Streams markets from the Gamma API.
+async fn stream_markets(client: &Client) -> anyhow::Result<()> {
+ info!(stream = "markets", "starting stream");
+
+ let mut stream = client
+ .stream_data(
+ |c, limit, offset| {
+ let request = MarketsRequest::builder()
+ .closed(false)
+ .limit(limit)
+ .offset(offset)
+ .build();
+ async move { c.markets(&request).await }
+ },
+ 100,
+ )
+ .take(100)
+ .boxed();
+
+ let mut count = 0_u32;
+
+ while let Some(result) = stream.next().await {
+ match result {
+ Ok(market) => {
+ count += 1;
+ info!(stream = "markets", count, "{market:?}");
+ }
+ Err(e) => {
+ warn!(stream = "markets", error = %e, "stream error");
+ break;
+ }
+ }
+ }
+
+ info!(stream = "markets", total = count, "stream completed");
+ Ok(())
+}
diff --git a/polymarket-client-sdk/examples/rtds_crypto_prices.rs b/polymarket-client-sdk/examples/rtds_crypto_prices.rs
new file mode 100644
index 0000000..7975399
--- /dev/null
+++ b/polymarket-client-sdk/examples/rtds_crypto_prices.rs
@@ -0,0 +1,246 @@
+//! Comprehensive RTDS (Real-Time Data Socket) endpoint explorer.
+//!
+//! This example dynamically tests all RTDS streaming endpoints by:
+//! 1. Subscribing to Binance crypto prices (all symbols and filtered)
+//! 2. Subscribing to Chainlink price feeds
+//! 3. Subscribing to comment events
+//! 4. Demonstrating unsubscribe functionality
+//! 5. Showing connection state and subscription count
+//!
+//! Run with tracing enabled:
+//! ```sh
+//! RUST_LOG=info cargo run --example rtds_crypto_prices --features rtds,tracing
+//! ```
+
+use std::time::Duration;
+
+use futures::StreamExt as _;
+use polymarket_client_sdk::rtds::Client;
+use polymarket_client_sdk::rtds::types::response::CommentType;
+use tokio::time::timeout;
+use tracing::{debug, info, warn};
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ tracing_subscriber::fmt::init();
+
+ let client = Client::default();
+
+ // Show connection state
+ let state = client.connection_state();
+ info!(endpoint = "connection_state", state = ?state);
+
+ // Subscribe to all crypto prices from Binance
+ info!(
+ stream = "crypto_prices",
+ "Subscribing to Binance prices (all symbols)"
+ );
+ match client.subscribe_crypto_prices(None) {
+ Ok(stream) => {
+ let mut stream = Box::pin(stream);
+ let mut count = 0;
+
+ while let Ok(Some(result)) = timeout(Duration::from_secs(5), stream.next()).await {
+ match result {
+ Ok(price) => {
+ info!(
+ stream = "crypto_prices",
+ symbol = %price.symbol.to_uppercase(),
+ value = %price.value,
+ timestamp = %price.timestamp
+ );
+ count += 1;
+ if count >= 5 {
+ break;
+ }
+ }
+ Err(e) => debug!(stream = "crypto_prices", error = %e),
+ }
+ }
+ info!(stream = "crypto_prices", received = count);
+ }
+ Err(e) => debug!(stream = "crypto_prices", error = %e),
+ }
+
+ // Subscribe to specific crypto symbols
+ let symbols = vec!["btcusdt".to_owned(), "ethusdt".to_owned()];
+ info!(
+ stream = "crypto_prices_filtered",
+ symbols = ?symbols,
+ "Subscribing to specific symbols"
+ );
+ match client.subscribe_crypto_prices(Some(symbols.clone())) {
+ Ok(stream) => {
+ let mut stream = Box::pin(stream);
+ let mut count = 0;
+
+ while let Ok(Some(result)) = timeout(Duration::from_secs(5), stream.next()).await {
+ match result {
+ Ok(price) => {
+ info!(
+ stream = "crypto_prices_filtered",
+ symbol = %price.symbol.to_uppercase(),
+ value = %price.value
+ );
+ count += 1;
+ if count >= 3 {
+ break;
+ }
+ }
+ Err(e) => debug!(stream = "crypto_prices_filtered", error = %e),
+ }
+ }
+ info!(stream = "crypto_prices_filtered", received = count);
+ }
+ Err(e) => debug!(stream = "crypto_prices_filtered", error = %e),
+ }
+
+ // Subscribe to specific Chainlink symbol
+ let chainlink_symbol = "btc/usd".to_owned();
+ info!(
+ stream = "chainlink_prices",
+ symbol = %chainlink_symbol,
+ "Subscribing to Chainlink price feed"
+ );
+ match client.subscribe_chainlink_prices(Some(chainlink_symbol)) {
+ Ok(stream) => {
+ let mut stream = Box::pin(stream);
+ let mut count = 0;
+
+ while let Ok(Some(result)) = timeout(Duration::from_secs(5), stream.next()).await {
+ match result {
+ Ok(price) => {
+ info!(
+ stream = "chainlink_prices",
+ symbol = %price.symbol,
+ value = %price.value,
+ timestamp = %price.timestamp
+ );
+ count += 1;
+ if count >= 3 {
+ break;
+ }
+ }
+ Err(e) => debug!(stream = "chainlink_prices", error = %e),
+ }
+ }
+ info!(stream = "chainlink_prices", received = count);
+ }
+ Err(e) => debug!(stream = "chainlink_prices", error = %e),
+ }
+
+ // Subscribe to comments (unauthenticated)
+ info!(stream = "comments", "Subscribing to comment events");
+ match client.subscribe_comments(None) {
+ Ok(stream) => {
+ let mut stream = Box::pin(stream);
+ let mut count = 0;
+
+ // Comments may be infrequent, use shorter timeout
+ while let Ok(Some(result)) = timeout(Duration::from_secs(3), stream.next()).await {
+ match result {
+ Ok(comment) => {
+ info!(
+ stream = "comments",
+ id = %comment.id,
+ parent_type = ?comment.parent_entity_type,
+ parent_id = %comment.parent_entity_id
+ );
+ count += 1;
+ if count >= 3 {
+ break;
+ }
+ }
+ Err(e) => debug!(stream = "comments", error = %e),
+ }
+ }
+ if count > 0 {
+ info!(stream = "comments", received = count);
+ } else {
+ debug!(stream = "comments", "no comments received within timeout");
+ }
+ }
+ Err(e) => debug!(stream = "comments", error = %e),
+ }
+
+ // Subscribe to specific comment type
+ info!(
+ stream = "comments_created",
+ comment_type = ?CommentType::CommentCreated,
+ "Subscribing to created comments only"
+ );
+ match client.subscribe_comments(Some(CommentType::CommentCreated)) {
+ Ok(stream) => {
+ let mut stream = Box::pin(stream);
+ let mut count = 0;
+
+ while let Ok(Some(result)) = timeout(Duration::from_secs(3), stream.next()).await {
+ match result {
+ Ok(comment) => {
+ info!(
+ stream = "comments_created",
+ id = %comment.id,
+ parent_id = %comment.parent_entity_id
+ );
+ count += 1;
+ if count >= 2 {
+ break;
+ }
+ }
+ Err(e) => debug!(stream = "comments_created", error = %e),
+ }
+ }
+ if count > 0 {
+ info!(stream = "comments_created", received = count);
+ } else {
+ debug!(
+ stream = "comments_created",
+ "no created comments received within timeout"
+ );
+ }
+ }
+ Err(e) => debug!(stream = "comments_created", error = %e),
+ }
+
+ // Show subscription count before unsubscribe
+ let sub_count = client.subscription_count();
+ info!(
+ endpoint = "subscription_count",
+ count = sub_count,
+ "Before unsubscribe"
+ );
+
+ // Demonstrate unsubscribe functionality
+ info!("=== Demonstrating unsubscribe ===");
+
+ // Unsubscribe from crypto_prices (Binance)
+ info!("Unsubscribing from Binance crypto prices");
+ match client.unsubscribe_crypto_prices() {
+ Ok(()) => info!("Successfully unsubscribed from crypto_prices"),
+ Err(e) => warn!(error = %e, "Failed to unsubscribe from crypto_prices"),
+ }
+
+ // Unsubscribe from chainlink prices
+ info!("Unsubscribing from Chainlink prices");
+ match client.unsubscribe_chainlink_prices() {
+ Ok(()) => info!("Successfully unsubscribed from chainlink_prices"),
+ Err(e) => warn!(error = %e, "Failed to unsubscribe from chainlink_prices"),
+ }
+
+ // Unsubscribe from comments (wildcard)
+ info!("Unsubscribing from comments");
+ match client.unsubscribe_comments(None) {
+ Ok(()) => info!("Successfully unsubscribed from comments"),
+ Err(e) => warn!(error = %e, "Failed to unsubscribe from comments"),
+ }
+
+ // Show final subscription count after unsubscribe
+ let sub_count = client.subscription_count();
+ info!(
+ endpoint = "subscription_count",
+ count = sub_count,
+ "After unsubscribe"
+ );
+
+ Ok(())
+}
diff --git a/polymarket-client-sdk/rustfmt.toml b/polymarket-client-sdk/rustfmt.toml
new file mode 100644
index 0000000..9475118
--- /dev/null
+++ b/polymarket-client-sdk/rustfmt.toml
@@ -0,0 +1,4 @@
+reorder_imports = true
+reorder_modules = true
+
+group_imports = "StdExternalCrate"
diff --git a/polymarket-client-sdk/src/auth.rs b/polymarket-client-sdk/src/auth.rs
new file mode 100644
index 0000000..fd5e0b5
--- /dev/null
+++ b/polymarket-client-sdk/src/auth.rs
@@ -0,0 +1,616 @@
+// Re-exported types for public API convenience
+/// The [`Signer`] trait from alloy for signing operations.
+/// Implement this trait or use provided signers like [`LocalSigner`] or AWS KMS signers.
+pub use alloy::signers::Signer;
+/// Local wallet signer for signing with a private key.
+/// This is the most common signer implementation.
+pub use alloy::signers::local::LocalSigner;
+use async_trait::async_trait;
+use base64::Engine as _;
+use base64::engine::general_purpose::URL_SAFE;
+use hmac::{Hmac, Mac as _};
+use reqwest::header::HeaderMap;
+use reqwest::{Body, Request};
+/// Secret string types that redact values in debug output for security.
+pub use secrecy::{ExposeSecret, SecretString};
+use serde::Deserialize;
+use sha2::Sha256;
+/// UUID type used for API keys and identifiers.
+pub use uuid::Uuid;
+
+use crate::{Result, Timestamp};
+
+/// Type alias for API keys, which are UUIDs.
+pub type ApiKey = Uuid;
+
+/// Generic set of credentials used to authenticate to the Polymarket API. These credentials are
+/// returned when calling [`crate::clob::Client::create_or_derive_api_key`], [`crate::clob::Client::derive_api_key`], or
+/// [`crate::clob::Client::create_api_key`]. They are used by the [`state::Authenticated`] client to
+/// sign the [`Request`] when making calls to the API.
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct Credentials {
+ #[serde(alias = "apiKey")]
+ pub(crate) key: ApiKey,
+ pub(crate) secret: SecretString,
+ pub(crate) passphrase: SecretString,
+}
+
+impl Credentials {
+ #[must_use]
+ pub fn new(key: Uuid, secret: String, passphrase: String) -> Self {
+ Self {
+ key,
+ secret: SecretString::from(secret),
+ passphrase: SecretString::from(passphrase),
+ }
+ }
+
+ /// Returns the API key.
+ #[must_use]
+ pub fn key(&self) -> ApiKey {
+ self.key
+ }
+
+ /// Returns the secret.
+ #[must_use]
+ pub fn secret(&self) -> &SecretString {
+ &self.secret
+ }
+
+ /// Returns the passphrase.
+ #[must_use]
+ pub fn passphrase(&self) -> &SecretString {
+ &self.passphrase
+ }
+}
+
+/// Each client can exist in one state at a time, i.e. [`state::Unauthenticated`] or
+/// [`state::Authenticated`].
+pub mod state {
+ use crate::auth::{Credentials, Kind};
+ use crate::types::Address;
+
+ /// The initial state of the client
+ #[non_exhaustive]
+ #[derive(Clone, Debug)]
+ pub struct Unauthenticated;
+
+ /// The elevated state of the client. For example, calling [`crate::clob::Client::authentication_builder`]
+ /// will return an [`crate::clob::client::AuthenticationBuilder`], which can be turned into
+ /// an authenticated clob via [`crate::clob::client::AuthenticationBuilder::authenticate`].
+ ///
+ /// See `examples/authenticated.rs` for more context.
+ #[non_exhaustive]
+ #[derive(Clone, Debug)]
+ #[cfg_attr(
+ not(feature = "clob"),
+ expect(dead_code, reason = "Fields used by clob module when feature enabled")
+ )]
+ pub struct Authenticated {
+ /// The signer's address that created the credentials
+ pub(crate) address: Address,
+ /// The [`Credentials`]'s `secret` is used to generate an [`crate::signer::hmac`] which is
+ /// passed in the L2 headers ([`super::HeaderMap`]) `POLY_SIGNATURE` field.
+ pub(crate) credentials: Credentials,
+ /// The [`Kind`] that this [`Authenticated`] exhibits. Used to generate additional headers
+ /// for different types of authentication, e.g. Builder.
+ pub(crate) kind: K,
+ }
+
+ /// The clob state can only be [`Unauthenticated`] or [`Authenticated`].
+ pub trait State: sealed::Sealed {}
+
+ impl State for Unauthenticated {}
+ impl sealed::Sealed for Unauthenticated {}
+
+ impl State for Authenticated {}
+ impl sealed::Sealed for Authenticated {}
+
+ mod sealed {
+ pub trait Sealed {}
+ }
+}
+
+/// Asynchronous authentication enricher
+///
+/// This trait is used to apply extra headers to authenticated requests. For example, in the case
+/// of [`builder::Builder`] authentication, Builder headers are added in addition to the [`Normal`]
+/// L2 headers.
+#[async_trait]
+pub trait Kind: sealed::Sealed + Clone + Send + Sync + 'static {
+ async fn extra_headers(&self, request: &Request, timestamp: Timestamp) -> Result;
+}
+
+/// Non-special, generic authentication. Sometimes referred to as L2 authentication.
+#[non_exhaustive]
+#[derive(Clone, Debug)]
+pub struct Normal;
+
+#[async_trait]
+impl Kind for Normal {
+ async fn extra_headers(&self, _request: &Request, _timestamp: Timestamp) -> Result {
+ Ok(HeaderMap::new())
+ }
+}
+
+impl sealed::Sealed for Normal {}
+
+#[async_trait]
+impl Kind for builder::Builder {
+ async fn extra_headers(&self, request: &Request, timestamp: Timestamp) -> Result {
+ self.create_headers(request, timestamp).await
+ }
+}
+
+impl sealed::Sealed for builder::Builder {}
+
+mod sealed {
+ pub trait Sealed {}
+}
+
+#[cfg(feature = "clob")]
+pub(crate) mod l1 {
+ use std::borrow::Cow;
+
+ use alloy::core::sol;
+ use alloy::dyn_abi::Eip712Domain;
+ use alloy::hex::ToHexExt as _;
+ use alloy::primitives::{ChainId, U256};
+ use alloy::signers::Signer;
+ use alloy::sol_types::SolStruct as _;
+ use reqwest::header::HeaderMap;
+
+ use crate::{Result, Timestamp};
+
+ pub(crate) const POLY_ADDRESS: &str = "POLY_ADDRESS";
+ pub(crate) const POLY_NONCE: &str = "POLY_NONCE";
+ pub(crate) const POLY_SIGNATURE: &str = "POLY_SIGNATURE";
+ pub(crate) const POLY_TIMESTAMP: &str = "POLY_TIMESTAMP";
+
+ sol! {
+ #[non_exhaustive]
+ struct ClobAuth {
+ address address;
+ string timestamp;
+ uint256 nonce;
+ string message;
+ }
+ }
+
+ /// Returns the [`HeaderMap`] needed to obtain [`Credentials`] .
+ pub(crate) async fn create_headers(
+ signer: &S,
+ chain_id: ChainId,
+ timestamp: Timestamp,
+ nonce: Option,
+ ) -> Result {
+ let naive_nonce = nonce.unwrap_or(0);
+
+ let auth = ClobAuth {
+ address: signer.address(),
+ timestamp: timestamp.to_string(),
+ nonce: U256::from(naive_nonce),
+ message: "This message attests that I control the given wallet".to_owned(),
+ };
+
+ let domain = Eip712Domain {
+ name: Some(Cow::Borrowed("ClobAuthDomain")),
+ version: Some(Cow::Borrowed("1")),
+ chain_id: Some(U256::from(chain_id)),
+ ..Eip712Domain::default()
+ };
+
+ let hash = auth.eip712_signing_hash(&domain);
+ let signature = signer.sign_hash(&hash).await?;
+
+ let mut map = HeaderMap::new();
+ map.insert(
+ POLY_ADDRESS,
+ signer.address().encode_hex_with_prefix().parse()?,
+ );
+ map.insert(POLY_NONCE, naive_nonce.to_string().parse()?);
+ map.insert(POLY_SIGNATURE, signature.to_string().parse()?);
+ map.insert(POLY_TIMESTAMP, timestamp.to_string().parse()?);
+
+ Ok(map)
+ }
+}
+
+#[cfg(feature = "clob")]
+pub(crate) mod l2 {
+ use alloy::hex::ToHexExt as _;
+ use reqwest::Request;
+ use reqwest::header::HeaderMap;
+ use secrecy::ExposeSecret as _;
+
+ use crate::auth::state::Authenticated;
+ use crate::auth::{Kind, hmac, to_message};
+ use crate::{Result, Timestamp};
+
+ pub(crate) const POLY_ADDRESS: &str = "POLY_ADDRESS";
+ pub(crate) const POLY_API_KEY: &str = "POLY_API_KEY";
+ pub(crate) const POLY_PASSPHRASE: &str = "POLY_PASSPHRASE";
+ pub(crate) const POLY_SIGNATURE: &str = "POLY_SIGNATURE";
+ pub(crate) const POLY_TIMESTAMP: &str = "POLY_TIMESTAMP";
+
+ /// Returns the [`Headers`] needed to interact with any authenticated endpoints.
+ pub(crate) async fn create_headers(
+ state: &Authenticated,
+ request: &Request,
+ timestamp: Timestamp,
+ ) -> Result {
+ let credentials = &state.credentials;
+ let signature = hmac(&credentials.secret, &to_message(request, timestamp))?;
+
+ let mut map = HeaderMap::new();
+
+ map.insert(
+ POLY_ADDRESS,
+ state.address.encode_hex_with_prefix().parse()?,
+ );
+ map.insert(POLY_API_KEY, state.credentials.key.to_string().parse()?);
+ map.insert(
+ POLY_PASSPHRASE,
+ state.credentials.passphrase.expose_secret().parse()?,
+ );
+ map.insert(POLY_SIGNATURE, signature.parse()?);
+ map.insert(POLY_TIMESTAMP, timestamp.to_string().parse()?);
+
+ let extra_headers = state.kind.extra_headers(request, timestamp).await?;
+
+ map.extend(extra_headers);
+
+ Ok(map)
+ }
+}
+
+/// Specific structs and methods used in configuring and authenticating the Builder flow
+pub mod builder {
+ use reqwest::header::HeaderMap;
+ use reqwest::{Client, Request};
+ use secrecy::ExposeSecret as _;
+ use serde::{Deserialize, Serialize};
+ use serde_json::json;
+ /// URL type for remote builder host configuration.
+ pub use url::Url;
+
+ use crate::auth::{Credentials, body_to_string, hmac, to_message};
+ use crate::{Result, Timestamp};
+
+ pub(crate) const POLY_BUILDER_API_KEY: &str = "POLY_BUILDER_API_KEY";
+ pub(crate) const POLY_BUILDER_PASSPHRASE: &str = "POLY_BUILDER_PASSPHRASE";
+ pub(crate) const POLY_BUILDER_SIGNATURE: &str = "POLY_BUILDER_SIGNATURE";
+ pub(crate) const POLY_BUILDER_TIMESTAMP: &str = "POLY_BUILDER_TIMESTAMP";
+
+ #[derive(Clone, Debug, Deserialize, Serialize)]
+ #[serde(rename_all = "UPPERCASE")]
+ #[expect(
+ clippy::struct_field_names,
+ reason = "Have to prefix `poly_builder` for serde"
+ )]
+ struct HeaderPayload {
+ poly_builder_api_key: String,
+ poly_builder_timestamp: String,
+ poly_builder_passphrase: String,
+ poly_builder_signature: String,
+ }
+
+ /// Configuration used to authenticate as a [Builder](https://docs.polymarket.com/developers/builders/builder-intro). Can either be [`Config::local`]
+ /// or [`Config::remote`]. Local uses locally accessible Builder credentials to generate builder headers. Remote obtains them from a signing server
+ #[non_exhaustive]
+ #[derive(Clone, Debug)]
+ pub enum Config {
+ Local(Credentials),
+ Remote { host: Url, token: Option },
+ }
+
+ impl Config {
+ #[must_use]
+ pub fn local(credentials: Credentials) -> Self {
+ Config::Local(credentials)
+ }
+
+ pub fn remote(host: &str, token: Option) -> Result {
+ let host = Url::parse(host)?;
+ Ok(Config::Remote { host, token })
+ }
+ }
+
+ /// Used to generate the Builder headers
+ #[non_exhaustive]
+ #[derive(Clone, Debug)]
+ pub struct Builder {
+ pub(crate) config: Config,
+ pub(crate) client: Client,
+ }
+
+ impl Builder {
+ pub(crate) async fn create_headers(
+ &self,
+ request: &Request,
+ timestamp: Timestamp,
+ ) -> Result {
+ match &self.config {
+ Config::Local(credentials) => {
+ let signature = hmac(&credentials.secret, &to_message(request, timestamp))?;
+
+ let mut map = HeaderMap::new();
+
+ map.insert(POLY_BUILDER_API_KEY, credentials.key.to_string().parse()?);
+ map.insert(
+ POLY_BUILDER_PASSPHRASE,
+ credentials.passphrase.expose_secret().parse()?,
+ );
+ map.insert(POLY_BUILDER_SIGNATURE, signature.parse()?);
+ map.insert(POLY_BUILDER_TIMESTAMP, timestamp.to_string().parse()?);
+
+ Ok(map)
+ }
+ Config::Remote { host, token } => {
+ let payload = json!({
+ "method": request.method().as_str(),
+ "path": request.url().path(),
+ "body": &request.body().and_then(body_to_string).unwrap_or_default(),
+ "timestamp": timestamp,
+ });
+
+ let mut headers = HeaderMap::new();
+ if let Some(token) = token {
+ headers.insert("Authorization", format!("Bearer {token}").parse()?);
+ }
+
+ let response = self
+ .client
+ .post(host.to_string())
+ .headers(headers)
+ .json(&payload)
+ .send()
+ .await?;
+
+ let remote_headers: HeaderPayload = response.error_for_status()?.json().await?;
+
+ let mut map = HeaderMap::new();
+
+ map.insert(
+ POLY_BUILDER_SIGNATURE,
+ remote_headers.poly_builder_signature.parse()?,
+ );
+ map.insert(
+ POLY_BUILDER_TIMESTAMP,
+ remote_headers.poly_builder_timestamp.parse()?,
+ );
+ map.insert(
+ POLY_BUILDER_API_KEY,
+ remote_headers.poly_builder_api_key.parse()?,
+ );
+ map.insert(
+ POLY_BUILDER_PASSPHRASE,
+ remote_headers.poly_builder_passphrase.parse()?,
+ );
+
+ Ok(map)
+ }
+ }
+ }
+ }
+}
+
+#[must_use]
+fn to_message(request: &Request, timestamp: Timestamp) -> String {
+ let method = request.method();
+ let body = request.body().and_then(body_to_string).unwrap_or_default();
+ let path = request.url().path();
+
+ format!("{timestamp}{method}{path}{body}")
+}
+
+#[must_use]
+fn body_to_string(body: &Body) -> Option {
+ body.as_bytes()
+ .map(String::from_utf8_lossy)
+ .map(|b| b.replace('\'', "\""))
+}
+
+fn hmac(secret: &SecretString, message: &str) -> Result {
+ let decoded_secret = URL_SAFE.decode(secret.expose_secret())?;
+ let mut mac = Hmac::::new_from_slice(&decoded_secret)?;
+ mac.update(message.as_bytes());
+
+ let result = mac.finalize().into_bytes();
+ Ok(URL_SAFE.encode(result))
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr as _;
+
+ #[cfg(feature = "clob")]
+ use alloy::signers::local::LocalSigner;
+ use reqwest::{Client, Method, RequestBuilder};
+ use serde_json::json;
+ use url::Url;
+ use uuid::Uuid;
+
+ use super::*;
+ use crate::auth::builder::Config;
+ #[cfg(feature = "clob")]
+ use crate::auth::state::Authenticated;
+ #[cfg(feature = "clob")]
+ use crate::types::address;
+ #[cfg(feature = "clob")]
+ use crate::{AMOY, Result};
+
+ // publicly known private key
+ #[cfg(feature = "clob")]
+ const PRIVATE_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
+
+ #[cfg(feature = "clob")]
+ #[tokio::test]
+ async fn l1_headers_should_succeed() -> anyhow::Result<()> {
+ let signer = LocalSigner::from_str(PRIVATE_KEY)?.with_chain_id(Some(AMOY));
+
+ let headers = l1::create_headers(&signer, AMOY, 10_000_000, Some(23)).await?;
+
+ assert_eq!(
+ signer.address(),
+ address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")
+ );
+ assert_eq!(
+ headers[l1::POLY_ADDRESS],
+ "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
+ );
+ assert_eq!(headers[l1::POLY_NONCE], "23");
+ assert_eq!(
+ headers[l1::POLY_SIGNATURE],
+ "0xf62319a987514da40e57e2f4d7529f7bac38f0355bd88bb5adbb3768d80de6c1682518e0af677d5260366425f4361e7b70c25ae232aff0ab2331e2b164a1aedc1b"
+ );
+ assert_eq!(headers[l1::POLY_TIMESTAMP], "10000000");
+
+ Ok(())
+ }
+
+ #[cfg(feature = "clob")]
+ #[tokio::test]
+ async fn l2_headers_should_succeed() -> anyhow::Result<()> {
+ let signer = LocalSigner::from_str(PRIVATE_KEY)?;
+
+ let authenticated = Authenticated {
+ address: signer.address(),
+ credentials: Credentials {
+ key: Uuid::nil(),
+ passphrase: SecretString::from(
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
+ ),
+ secret: SecretString::from(
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
+ ),
+ },
+ kind: Normal,
+ };
+
+ let request = Request::new(Method::GET, Url::parse("http://localhost/")?);
+ let headers = l2::create_headers(&authenticated, &request, 1).await?;
+
+ assert_eq!(
+ headers[l2::POLY_ADDRESS],
+ "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
+ );
+ assert_eq!(
+ headers[l2::POLY_PASSPHRASE],
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ );
+ assert_eq!(headers[l2::POLY_API_KEY], Uuid::nil().to_string());
+ assert_eq!(
+ headers[l2::POLY_SIGNATURE],
+ "eHaylCwqRSOa2LFD77Nt_SaTpbsxzN8eTEI3LryhEj4="
+ );
+ assert_eq!(headers[l2::POLY_TIMESTAMP], "1");
+
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn builder_headers_should_succeed() -> Result<()> {
+ let credentials = Credentials {
+ key: Uuid::nil(),
+ passphrase: SecretString::from(
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
+ ),
+ secret: SecretString::from("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
+ };
+ let config = Config::local(credentials);
+ let request = Request::new(Method::GET, Url::parse("http://localhost/")?);
+ let timestamp = 1;
+
+ let builder = builder::Builder {
+ config,
+ client: Client::default(),
+ };
+
+ let headers = builder.create_headers(&request, timestamp).await?;
+
+ assert_eq!(
+ headers[builder::POLY_BUILDER_API_KEY],
+ Uuid::nil().to_string()
+ );
+ assert_eq!(
+ headers[builder::POLY_BUILDER_PASSPHRASE],
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ );
+ assert_eq!(headers[builder::POLY_BUILDER_TIMESTAMP], "1");
+
+ Ok(())
+ }
+
+ #[test]
+ fn request_args_should_succeed() -> Result<()> {
+ let request = Request::new(Method::POST, Url::parse("http://localhost/path")?);
+ let request = RequestBuilder::from_parts(Client::new(), request)
+ .json(&json!({"foo": "bar"}))
+ .build()?;
+
+ let timestamp = 1;
+
+ assert_eq!(
+ to_message(&request, timestamp),
+ r#"1POST/path{"foo":"bar"}"#
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn hmac_succeeds() -> Result<()> {
+ let json = json!({
+ "hash": "0x123"
+ });
+
+ let method = Method::from_str("test-sign")
+ .expect("To avoid needing an error variant just for one test");
+ let request = Request::new(method, Url::parse("http://localhost/orders")?);
+ let request = RequestBuilder::from_parts(Client::new(), request)
+ .json(&json)
+ .build()?;
+
+ let message = to_message(&request, 1_000_000);
+ let signature = hmac(
+ &SecretString::from("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
+ &message,
+ )?;
+
+ assert_eq!(message, r#"1000000test-sign/orders{"hash":"0x123"}"#);
+ assert_eq!(signature, "4gJVbox-R6XlDK4nlaicig0_ANVL1qdcahiL8CXfXLM=");
+
+ Ok(())
+ }
+
+ #[test]
+ fn credentials_key_returns_api_key() {
+ let key = Uuid::new_v4();
+ let credentials = Credentials::new(key, "secret".to_owned(), "passphrase".to_owned());
+ assert_eq!(credentials.key(), key);
+ }
+
+ #[test]
+ fn debug_does_not_expose_secrets() {
+ let secret_value = "my_super_secret_value_12345";
+ let passphrase_value = "my_super_secret_passphrase_67890";
+ let credentials = Credentials::new(
+ Uuid::nil(),
+ secret_value.to_owned(),
+ passphrase_value.to_owned(),
+ );
+
+ let debug_output = format!("{credentials:?}");
+
+ // Verify that the secret values are NOT present in the debug output
+ assert!(
+ !debug_output.contains(secret_value),
+ "Debug output should NOT contain the secret value. Got: {debug_output}"
+ );
+ assert!(
+ !debug_output.contains(passphrase_value),
+ "Debug output should NOT contain the passphrase value. Got: {debug_output}"
+ );
+ }
+}
diff --git a/polymarket-client-sdk/src/bridge/client.rs b/polymarket-client-sdk/src/bridge/client.rs
new file mode 100644
index 0000000..f46de2d
--- /dev/null
+++ b/polymarket-client-sdk/src/bridge/client.rs
@@ -0,0 +1,191 @@
+use reqwest::{
+ Client as ReqwestClient, Method,
+ header::{HeaderMap, HeaderValue},
+};
+use url::Url;
+
+use super::types::{
+ DepositRequest, DepositResponse, StatusRequest, StatusResponse, SupportedAssetsResponse,
+};
+use crate::Result;
+
+/// Client for the Polymarket Bridge API.
+///
+/// The Bridge API enables bridging assets from various chains (EVM, Solana, Bitcoin)
+/// to USDC.e on Polygon for trading on Polymarket.
+///
+/// # Example
+///
+/// ```no_run
+/// use polymarket_client_sdk::types::address;
+/// use polymarket_client_sdk::bridge::{Client, types::DepositRequest};
+///
+/// # async fn example() -> Result<(), Box> {
+/// let client = Client::default();
+///
+/// // Get deposit addresses
+/// let request = DepositRequest::builder()
+/// .address(address!("56687bf447db6ffa42ffe2204a05edaa20f55839"))
+/// .build();
+/// let response = client.deposit(&request).await?;
+///
+/// // Get supported assets
+/// let assets = client.supported_assets().await?;
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Clone, Debug)]
+pub struct Client {
+ host: Url,
+ client: ReqwestClient,
+}
+
+impl Default for Client {
+ fn default() -> Self {
+ Client::new("https://bridge.polymarket.com")
+ .expect("Client with default endpoint should succeed")
+ }
+}
+
+impl Client {
+ /// Creates a new Bridge API client with a custom host.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the host URL is invalid or the HTTP client fails to build.
+ pub fn new(host: &str) -> Result {
+ let mut headers = HeaderMap::new();
+
+ headers.insert("User-Agent", HeaderValue::from_static("rs_clob_client"));
+ headers.insert("Accept", HeaderValue::from_static("*/*"));
+ headers.insert("Connection", HeaderValue::from_static("keep-alive"));
+ headers.insert("Content-Type", HeaderValue::from_static("application/json"));
+ let client = ReqwestClient::builder().default_headers(headers).build()?;
+
+ Ok(Self {
+ host: Url::parse(host)?,
+ client,
+ })
+ }
+
+ /// Returns the host URL for the client.
+ #[must_use]
+ pub fn host(&self) -> &Url {
+ &self.host
+ }
+
+ #[must_use]
+ fn client(&self) -> &ReqwestClient {
+ &self.client
+ }
+
+ /// Create deposit addresses for a Polymarket wallet.
+ ///
+ /// Generates unique deposit addresses for bridging assets to Polymarket.
+ /// Returns addresses for EVM-compatible chains, Solana, and Bitcoin.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use polymarket_client_sdk::types::address;
+ /// use polymarket_client_sdk::bridge::{Client, types::DepositRequest};
+ ///
+ /// # async fn example() -> Result<(), Box> {
+ /// let client = Client::default();
+ /// let request = DepositRequest::builder()
+ /// .address(address!("56687bf447db6ffa42ffe2204a05edaa20f55839"))
+ /// .build();
+ ///
+ /// let response = client.deposit(&request).await?;
+ /// println!("EVM: {}", response.address.evm);
+ /// println!("SVM: {}", response.address.svm);
+ /// println!("BTC: {}", response.address.btc);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn deposit(&self, request: &DepositRequest) -> Result {
+ let request = self
+ .client()
+ .request(Method::POST, format!("{}deposit", self.host()))
+ .json(request)
+ .build()?;
+
+ crate::request(&self.client, request, None).await
+ }
+
+ /// Get all supported chains and tokens for deposits.
+ ///
+ /// Returns information about which assets can be deposited and their
+ /// minimum deposit amounts in USD.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use polymarket_client_sdk::bridge::Client;
+ ///
+ /// # async fn example() -> Result<(), Box> {
+ /// let client = Client::default();
+ /// let response = client.supported_assets().await?;
+ ///
+ /// for asset in response.supported_assets {
+ /// println!(
+ /// "{} ({}) on {} - min: ${:.2}",
+ /// asset.token.name,
+ /// asset.token.symbol,
+ /// asset.chain_name,
+ /// asset.min_checkout_usd
+ /// );
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub async fn supported_assets(&self) -> Result {
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}supported-assets", self.host()))
+ .build()?;
+
+ crate::request(&self.client, request, None).await
+ }
+
+ /// Get the transaction status for all deposits associated with a given deposit address.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// use polymarket_client_sdk::bridge::{Client, types::StatusRequest};
+ ///
+ /// # async fn example() -> Result<(), Box> {
+ /// let client = Client::default();
+ ///
+ /// let request = StatusRequest::builder()
+ /// .address("56687bf447db6ffa42ffe2204a05edaa20f55839")
+ /// .build();
+ /// let response = client.status(&request).await?;
+ ///
+ /// for tx in response.transactions {
+ /// println!(
+ /// "Sent {} amount of token {} on chainId {} to destination chainId {} with status {:?}",
+ /// tx.from_amount_base_unit,
+ /// tx.from_token_address,
+ /// tx.from_chain_id,
+ /// tx.to_chain_id,
+ /// tx.status
+ /// );
+ /// }
+ /// # Ok(())
+ /// # }
+ ///
+ /// ```
+ pub async fn status(&self, request: &StatusRequest) -> Result {
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}status/{}", self.host(), request.address),
+ )
+ .build()?;
+
+ crate::request(&self.client, request, None).await
+ }
+}
diff --git a/polymarket-client-sdk/src/bridge/mod.rs b/polymarket-client-sdk/src/bridge/mod.rs
new file mode 100644
index 0000000..423aab2
--- /dev/null
+++ b/polymarket-client-sdk/src/bridge/mod.rs
@@ -0,0 +1,52 @@
+//! Polymarket Bridge API client and types.
+//!
+//! **Feature flag:** `bridge` (required to use this module)
+//!
+//! This module provides a client for interacting with the Polymarket Bridge API,
+//! which enables bridging assets from various chains (EVM, Solana, Bitcoin) to
+//! USDC.e on Polygon for trading on Polymarket.
+//!
+//! # Overview
+//!
+//! The Bridge API is a read/write HTTP API that provides:
+//! - Deposit address generation for multi-chain asset bridging
+//! - Supported asset and chain information
+//!
+//! ## Available Endpoints
+//!
+//! | Endpoint | Method | Description |
+//! |----------|--------|-------------|
+//! | `/deposit` | POST | Create deposit addresses for a wallet |
+//! | `/supported-assets` | GET | Get supported chains and tokens |
+//!
+//! # Example
+//!
+//! ```no_run
+//! use polymarket_client_sdk::types::address;
+//! use polymarket_client_sdk::bridge::{Client, types::DepositRequest};
+//!
+//! # async fn example() -> Result<(), Box> {
+//! // Create a client with the default endpoint
+//! let client = Client::default();
+//!
+//! // Get deposit addresses for a wallet
+//! let request = DepositRequest::builder()
+//! .address(address!("56687bf447db6ffa42ffe2204a05edaa20f55839"))
+//! .build();
+//!
+//! let response = client.deposit(&request).await?;
+//! println!("EVM: {}", response.address.evm);
+//! println!("SVM: {}", response.address.svm);
+//! println!("BTC: {}", response.address.btc);
+//! # Ok(())
+//! # }
+//! ```
+//!
+//! # API Base URL
+//!
+//! The default API endpoint is `https://bridge.polymarket.com`.
+
+pub mod client;
+pub mod types;
+
+pub use client::Client;
diff --git a/polymarket-client-sdk/src/bridge/types/mod.rs b/polymarket-client-sdk/src/bridge/types/mod.rs
new file mode 100644
index 0000000..b8be632
--- /dev/null
+++ b/polymarket-client-sdk/src/bridge/types/mod.rs
@@ -0,0 +1,5 @@
+mod request;
+mod response;
+
+pub use request::*;
+pub use response::*;
diff --git a/polymarket-client-sdk/src/bridge/types/request.rs b/polymarket-client-sdk/src/bridge/types/request.rs
new file mode 100644
index 0000000..8dccf98
--- /dev/null
+++ b/polymarket-client-sdk/src/bridge/types/request.rs
@@ -0,0 +1,41 @@
+use bon::Builder;
+use serde::Serialize;
+
+use crate::types::Address;
+
+/// Request to create deposit addresses for a Polymarket wallet.
+///
+/// # Example
+///
+/// ```
+/// use polymarket_client_sdk::types::address;
+/// use polymarket_client_sdk::bridge::types::DepositRequest;
+///
+/// let request = DepositRequest::builder()
+/// .address(address!("56687bf447db6ffa42ffe2204a05edaa20f55839"))
+/// .build();
+/// ```
+#[non_exhaustive]
+#[derive(Debug, Clone, Serialize, Builder)]
+pub struct DepositRequest {
+ /// The Polymarket wallet address to generate deposit addresses for.
+ pub address: Address,
+}
+
+/// Request to get deposit statuses for a given deposit address.
+///
+/// ### Note: This doesn't use the alloy Address type, since it supports Solana and Bitcoin addresses.
+///
+/// # Example
+///
+/// ```
+/// use polymarket_client_sdk::bridge::types::StatusRequest;
+///
+/// let request = StatusRequest::builder().address("0x9cb12Ec30568ab763ae5891ce4b8c5C96CeD72C9").build();
+/// ```
+#[non_exhaustive]
+#[derive(Debug, Clone, Builder)]
+#[builder(on(String, into))]
+pub struct StatusRequest {
+ pub address: String,
+}
diff --git a/polymarket-client-sdk/src/bridge/types/response.rs b/polymarket-client-sdk/src/bridge/types/response.rs
new file mode 100644
index 0000000..d2fa981
--- /dev/null
+++ b/polymarket-client-sdk/src/bridge/types/response.rs
@@ -0,0 +1,124 @@
+use alloy::primitives::U256;
+use bon::Builder;
+use serde::Deserialize;
+use serde_with::{DisplayFromStr, serde_as};
+
+use crate::types::{Address, ChainId, Decimal};
+
+/// Response containing deposit addresses for different blockchain networks.
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+pub struct DepositResponse {
+ /// Deposit addresses for different blockchain networks.
+ pub address: DepositAddresses,
+ /// Additional information about supported chains.
+ pub note: Option,
+}
+
+/// Deposit addresses for different blockchain networks.
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[builder(on(String, into))]
+pub struct DepositAddresses {
+ /// EVM-compatible deposit address (Ethereum, Polygon, Arbitrum, Base, etc.).
+ pub evm: Address,
+ /// Solana Virtual Machine deposit address.
+ pub svm: String,
+ /// Bitcoin deposit address.
+ pub btc: String,
+}
+
+/// Response containing all supported assets for deposits.
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[serde(rename_all = "camelCase")]
+pub struct SupportedAssetsResponse {
+ /// List of supported assets with minimum deposit amounts.
+ pub supported_assets: Vec,
+ /// Additional information about supported chains and assets.
+ pub note: Option,
+}
+
+/// A supported asset with chain and token information.
+#[non_exhaustive]
+#[serde_as]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[builder(on(String, into))]
+#[serde(rename_all = "camelCase")]
+pub struct SupportedAsset {
+ /// Blockchain chain ID (e.g., 1 for Ethereum mainnet, 137 for Polygon).
+ /// Deserialized from JSON string representation (e.g., `"137"`).
+ #[serde_as(as = "DisplayFromStr")]
+ pub chain_id: ChainId,
+ /// Human-readable chain name.
+ pub chain_name: String,
+ /// Token information.
+ pub token: Token,
+ /// Minimum deposit amount in USD.
+ pub min_checkout_usd: Decimal,
+}
+
+/// Token information for a supported asset.
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[builder(on(String, into))]
+pub struct Token {
+ /// Full token name.
+ pub name: String,
+ /// Token symbol.
+ pub symbol: String,
+ /// Token contract address.
+ pub address: String,
+ /// Token decimals.
+ pub decimals: u8,
+}
+
+/// Transaction status for all deposits associated with a given deposit address.
+#[non_exhaustive]
+#[serde_as]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[builder(on(String, into))]
+#[serde(rename_all = "camelCase")]
+pub struct StatusResponse {
+ /// List of transactions for the given address
+ pub transactions: Vec,
+}
+
+#[non_exhaustive]
+#[serde_as]
+#[derive(Debug, Clone, Deserialize, PartialEq, Builder)]
+#[builder(on(String, into))]
+#[serde(rename_all = "camelCase")]
+pub struct DepositTransaction {
+ /// Source chain ID
+ #[serde_as(as = "DisplayFromStr")]
+ pub from_chain_id: ChainId,
+ /// Source token contract address
+ pub from_token_address: String,
+ /// Amount in base units (without decimals)
+ #[serde_as(as = "DisplayFromStr")]
+ pub from_amount_base_unit: U256,
+ /// Destination chain ID
+ #[serde_as(as = "DisplayFromStr")]
+ pub to_chain_id: ChainId,
+ /// Destination chain ID
+ pub to_token_address: Address,
+ /// Current status of the transaction
+ pub status: DepositTransactionStatus,
+ /// Transaction hash (only available when status is Completed)
+ pub tx_hash: Option,
+ /// Unix timestamp in milliseconds when transaction was created (missing when status is `DepositDetected`)
+ pub created_time_ms: Option,
+}
+
+#[non_exhaustive]
+#[derive(Debug, Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum DepositTransactionStatus {
+ DepositDetected,
+ Processing,
+ OriginTxConfirmed,
+ Submitted,
+ Completed,
+ Failed,
+}
diff --git a/polymarket-client-sdk/src/clob/client.rs b/polymarket-client-sdk/src/clob/client.rs
new file mode 100644
index 0000000..e9d4042
--- /dev/null
+++ b/polymarket-client-sdk/src/clob/client.rs
@@ -0,0 +1,2455 @@
+use std::borrow::Cow;
+use std::marker::PhantomData;
+use std::mem;
+use std::sync::Arc;
+#[cfg(feature = "heartbeats")]
+use std::time::Duration;
+
+use alloy::dyn_abi::Eip712Domain;
+use alloy::primitives::U256;
+use alloy::signers::Signer;
+use alloy::sol_types::SolStruct as _;
+use async_stream::try_stream;
+use bon::Builder;
+use chrono::{NaiveDate, Utc};
+use dashmap::DashMap;
+use futures::Stream;
+use reqwest::header::{HeaderMap, HeaderValue};
+use reqwest::{Client as ReqwestClient, Method, Request};
+use serde_json::json;
+#[cfg(all(feature = "tracing", feature = "heartbeats"))]
+use tracing::{debug, error};
+use url::Url;
+use uuid::Uuid;
+#[cfg(feature = "heartbeats")]
+use {tokio::sync::oneshot::Receiver, tokio::time, tokio_util::sync::CancellationToken};
+
+use crate::auth::builder::{Builder, Config as BuilderConfig};
+use crate::auth::state::{Authenticated, State, Unauthenticated};
+use crate::auth::{Credentials, Kind, Normal};
+use crate::clob::order_builder::{Limit, Market, OrderBuilder, generate_seed};
+use crate::clob::types::request::{
+ BalanceAllowanceRequest, CancelMarketOrderRequest, DeleteNotificationsRequest,
+ LastTradePriceRequest, MidpointRequest, OrderBookSummaryRequest, OrdersRequest,
+ PriceHistoryRequest, PriceRequest, SpreadRequest, TradesRequest, UpdateBalanceAllowanceRequest,
+ UserRewardsEarningRequest,
+};
+use crate::clob::types::response::{
+ ApiKeysResponse, BalanceAllowanceResponse, BanStatusResponse, BuilderApiKeyResponse,
+ BuilderTradeResponse, CancelOrdersResponse, CurrentRewardResponse, FeeRateResponse,
+ GeoblockResponse, HeartbeatResponse, LastTradePriceResponse, LastTradesPricesResponse,
+ MarketResponse, MarketRewardResponse, MidpointResponse, MidpointsResponse, NegRiskResponse,
+ NotificationResponse, OpenOrderResponse, OrderBookSummaryResponse, OrderScoringResponse,
+ OrdersScoringResponse, Page, PostOrderResponse, PriceHistoryResponse, PriceResponse,
+ PricesResponse, RewardsPercentagesResponse, SimplifiedMarketResponse, SpreadResponse,
+ SpreadsResponse, TickSizeResponse, TotalUserEarningResponse, TradeResponse,
+ UserEarningResponse, UserRewardsEarningResponse,
+};
+#[cfg(feature = "rfq")]
+use crate::clob::types::{
+ AcceptRfqQuoteRequest, AcceptRfqQuoteResponse, ApproveRfqOrderRequest, ApproveRfqOrderResponse,
+ CancelRfqQuoteRequest, CancelRfqRequestRequest, CreateRfqQuoteRequest, CreateRfqQuoteResponse,
+ CreateRfqRequestRequest, CreateRfqRequestResponse, RfqQuote, RfqQuotesRequest, RfqRequest,
+ RfqRequestsRequest,
+};
+use crate::clob::types::{SignableOrder, SignatureType, SignedOrder, TickSize};
+use crate::error::{Error, Kind as ErrorKind, Synchronization};
+use crate::types::Address;
+use crate::{
+ AMOY, POLYGON, Result, Timestamp, ToQueryParams as _, auth, contract_config,
+ derive_proxy_wallet, derive_safe_wallet,
+};
+
+const ORDER_NAME: Option> = Some(Cow::Borrowed("Polymarket CTF Exchange"));
+const VERSION: Option> = Some(Cow::Borrowed("1"));
+
+const TERMINAL_CURSOR: &str = "LTE="; // base64("-1")
+
+/// The type used to build a request to authenticate the inner [`Client`]. Calling
+/// `authenticate` on this will elevate that inner `client` into an [`Client>`].
+pub struct AuthenticationBuilder<'signer, S: Signer, K: Kind = Normal> {
+ /// The initially unauthenticated client that is "carried forward" into the authenticated client.
+ client: Client,
+ /// The signer used to generate the L1 headers that will return a set of [`Credentials`].
+ signer: &'signer S,
+ /// If [`Credentials`] are supplied, then those are used instead of making new calls to obtain one.
+ credentials: Option,
+ /// An optional `nonce` value, when `credentials` are not present, to pass along to the call to
+ /// create or derive [`Credentials`].
+ nonce: Option,
+ /// The [`Kind`] that this [`AuthenticationBuilder`] exhibits. Used to generate additional
+ /// headers for different types of authentication, e.g. Builder.
+ kind: K,
+ /// The optional [`Address`] used to represent the funder for this `client`. If a funder is set
+ /// then `signature_type` must match `Some(SignatureType::Proxy | Signature::GnosisSafe)`. Conversely,
+ /// if funder is not set, then `signature_type` must be `Some(SignatureType::Eoa)`.
+ funder: Option,
+ /// The optional [`SignatureType`], see `funder` for more information.
+ signature_type: Option,
+ /// The optional salt/seed generator for use in creating [`SignableOrder`]s
+ salt_generator: Option u64>,
+}
+
+impl AuthenticationBuilder<'_, S, K> {
+ #[must_use]
+ pub fn nonce(mut self, nonce: u32) -> Self {
+ self.nonce = Some(nonce);
+ self
+ }
+
+ #[must_use]
+ pub fn credentials(mut self, credentials: Credentials) -> Self {
+ self.credentials = Some(credentials);
+ self
+ }
+
+ #[must_use]
+ pub fn funder(mut self, funder: Address) -> Self {
+ self.funder = Some(funder);
+ self
+ }
+
+ #[must_use]
+ pub fn signature_type(mut self, signature_type: SignatureType) -> Self {
+ self.signature_type = Some(signature_type);
+ self
+ }
+
+ #[must_use]
+ pub fn salt_generator(mut self, salt_generator: fn() -> u64) -> Self {
+ self.salt_generator = Some(salt_generator);
+ self
+ }
+
+ /// Attempt to elevate the inner `client` to [`Client>`] using the optional
+ /// fields supplied in the builder.
+ #[expect(
+ clippy::missing_panics_doc,
+ reason = "chain_id panic is guarded by prior validation"
+ )]
+ pub async fn authenticate(self) -> Result>> {
+ let inner = Arc::into_inner(self.client.inner).ok_or(Synchronization)?;
+
+ match self.signer.chain_id() {
+ Some(chain) if chain == POLYGON || chain == AMOY => {}
+ Some(chain) => {
+ return Err(Error::validation(format!(
+ "Only Polygon and AMOY are supported, got {chain}"
+ )));
+ }
+ None => {
+ return Err(Error::validation(
+ "Chain id not set, be sure to provide one on the signer",
+ ));
+ }
+ }
+
+ // SAFETY: chain_id is validated above to be either POLYGON or AMOY
+ let chain_id = self.signer.chain_id().expect("validated above");
+
+ // Auto-derive funder from signer using CREATE2 when using proxy signature types
+ // without explicit funder. This computes the deterministic wallet address that
+ // Polymarket deploys for the user.
+ let funder = match (self.funder, self.signature_type) {
+ (None, Some(SignatureType::Proxy)) => {
+ let derived =
+ derive_proxy_wallet(self.signer.address(), chain_id).ok_or_else(|| {
+ Error::validation(
+ "Proxy wallet derivation not supported on this chain. \
+ Please provide an explicit funder address.",
+ )
+ })?;
+ Some(derived)
+ }
+ (None, Some(SignatureType::GnosisSafe)) => {
+ let derived =
+ derive_safe_wallet(self.signer.address(), chain_id).ok_or_else(|| {
+ Error::validation(
+ "Safe wallet derivation not supported on this chain. \
+ Please provide an explicit funder address.",
+ )
+ })?;
+ Some(derived)
+ }
+ (funder, _) => funder,
+ };
+
+ match (funder, self.signature_type) {
+ (Some(_), Some(sig @ SignatureType::Eoa)) => {
+ return Err(Error::validation(format!(
+ "Cannot have a funder address with a {sig} signature type"
+ )));
+ }
+ (
+ Some(Address::ZERO),
+ Some(sig @ (SignatureType::Proxy | SignatureType::GnosisSafe)),
+ ) => {
+ return Err(Error::validation(format!(
+ "Cannot have a zero funder address with a {sig} signature type"
+ )));
+ }
+ // Note: (None, Some(Proxy/GnosisSafe)) is unreachable due to auto-derivation above
+ _ => {}
+ }
+
+ let credentials = match self.credentials {
+ Some(_) if self.nonce.is_some() => {
+ return Err(Error::validation(
+ "Credentials and nonce are both set. If nonce is set, then you must not supply credentials",
+ ));
+ }
+ Some(credentials) => credentials,
+ None => {
+ inner
+ .create_or_derive_api_key(self.signer, self.nonce)
+ .await?
+ }
+ };
+
+ let state = Authenticated {
+ address: self.signer.address(),
+ credentials,
+ kind: self.kind,
+ };
+
+ #[cfg_attr(
+ not(feature = "heartbeats"),
+ expect(
+ unused_mut,
+ reason = "Modifier only needed when heartbeats feature is enabled"
+ )
+ )]
+ let mut client = Client {
+ inner: Arc::new(ClientInner {
+ state,
+ config: inner.config,
+ host: inner.host,
+ geoblock_host: inner.geoblock_host,
+ client: inner.client,
+ tick_sizes: inner.tick_sizes,
+ neg_risk: inner.neg_risk,
+ fee_rate_bps: inner.fee_rate_bps,
+ funder,
+ signature_type: self.signature_type.unwrap_or(SignatureType::Eoa),
+ salt_generator: self.salt_generator.unwrap_or(generate_seed),
+ }),
+ #[cfg(feature = "heartbeats")]
+ heartbeat_token: DroppingCancellationToken(None),
+ };
+
+ #[cfg(feature = "heartbeats")]
+ Client::>::start_heartbeats(&mut client)?;
+
+ Ok(client)
+ }
+}
+
+/// The main way for API users to interact with the Polymarket CLOB.
+///
+/// A [`Client`] can either be [`Unauthenticated`] or [`Authenticated`], that is, authenticated
+/// with a particular [`Signer`], `S`, and a particular [`Kind`], `K`. That [`Kind`] lets
+/// the client know if it's authenticating [`Normal`]ly or as a [`auth::builder::Builder`].
+///
+/// Only the allowed methods will be available for use when in a particular state, i.e. only
+/// unauthenticated methods will be visible when unauthenticated, same for authenticated/builder
+/// authenticated methods.
+///
+/// [`Client`] is thread-safe
+///
+/// Create an unauthenticated client:
+/// ```rust,no_run
+/// use polymarket_client_sdk::Result;
+/// use polymarket_client_sdk::clob::{Client, Config};
+///
+/// #[tokio::main]
+/// async fn main() -> Result<()> {
+/// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+///
+/// let ok = client.ok().await?;
+/// println!("Ok: {ok}");
+///
+/// Ok(())
+/// }
+/// ```
+///
+/// Elevate into an authenticated client:
+/// ```rust,no_run
+/// use std::str::FromStr as _;
+///
+/// use alloy::signers::Signer as _;
+/// use alloy::signers::local::LocalSigner;
+/// use polymarket_client_sdk::{POLYGON, PRIVATE_KEY_VAR};
+/// use polymarket_client_sdk::clob::{Client, Config};
+///
+/// #[tokio::main]
+/// async fn main() -> anyhow::Result<()> {
+/// let private_key = std::env::var(PRIVATE_KEY_VAR).expect("Need a private key");
+/// let signer = LocalSigner::from_str(&private_key)?.with_chain_id(Some(POLYGON));
+/// let client = Client::new("https://clob.polymarket.com", Config::default())?
+/// .authentication_builder(&signer)
+/// .authenticate()
+/// .await?;
+///
+/// let ok = client.ok().await?;
+/// println!("Ok: {ok}");
+///
+/// let api_keys = client.api_keys().await?;
+/// println!("API keys: {api_keys:?}");
+///
+/// Ok(())
+/// }
+/// ```
+#[derive(Clone, Debug)]
+pub struct Client {
+ inner: Arc>,
+ #[cfg(feature = "heartbeats")]
+ /// When the `heartbeats` feature is enabled, the authenticated [`Client`] will automatically
+ /// send heartbeats at the default cadence. See [`Config`] for more details.
+ heartbeat_token: DroppingCancellationToken,
+}
+
+#[cfg(feature = "heartbeats")]
+/// A specific wrapper type to invoke the inner [`CancellationToken`] (if it's present) to:
+/// 1. Avoid manually implementing [`Drop`] for [`Client`] which causes issues with moving values
+/// out of such a type
+/// 2. Replace the (currently non-existent) ability of specialized implementations of [`Drop`]
+///
+///
+/// This way, the inner token is expressly cancelled when [`DroppingCancellationToken`] is dropped.
+/// We also have a [`Receiver<()>`] to notify when the inner [`Client`] has been dropped so that
+/// we can avoid a race condition when calling [`Arc::into_inner`] on promotion and demotion methods.
+#[derive(Clone, Debug, Default)]
+struct DroppingCancellationToken(Option<(CancellationToken, Arc>)>);
+
+#[cfg(feature = "heartbeats")]
+impl DroppingCancellationToken {
+ /// Cancel the inner [`CancellationToken`] and wait to be notified of the relevant cleanup via
+ /// [`Receiver`]. This is primarily used by the authentication methods when promoting [`Client`]s
+ /// to ensure that we do not error when transferring ownership of [`ClientInner`].
+ pub(crate) async fn cancel_and_wait(&mut self) -> Result<()> {
+ if let Some((token, rx)) = self.0.take() {
+ return match Arc::try_unwrap(rx) {
+ // If this is the only reference, cancel the token and wait for the resources to be
+ // cleaned up.
+ Ok(inner) => {
+ token.cancel();
+ _ = inner.await;
+ Ok(())
+ }
+ // If not, _save_ the original token and receiver to re-use later if desired
+ Err(original) => {
+ *self = DroppingCancellationToken(Some((token, original)));
+ Err(Synchronization.into())
+ }
+ };
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(feature = "heartbeats")]
+impl Drop for DroppingCancellationToken {
+ fn drop(&mut self) {
+ if let Some((token, _)) = self.0.take() {
+ token.cancel();
+ }
+ }
+}
+
+impl Default for Client {
+ fn default() -> Self {
+ Client::new("https://clob.polymarket.com", Config::default())
+ .expect("Client with default endpoint should succeed")
+ }
+}
+
+/// Configuration for [`Client`]
+#[derive(Clone, Debug, Default, Builder)]
+pub struct Config {
+ /// Whether the [`Client`] will use the server time provided by Polymarket when creating auth
+ /// headers. This adds another round trip to the requests.
+ #[builder(default)]
+ use_server_time: bool,
+ /// Override for the geoblock API host. Defaults to `https://polymarket.com`.
+ /// This is primarily useful for testing.
+ #[builder(into)]
+ geoblock_host: Option,
+ #[cfg(feature = "heartbeats")]
+ #[builder(default = Duration::from_secs(5))]
+ /// How often the [`Client`] will automatically submit heartbeats. The default is five (5) seconds.
+ heartbeat_interval: Duration,
+}
+
+/// The default geoblock API host (separate from CLOB host)
+const DEFAULT_GEOBLOCK_HOST: &str = "https://polymarket.com";
+
+#[derive(Debug)]
+struct ClientInner {
+ config: Config,
+ /// The current [`State`] of this client
+ state: S,
+ /// The [`Url`] against which `client` is making requests.
+ host: Url,
+ /// The [`Url`] for the geoblock API endpoint.
+ geoblock_host: Url,
+ /// The inner [`ReqwestClient`] used to make requests to `host`.
+ client: ReqwestClient,
+ /// Local cache of [`TickSize`] per token ID
+ tick_sizes: DashMap,
+ /// Local cache representing whether this token is part of a `neg_risk` market
+ neg_risk: DashMap,
+ /// Local cache representing the fee rate in basis points per token ID
+ fee_rate_bps: DashMap,
+ /// The funder for this [`ClientInner`]. If funder is present, then `signature_type` cannot
+ /// be [`SignatureType::Eoa`]. Conversely, if funder is absent, then `signature_type` cannot be
+ /// [`SignatureType::Proxy`] or [`SignatureType::GnosisSafe`].
+ funder: Option,
+ /// The signature type for this [`ClientInner`]. Defaults to [`SignatureType::Eoa`]
+ signature_type: SignatureType,
+ /// The salt/seed generator for use in creating [`SignableOrder`]s
+ salt_generator: fn() -> u64,
+}
+
+impl ClientInner {
+ pub async fn server_time(&self) -> Result {
+ let request = self
+ .client
+ .request(Method::GET, format!("{}time", self.host))
+ .build()?;
+
+ crate::request(&self.client, request, None).await
+ }
+}
+
+impl ClientInner {
+ pub async fn create_api_key(
+ &self,
+ signer: &S,
+ nonce: Option,
+ ) -> Result {
+ let request = self
+ .client
+ .request(Method::POST, format!("{}auth/api-key", self.host))
+ .build()?;
+ let headers = self.create_headers(signer, nonce).await?;
+
+ crate::request(&self.client, request, Some(headers)).await
+ }
+
+ pub async fn derive_api_key(
+ &self,
+ signer: &S,
+ nonce: Option,
+ ) -> Result {
+ let request = self
+ .client
+ .request(Method::GET, format!("{}auth/derive-api-key", self.host))
+ .build()?;
+ let headers = self.create_headers(signer, nonce).await?;
+
+ crate::request(&self.client, request, Some(headers)).await
+ }
+
+ async fn create_or_derive_api_key(
+ &self,
+ signer: &S,
+ nonce: Option,
+ ) -> Result {
+ match self.create_api_key(signer, nonce).await {
+ Ok(creds) => Ok(creds),
+ Err(err) if err.kind() == ErrorKind::Status => {
+ // Only fall back to derive_api_key for HTTP status errors (server responded
+ // with an error, e.g., key already exists). Propagate network/internal errors.
+ self.derive_api_key(signer, nonce).await
+ }
+ Err(err) => Err(err),
+ }
+ }
+
+ async fn create_headers(&self, signer: &S, nonce: Option) -> Result {
+ let chain_id = signer.chain_id().ok_or(Error::validation(
+ "Chain id not set, be sure to provide one on the signer",
+ ))?;
+
+ let timestamp = if self.config.use_server_time {
+ self.server_time().await?
+ } else {
+ Utc::now().timestamp()
+ };
+
+ auth::l1::create_headers(signer, chain_id, timestamp, nonce).await
+ }
+}
+
+impl Client {
+ /// Returns the CLOB API host URL.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use polymarket_client_sdk::clob::{Client, Config};
+ /// # fn main() -> Result<(), Box> {
+ /// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ /// println!("Host: {}", client.host());
+ /// # Ok(())
+ /// # }
+ /// ```
+ #[must_use]
+ pub fn host(&self) -> &Url {
+ &self.inner.host
+ }
+
+ /// Invalidates all internal caches (tick sizes, neg risk flags, and fee rates).
+ ///
+ /// This method clears the cached market configuration data, forcing subsequent
+ /// requests to fetch fresh data from the API. Use this when you suspect
+ /// cached data may be stale.
+ pub fn invalidate_internal_caches(&self) {
+ self.inner.tick_sizes.clear();
+ self.inner.fee_rate_bps.clear();
+ self.inner.neg_risk.clear();
+ }
+
+ /// Pre-populates the tick size cache for a token, avoiding the HTTP call.
+ ///
+ /// Use this when you already have the tick size data from another source
+ /// (e.g., cached locally or retrieved from a different API).
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use polymarket_client_sdk::clob::{Client, Config, types::TickSize};
+ /// # fn main() -> Result<(), Box> {
+ /// use polymarket_client_sdk::types::U256;
+ ///
+ /// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ /// client.set_tick_size(U256::ZERO, TickSize::Hundredth);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn set_tick_size(&self, token_id: U256, tick_size: TickSize) {
+ self.inner.tick_sizes.insert(token_id, tick_size);
+ }
+
+ /// Pre-populates the neg risk cache for a token, avoiding the HTTP call.
+ ///
+ /// Use this when you already have the neg risk data from another source
+ /// (e.g., cached locally or retrieved from a different API).
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use polymarket_client_sdk::clob::{Client, Config};
+ /// # fn main() -> Result<(), Box> {
+ /// use polymarket_client_sdk::types::U256;
+ ///
+ /// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ /// client.set_neg_risk(U256::ZERO, true);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn set_neg_risk(&self, token_id: U256, neg_risk: bool) {
+ self.inner.neg_risk.insert(token_id, neg_risk);
+ }
+
+ /// Pre-populates the fee rate cache for a token, avoiding the HTTP call.
+ ///
+ /// Use this when you already have the fee rate data from another source
+ /// (e.g., cached locally or retrieved from a different API). The fee rate
+ /// is specified in basis points (bps), where 100 bps = 1%.
+ ///
+ /// # Example
+ ///
+ /// ```no_run
+ /// # use polymarket_client_sdk::clob::{Client, Config};
+ /// # fn main() -> Result<(), Box> {
+ /// use polymarket_client_sdk::types::U256;
+ ///
+ /// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ /// client.set_fee_rate_bps(U256::ZERO, 10); // 0.10% fee
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn set_fee_rate_bps(&self, token_id: U256, fee_rate_bps: u32) {
+ self.inner.fee_rate_bps.insert(token_id, fee_rate_bps);
+ }
+
+ /// Checks if the CLOB API is healthy and operational.
+ ///
+ /// Returns "OK" if the API is functioning properly. This method is useful
+ /// for health checks and monitoring the API status.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the network request fails or the API is unreachable.
+ pub async fn ok(&self) -> Result {
+ let request = self
+ .client()
+ .request(Method::GET, self.host().to_owned())
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Returns the current server timestamp in milliseconds since Unix epoch.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn server_time(&self) -> Result {
+ self.inner.server_time().await
+ }
+
+ /// Retrieves the midpoint price for a single market outcome token.
+ ///
+ /// The midpoint is the average of the best bid and best ask prices,
+ /// calculated as `(best_bid + best_ask) / 2`. This represents a fair
+ /// market price estimate for the token.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn midpoint(&self, request: &MidpointRequest) -> Result {
+ let params = request.query_params(None);
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}midpoint{params}", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves midpoint prices for multiple market outcome tokens in a single request.
+ ///
+ /// This is the batch version of [`Self::midpoint`]. Returns midpoint prices
+ /// for all requested tokens, allowing efficient bulk price queries.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or any token ID is invalid.
+ pub async fn midpoints(&self, requests: &[MidpointRequest]) -> Result {
+ let request = self
+ .client()
+ .request(Method::POST, format!("{}midpoints", self.host()))
+ .json(requests)
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves the current price for a market outcome token on a specific side.
+ ///
+ /// Returns the best available price for buying (BUY side) or selling (SELL side)
+ /// the specified token. This reflects the actual executable price on the orderbook.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn price(&self, request: &PriceRequest) -> Result {
+ let params = request.query_params(None);
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}price{params}", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves prices for multiple market outcome tokens on their specific sides.
+ ///
+ /// This is the batch version of [`Self::price`]. Allows querying prices
+ /// for many tokens at once, with each request specifying its own side (BUY or SELL).
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or any token ID is invalid.
+ pub async fn prices(&self, requests: &[PriceRequest]) -> Result {
+ let request = self
+ .client()
+ .request(Method::POST, format!("{}prices", self.host()))
+ .json(requests)
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves prices for all available market outcome tokens.
+ ///
+ /// Returns the current best bid and ask prices for every active token
+ /// in the system. This is useful for getting a complete market overview.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn all_prices(&self) -> Result {
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}prices", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves historical price data for a market.
+ ///
+ /// Returns time-series price data over a specified time range or interval.
+ /// The `fidelity` parameter controls the granularity of data points returned.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the market ID is invalid.
+ pub async fn price_history(
+ &self,
+ request: &PriceHistoryRequest,
+ ) -> Result {
+ let params = request.query_params(None);
+ let req = self.client().request(
+ Method::GET,
+ format!("{}prices-history{params}", self.host()),
+ );
+
+ crate::request(&self.inner.client, req.build()?, None).await
+ }
+
+ /// Retrieves the bid-ask spread for a single market outcome token.
+ ///
+ /// The spread is the difference between the best ask price and the best bid price,
+ /// representing the cost of immediate execution. A smaller spread indicates higher
+ /// liquidity and more efficient markets.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn spread(&self, request: &SpreadRequest) -> Result {
+ let params = request.query_params(None);
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}spread{params}", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves bid-ask spreads for multiple market outcome tokens.
+ ///
+ /// This is the batch version of [`Self::spread`], allowing efficient
+ /// retrieval of spread data for many tokens simultaneously.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or any token ID is invalid.
+ pub async fn spreads(&self, requests: &[SpreadRequest]) -> Result {
+ let request = self
+ .client()
+ .request(Method::POST, format!("{}spreads", self.host()))
+ .json(requests)
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves the minimum tick size for a market outcome token.
+ ///
+ /// The tick size defines the minimum price increment for orders on this token.
+ /// Results are cached internally to reduce API calls. For example, a tick size
+ /// of 0.01 means prices must be in increments of $0.01.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn tick_size(&self, token_id: U256) -> Result {
+ if let Some(tick_size) = self.inner.tick_sizes.get(&token_id) {
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, tick_size = ?tick_size.value(), "cache hit: tick_size");
+ return Ok(TickSizeResponse {
+ minimum_tick_size: *tick_size,
+ });
+ }
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cache miss: tick_size");
+
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}tick-size", self.host()))
+ .query(&[("token_id", token_id.to_string())])
+ .build()?;
+
+ let response =
+ crate::request::(&self.inner.client, request, None).await?;
+
+ self.inner
+ .tick_sizes
+ .insert(token_id, response.minimum_tick_size);
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cached tick_size");
+
+ Ok(response)
+ }
+
+ /// Checks if a market outcome token uses the negative risk (`NegRisk`) adapter.
+ ///
+ /// `NegRisk` markets have special settlement logic where one outcome is
+ /// "negative" (representing an event not happening). Returns `true` if the
+ /// token requires the `NegRisk` adapter contract. Results are cached internally.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn neg_risk(&self, token_id: U256) -> Result {
+ if let Some(neg_risk) = self.inner.neg_risk.get(&token_id) {
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, neg_risk = *neg_risk, "cache hit: neg_risk");
+ return Ok(NegRiskResponse {
+ neg_risk: *neg_risk,
+ });
+ }
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cache miss: neg_risk");
+
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}neg-risk", self.host()))
+ .query(&[("token_id", token_id.to_string())])
+ .build()?;
+
+ let response = crate::request::(&self.inner.client, request, None).await?;
+
+ self.inner.neg_risk.insert(token_id, response.neg_risk);
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cached neg_risk");
+
+ Ok(response)
+ }
+
+ /// Retrieves the trading fee rate for a market outcome token.
+ ///
+ /// Returns the fee rate in basis points (bps) charged on trades for this token.
+ /// For example, 10 bps = 0.10% fee. Results are cached internally to reduce API calls.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn fee_rate_bps(&self, token_id: U256) -> Result {
+ if let Some(base_fee) = self.inner.fee_rate_bps.get(&token_id) {
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, base_fee = *base_fee, "cache hit: fee_rate_bps");
+ return Ok(FeeRateResponse {
+ base_fee: *base_fee,
+ });
+ }
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cache miss: fee_rate_bps");
+
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}fee-rate", self.host()))
+ .query(&[("token_id", token_id.to_string())])
+ .build()?;
+
+ let response = crate::request::(&self.inner.client, request, None).await?;
+
+ self.inner.fee_rate_bps.insert(token_id, response.base_fee);
+
+ #[cfg(feature = "tracing")]
+ tracing::trace!(token_id = %token_id, "cached fee_rate_bps");
+
+ Ok(response)
+ }
+
+ /// Checks if the current IP address is geoblocked from accessing Polymarket.
+ ///
+ /// This method queries the Polymarket geoblock endpoint to determine if access
+ /// is restricted based on the caller's IP address and geographic location.
+ ///
+ /// # Returns
+ ///
+ /// Returns `Ok(GeoblockResponse)` containing the geoblock status and location info.
+ /// Check the `blocked` field to determine if access is restricted.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the HTTP request fails or the response cannot be parsed.
+ ///
+ /// # Example
+ ///
+ /// ```rust,no_run
+ /// use polymarket_client_sdk::clob::{Client, Config};
+ /// use polymarket_client_sdk::error::{Kind, Geoblock};
+ ///
+ /// #[tokio::main]
+ /// async fn main() -> anyhow::Result<()> {
+ /// let client = Client::new("https://clob.polymarket.com", Config::default())?;
+ ///
+ /// let geoblock = client.check_geoblock().await?;
+ ///
+ /// if geoblock.blocked {
+ /// eprintln!(
+ /// "Trading not available in {}, {}",
+ /// geoblock.country, geoblock.region
+ /// );
+ /// // Optionally convert to an error:
+ /// // return Err(Geoblock {
+ /// // ip: geoblock.ip,
+ /// // country: geoblock.country,
+ /// // region: geoblock.region,
+ /// // }.into());
+ /// } else {
+ /// println!("Trading available from IP: {}", geoblock.ip);
+ /// }
+ ///
+ /// Ok(())
+ /// }
+ /// ```
+ pub async fn check_geoblock(&self) -> Result {
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}api/geoblock", self.inner.geoblock_host),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves the full orderbook for a market outcome token.
+ ///
+ /// Returns all active bids and asks at various price levels, showing
+ /// the depth of liquidity available in the market. This includes the
+ /// best bid, best ask, and the full order depth on both sides.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn order_book(
+ &self,
+ request: &OrderBookSummaryRequest,
+ ) -> Result {
+ let params = request.query_params(None);
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}book{params}", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves orderbooks for multiple market outcome tokens.
+ ///
+ /// This is the batch version of [`Self::order_book`], allowing efficient
+ /// retrieval of orderbook data for many tokens in a single request.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or any token ID is invalid.
+ pub async fn order_books(
+ &self,
+ requests: &[OrderBookSummaryRequest],
+ ) -> Result> {
+ let request = self
+ .client()
+ .request(Method::POST, format!("{}books", self.host()))
+ .json(requests)
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves the price of the most recent trade for a market outcome token.
+ ///
+ /// Returns the last executed trade price, which represents the most recent
+ /// market consensus price. This is useful for tracking real-time price movements.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the token ID is invalid.
+ pub async fn last_trade_price(
+ &self,
+ request: &LastTradePriceRequest,
+ ) -> Result {
+ let params = request.query_params(None);
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}last-trade-price{params}", self.host()),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves the last trade prices for multiple market outcome tokens.
+ ///
+ /// This is the batch version of [`Self::last_trade_price`], returning
+ /// the most recent executed trade price for each requested token.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or any token ID is invalid.
+ pub async fn last_trades_prices(
+ &self,
+ token_ids: &[LastTradePriceRequest],
+ ) -> Result> {
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}last-trades-prices", self.host()))
+ .json(token_ids)
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves detailed information for a single market by condition ID.
+ ///
+ /// Returns comprehensive market data including all outcome tokens, current prices,
+ /// volume, and market metadata. The condition ID uniquely identifies the market.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails or the condition ID is invalid.
+ pub async fn market(&self, condition_id: &str) -> Result {
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}markets/{condition_id}", self.host()),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves a page of all active markets.
+ ///
+ /// Returns a paginated list of all markets with their full details.
+ /// Use the `next_cursor` from the response to fetch subsequent pages.
+ /// Useful for iterating through all available markets.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn markets(&self, next_cursor: Option) -> Result> {
+ let cursor = next_cursor.map_or(String::new(), |c| format!("?next_cursor={c}"));
+ let request = self
+ .client()
+ .request(Method::GET, format!("{}markets{cursor}", self.host()))
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves a page of sampling markets.
+ ///
+ /// Returns a paginated list of markets designated for the sampling program,
+ /// where market makers can earn rewards. Use the `next_cursor` from the
+ /// response to fetch subsequent pages.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn sampling_markets(
+ &self,
+ next_cursor: Option,
+ ) -> Result> {
+ let cursor = next_cursor.map_or(String::new(), |c| format!("?next_cursor={c}"));
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}sampling-markets{cursor}", self.host()),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves a page of simplified market data.
+ ///
+ /// Returns a paginated list of markets with reduced detail, providing only
+ /// essential information for faster queries and lower bandwidth usage.
+ /// Use the `next_cursor` from the response to fetch subsequent pages.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn simplified_markets(
+ &self,
+ next_cursor: Option,
+ ) -> Result> {
+ let cursor = next_cursor.map_or(String::new(), |c| format!("?next_cursor={c}"));
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}simplified-markets{cursor}", self.host()),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Retrieves a page of simplified sampling market data.
+ ///
+ /// Returns a paginated list of sampling program markets with reduced detail.
+ /// Combines the efficiency of simplified queries with the filtering of
+ /// sampling markets. Use the `next_cursor` from the response to fetch subsequent pages.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the request fails.
+ pub async fn sampling_simplified_markets(
+ &self,
+ next_cursor: Option,
+ ) -> Result> {
+ let cursor = next_cursor.map_or(String::new(), |c| format!("?next_cursor={c}"));
+ let request = self
+ .client()
+ .request(
+ Method::GET,
+ format!("{}sampling-simplified-markets{cursor}", self.host()),
+ )
+ .build()?;
+
+ crate::request(&self.inner.client, request, None).await
+ }
+
+ /// Returns a stream of results, using `self` to repeatedly invoke the provided closure,
+ /// `call`, which takes the next cursor to query against. Each `call` returns a future
+ /// that returns a [`Page`]. Each page is flattened into the underlying data in the stream.
+ pub fn stream_data<'client, Call, Fut, Data>(
+ &'client self,
+ call: Call,
+ ) -> impl Stream- > + 'client
+ where
+ Call: Fn(&'client Client
, Option) -> Fut + 'client,
+ Fut: Future