Rust Prediction Market API: Build a Typed, Cross-Venue Client

Rust Prediction Market API: Build a Typed, Cross-Venue Client

Rust Prediction Market API: Build a Typed, Cross-Venue Client

How to build a Rust client for prediction market data: native venue SDKs, a typed unified API layer, WebSocket handling, backoff and tests for bots.

Rust Prediction Market API: Build a Typed, Cross-Venue Client

There are two ways to reach prediction market data from Rust. You can use a venue’s own SDK and write one integration per exchange, or you can put a unified data API behind a single typed client and let normalization happen upstream. For anything that reads more than one venue, the second path is usually less code and fewer failure modes. This guide covers both, with the architecture, types and failure handling a Rust bot needs.

AssymetrixUnify Prediction Market DataAssymetrix brings Polymarket, Kalshi, and Limitless data together through one API integration for your Rust client.Explore the Data API

TL;DR

  • Native Rust SDKs exist for trading on individual venues. They are the right tool for order signing and execution on that venue, and the wrong tool for cross-venue research.

  • Reading three venues means three schemas, three auth models and three reconnect behaviors. Push that work into one normalized API and model it once in Rust.

  • Use serde enums with explicit fallbacks, bounded channels and a sequence-aware order book, so a bad message never panics a trading loop.

  • Keep data and execution separate: a data client you can replay in tests, and an execution client you can pause.

  • Our Data API covers Polymarket, Kalshi and Limitless through one integration.

Table of Contents

  • Why Rust for prediction market work

  • Two integration paths

  • Architecture: split data from execution

  • Modeling the schema in Rust

  • Streaming: WebSocket handling that survives a week

  • Cross-venue logic: where canonical IDs pay off

  • Backtesting with the same types

  • Production checklist

  • Our perspective: normalize upstream, not in your bot

  • Getting data into your Rust client

  • FAQ

  • Sources

Why Rust for prediction market work

Most prediction market tutorials use Python, and for notebooks that is fine. Rust earns its place in three situations.

Long-running collectors. A process that holds dozens of WebSocket subscriptions for weeks needs predictable memory and no garbage collection pauses. Rust gives you that without much tuning.

Latency-sensitive loops. Cross-venue price gaps close quickly. If your bot compares two books and decides to act, deserialization and book maintenance should cost microseconds, not milliseconds. Our write-up on latency arbitrage in prediction markets explains why that window matters.

Correctness under load. Prediction market payloads mix strings, decimals-as-strings, nullable fields and venue-specific enums. Rust’s type system forces you to decide what happens with each of them at compile time instead of in production at 3 a.m.

If you are earlier in the process and still deciding on a language, our guide to Python prediction market data covers the same ground for research workflows. Many teams prototype there and move the hot path to Rust.


Why Rust for prediction market work — overview diagram

Two integration paths

Path 1: the venue’s own Rust SDK

Polymarket maintains an open-source Rust client, rs-clob-client, aimed mainly at the central limit order book. Its README describes strongly typed request builders and authenticated endpoints, and the crate is modular, with optional feature flags such as ws and data that you enable in Cargo.toml. Installation is a standard cargo add polymarket-client-sdk.

That is a good fit when you trade on Polymarket only. You get signing, order types and the venue’s own market discovery in one dependency.

The limits show up when you add a second venue:

  • Each venue has its own identifiers for the same real-world event. There is no shared key.

  • Auth differs. One model signs with a wallet, another with API keys and request signatures.

  • Price and size conventions differ: probabilities versus cents, fractional versus whole contracts, different tick sizes.

  • Reconnect, heartbeat and snapshot semantics differ, so your order book code ends up forked per venue.

A cross-venue strategy written this way becomes an adapter layer you maintain forever. Teams building arbitrage bots on top of raw venue feeds typically spend more time on that layer than on the strategy.

Path 2: one normalized API behind one Rust client

The alternative is to read from a venue-neutral API and model its schema once. Our Data API normalizes Polymarket, Kalshi and Limitless into one schema with canonical identifiers, so the same event has the same key whichever venue lists it. It is backed by roughly 1.5 TB of historical data and close to a billion rows of trading activity, which means the code you use for live data also runs against history. The design behind this is described in Canonical IDs First.


Comparison of venue SDK and normalized API

The trade-off is plain. A unified API is a data layer, not an order router. You read through it and, where your strategy requires, execute through each venue’s own SDK. That separation is healthy, and the next section shows why.

Architecture: split data from execution

We recommend three crates in a workspace, even for a small bot.

Crate

Responsibility

Depends on

pm-types

Canonical structs and enums, no I/O

serde, rust_decimal

pm-data

REST and WebSocket client, book state, reconnect logic

pm-types, reqwest, tokio, tokio-tungstenite

pm-exec

Venue SDKs, order signing, risk limits

pm-types, venue crates

The strategy binary depends on all three and talks to them through traits. The payoff is testing: pm-data can be swapped for a replay source that streams recorded events from disk, so you can run last month’s session against today’s strategy code without touching the network.

Modeling the schema in Rust

The examples below are a sketch. Field names and endpoints are placeholders: take the real ones from the API reference for whichever service you use.

Use decimals, not floats

Prices in a binary market live between 0 and 1 (or 1 and 99 cents on some venues). Accumulating float error across thousands of fills is how a backtest drifts from reality. Parse prices into rust_decimal::Decimal, and convert to a single canonical unit at the boundary.

use rust_decimal::Decimal;
use serde::Deserialize;

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Venue {
    Polymarket,
    Kalshi,
    Limitless,
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Level {
    #[serde(with = "rust_decimal::serde::str")]
    pub price: Decimal,
    #[serde(with = "rust_decimal::serde::str")]
    pub size: Decimal,
}

#[derive(Debug, Clone, Deserialize)]
pub struct BookUpdate {
    pub market_id: String, // canonical ID, shared across venues
    pub venue: Venue,
    pub seq: u64,
    pub bids: Vec<Level>,
    pub asks: Vec<Level>,
}
use rust_decimal::Decimal;
use serde::Deserialize;

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Venue {
    Polymarket,
    Kalshi,
    Limitless,
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Level {
    #[serde(with = "rust_decimal::serde::str")]
    pub price: Decimal,
    #[serde(with = "rust_decimal::serde::str")]
    pub size: Decimal,
}

#[derive(Debug, Clone, Deserialize)]
pub struct BookUpdate {
    pub market_id: String, // canonical ID, shared across venues
    pub venue: Venue,
    pub seq: u64,
    pub bids: Vec<Level>,
    pub asks: Vec<Level>,
}

Two details matter here. The #[serde(other)] variant means a new venue added upstream deserializes as Unknown instead of killing your stream. And the canonical market_id is the field your cross-venue logic joins on.

Fail soft on unknown messages

Wrap inbound frames in a tagged enum with a catch-all, and count what you cannot parse.

#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Frame {
    Snapshot(BookUpdate),
    Delta(BookUpdate),
    Heartbeat { ts: u64 },
    #[serde(other)]
    Other,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Frame {
    Snapshot(BookUpdate),
    Delta(BookUpdate),
    Heartbeat { ts: u64 },
    #[serde(other)]
    Other,
}

A rising Other counter is an alert, not a crash. It tells you the schema moved before it costs you money.

Streaming: WebSocket handling that survives a week

Polling REST endpoints works for research. For anything live you want a stream. The patterns below apply to any feed, and our deeper guides on WebSocket prediction market data and the real-time WebSocket API cover the protocol details.

The reader task

Run one task that does nothing but read frames and push them into a bounded channel.

use tokio::sync::mpsc;
use futures_util::StreamExt;
use tokio_tungstenite::{connect_async, tungstenite::Message};

async fn read_loop(url: &str, tx: mpsc::Sender<Frame>) -> anyhow::Result<()> {
    let (ws, _) = connect_async(url).await?;
    let (_, mut rx) = ws.split();
    while let Some(msg) = rx.next().await {
        if let Message::Text(t) = msg? {
            let frame = serde_json::from_str::<Frame>(&t).unwrap_or(Frame::Other);
            if tx.send(frame).await.is_err() {
                break; // consumer gone
            }
        }
    }
    Ok(())
}
use tokio::sync::mpsc;
use futures_util::StreamExt;
use tokio_tungstenite::{connect_async, tungstenite::Message};

async fn read_loop(url: &str, tx: mpsc::Sender<Frame>) -> anyhow::Result<()> {
    let (ws, _) = connect_async(url).await?;
    let (_, mut rx) = ws.split();
    while let Some(msg) = rx.next().await {
        if let Message::Text(t) = msg? {
            let frame = serde_json::from_str::<Frame>(&t).unwrap_or(Frame::Other);
            if tx.send(frame).await.is_err() {
                break; // consumer gone
            }
        }
    }
    Ok(())
}

Use a bounded mpsc channel on purpose. If your strategy cannot keep up, you want backpressure and a metric, not an unbounded queue that eats memory until the OOM killer ends the process.

Reconnect with jittered backoff

Wrap read_loop in a supervisor. On any error, wait, then reconnect. Double the delay up to a cap, add random jitter so a fleet of collectors does not reconnect in lockstep, and reset the delay after a connection has stayed healthy for a minute.

On every reconnect, discard local book state and wait for a fresh snapshot. This is the single most common source of silent errors in prediction market bots.

Sequence numbers and book drift

A book built from deltas is only correct if you applied every delta in order. Track the seq of the last update per market. If the next one is not last + 1, you have a gap: mark the book stale, stop quoting from it, and resubscribe for a snapshot. We wrote a full treatment of this failure mode in fixing order book drift, and the orderbook data integration guide covers how to store depth for later analysis.

fn apply(book: &mut Book, u: &BookUpdate) -> Result<(), Gap> {
    if u.seq != book.seq + 1 {
        return Err(Gap { expected: book.seq + 1, got: u.seq });
    }
    book.merge(u);
    book.seq = u.seq;
    Ok(())
}
fn apply(book: &mut Book, u: &BookUpdate) -> Result<(), Gap> {
    if u.seq != book.seq + 1 {
        return Err(Gap { expected: book.seq + 1, got: u.seq });
    }
    book.merge(u);
    book.seq = u.seq;
    Ok(())
}

Cross-venue logic: where canonical IDs pay off

The reason to normalize is the join. With a canonical ID you can hold a HashMap<CanonicalId, HashMap<Venue, Book>> and ask one question per event: what is the best executable price on each side, across venues, after fees?

A minimal gap check looks like this:

  1. For each event with books on at least two venues, take the best ask for YES on one venue and the best ask for NO on another.

  2. Add the fee model for each venue.

  3. If the combined cost is under 1.00 by more than your threshold, you have a candidate.

  4. Check depth. A candidate that exists at 5 contracts and vanishes at 500 is not a signal.

  5. Check book freshness. Reject any book whose last update is older than your staleness limit.

Step 5 is where most paper-profitable strategies die. Our arbitrage strategy guide walks through the economics, and execution and slippage tracking shows how to measure the gap between the price you saw and the price you got.

Resolution rules deserve a line of their own. Two markets that look identical can resolve on different sources or different edge cases. A canonical ID tells you the venues list the same event, not that the contracts pay out identically. Read the rules for both before you treat a pair as a hedge.

Backtesting with the same types

Because pm-types has no I/O, a replay source can produce the same Frame values from stored history. The loop is simple: read events in timestamp order, feed them through the same apply function, and let the strategy react.

Three rules keep a backtest honest:

  • Replay by exchange timestamp, then by sequence. Never by arrival order, which is a property of your collector, not the market.

  • Model latency. Delay your strategy’s orders by a realistic round trip before matching them against the replayed book.

  • Count partial fills. If your order is larger than displayed depth, the fill is partial at worse prices.

Long history is what makes this useful. Strategies tuned on a few weeks of data overfit quickly; the historical and real-time feed guide describes how to pull depth across a long window, and our piece on market microstructure covers what that data shows about spreads and liquidity.

Production checklist

Concern

What to do

Secrets

Load API keys from the environment or a secrets manager, never from source. Wrap them in a type that redacts on Debug.

Observability

Export counters for frames received, parse failures, gaps, reconnects and staleness per market.

Rate limits

Put a token bucket in front of every REST client. Honor 429 responses with backoff.

Kill switch

A flag the execution crate checks before every order, flippable without a deploy.

Clock

Run NTP. Compare your clock against exchange timestamps and alert on drift.

Idempotency

Generate client order IDs yourself so a retry cannot double-submit.

Panics

Set panic = "abort" under a supervisor, or catch at task boundaries. Never let one market’s bad data stop the rest.

Our perspective: normalize upstream, not in your bot

We build the data layer, so we have a bias, and we will state it: the schema mapping between venues is the least interesting part of your system and the most expensive to keep correct. Every hour spent reconciling how three venues name the same event is an hour not spent on the signal.

Our position is that data infrastructure should be venue-neutral. A feed owned by an exchange has reasons to favor that exchange’s view of the market. A neutral layer can put Polymarket, Kalshi and Limitless side by side with the same identifiers, the same units and the same history, and leave the judgment to you. That is why we built canonical IDs first, and why on-chain coverage matters for the venues that settle on-chain: it lets you see wallet behavior, not just prices.

For Rust developers specifically, the practical advice is this. Keep your types small and strict. Treat the network as hostile and the schema as something that will change. Make data replayable so every bug becomes a test. And keep execution in a separate crate with a kill switch, because the best data layer in the world does not protect you from your own strategy code.

— Dean

Getting data into your Rust client

If you want to skip the per-venue adapters, the Assymetrix Data API gives you REST and WebSocket access to cross-venue prediction market data through one integration, with free and paid tiers and academic access for researchers. Pricing is not published on the page, so check it directly before you plan capacity. For signals built on top of the raw feed, such as Smart Money wallet tracking and divergence analysis, see prediction market intelligence.

FAQ

Is there an official Rust SDK for prediction markets?

Polymarket publishes an open-source Rust client, rs-clob-client, focused on its order book API with optional WebSocket and data features. Other venues may rely on REST documentation and community crates, so check each venue’s developer pages before depending on one. For reading several venues, a unified data API with your own thin typed client is often simpler.

Should I use REST or WebSocket from Rust?

Use REST for market discovery, metadata and historical pulls. Use WebSocket for live order books and trades. A common setup loads a snapshot over REST, then applies streamed deltas, and re-snapshots whenever a sequence gap appears.

How do I avoid floating-point errors with prices?

Parse every price and size into a decimal type such as rust_decimal::Decimal at the deserialization boundary, and convert venues to one canonical unit there. Never compare prices as f64 for equality, and never accumulate PnL in floats.

Can I run the same code for backtests and live trading?

Yes, if your strategy depends on a trait that yields typed frames, not on a socket. A live implementation reads from the network and a replay implementation reads stored history. The strategy cannot tell them apart, which is the point.

Sources

Rust Prediction Market API: Build a Typed, Cross-Venue Client

There are two ways to reach prediction market data from Rust. You can use a venue’s own SDK and write one integration per exchange, or you can put a unified data API behind a single typed client and let normalization happen upstream. For anything that reads more than one venue, the second path is usually less code and fewer failure modes. This guide covers both, with the architecture, types and failure handling a Rust bot needs.

AssymetrixUnify Prediction Market DataAssymetrix brings Polymarket, Kalshi, and Limitless data together through one API integration for your Rust client.Explore the Data API

TL;DR

  • Native Rust SDKs exist for trading on individual venues. They are the right tool for order signing and execution on that venue, and the wrong tool for cross-venue research.

  • Reading three venues means three schemas, three auth models and three reconnect behaviors. Push that work into one normalized API and model it once in Rust.

  • Use serde enums with explicit fallbacks, bounded channels and a sequence-aware order book, so a bad message never panics a trading loop.

  • Keep data and execution separate: a data client you can replay in tests, and an execution client you can pause.

  • Our Data API covers Polymarket, Kalshi and Limitless through one integration.

Table of Contents

  • Why Rust for prediction market work

  • Two integration paths

  • Architecture: split data from execution

  • Modeling the schema in Rust

  • Streaming: WebSocket handling that survives a week

  • Cross-venue logic: where canonical IDs pay off

  • Backtesting with the same types

  • Production checklist

  • Our perspective: normalize upstream, not in your bot

  • Getting data into your Rust client

  • FAQ

  • Sources

Why Rust for prediction market work

Most prediction market tutorials use Python, and for notebooks that is fine. Rust earns its place in three situations.

Long-running collectors. A process that holds dozens of WebSocket subscriptions for weeks needs predictable memory and no garbage collection pauses. Rust gives you that without much tuning.

Latency-sensitive loops. Cross-venue price gaps close quickly. If your bot compares two books and decides to act, deserialization and book maintenance should cost microseconds, not milliseconds. Our write-up on latency arbitrage in prediction markets explains why that window matters.

Correctness under load. Prediction market payloads mix strings, decimals-as-strings, nullable fields and venue-specific enums. Rust’s type system forces you to decide what happens with each of them at compile time instead of in production at 3 a.m.

If you are earlier in the process and still deciding on a language, our guide to Python prediction market data covers the same ground for research workflows. Many teams prototype there and move the hot path to Rust.


Why Rust for prediction market work — overview diagram

Two integration paths

Path 1: the venue’s own Rust SDK

Polymarket maintains an open-source Rust client, rs-clob-client, aimed mainly at the central limit order book. Its README describes strongly typed request builders and authenticated endpoints, and the crate is modular, with optional feature flags such as ws and data that you enable in Cargo.toml. Installation is a standard cargo add polymarket-client-sdk.

That is a good fit when you trade on Polymarket only. You get signing, order types and the venue’s own market discovery in one dependency.

The limits show up when you add a second venue:

  • Each venue has its own identifiers for the same real-world event. There is no shared key.

  • Auth differs. One model signs with a wallet, another with API keys and request signatures.

  • Price and size conventions differ: probabilities versus cents, fractional versus whole contracts, different tick sizes.

  • Reconnect, heartbeat and snapshot semantics differ, so your order book code ends up forked per venue.

A cross-venue strategy written this way becomes an adapter layer you maintain forever. Teams building arbitrage bots on top of raw venue feeds typically spend more time on that layer than on the strategy.

Path 2: one normalized API behind one Rust client

The alternative is to read from a venue-neutral API and model its schema once. Our Data API normalizes Polymarket, Kalshi and Limitless into one schema with canonical identifiers, so the same event has the same key whichever venue lists it. It is backed by roughly 1.5 TB of historical data and close to a billion rows of trading activity, which means the code you use for live data also runs against history. The design behind this is described in Canonical IDs First.


Comparison of venue SDK and normalized API

The trade-off is plain. A unified API is a data layer, not an order router. You read through it and, where your strategy requires, execute through each venue’s own SDK. That separation is healthy, and the next section shows why.

Architecture: split data from execution

We recommend three crates in a workspace, even for a small bot.

Crate

Responsibility

Depends on

pm-types

Canonical structs and enums, no I/O

serde, rust_decimal

pm-data

REST and WebSocket client, book state, reconnect logic

pm-types, reqwest, tokio, tokio-tungstenite

pm-exec

Venue SDKs, order signing, risk limits

pm-types, venue crates

The strategy binary depends on all three and talks to them through traits. The payoff is testing: pm-data can be swapped for a replay source that streams recorded events from disk, so you can run last month’s session against today’s strategy code without touching the network.

Modeling the schema in Rust

The examples below are a sketch. Field names and endpoints are placeholders: take the real ones from the API reference for whichever service you use.

Use decimals, not floats

Prices in a binary market live between 0 and 1 (or 1 and 99 cents on some venues). Accumulating float error across thousands of fills is how a backtest drifts from reality. Parse prices into rust_decimal::Decimal, and convert to a single canonical unit at the boundary.

use rust_decimal::Decimal;
use serde::Deserialize;

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Venue {
    Polymarket,
    Kalshi,
    Limitless,
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Level {
    #[serde(with = "rust_decimal::serde::str")]
    pub price: Decimal,
    #[serde(with = "rust_decimal::serde::str")]
    pub size: Decimal,
}

#[derive(Debug, Clone, Deserialize)]
pub struct BookUpdate {
    pub market_id: String, // canonical ID, shared across venues
    pub venue: Venue,
    pub seq: u64,
    pub bids: Vec<Level>,
    pub asks: Vec<Level>,
}

Two details matter here. The #[serde(other)] variant means a new venue added upstream deserializes as Unknown instead of killing your stream. And the canonical market_id is the field your cross-venue logic joins on.

Fail soft on unknown messages

Wrap inbound frames in a tagged enum with a catch-all, and count what you cannot parse.

#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Frame {
    Snapshot(BookUpdate),
    Delta(BookUpdate),
    Heartbeat { ts: u64 },
    #[serde(other)]
    Other,
}

A rising Other counter is an alert, not a crash. It tells you the schema moved before it costs you money.

Streaming: WebSocket handling that survives a week

Polling REST endpoints works for research. For anything live you want a stream. The patterns below apply to any feed, and our deeper guides on WebSocket prediction market data and the real-time WebSocket API cover the protocol details.

The reader task

Run one task that does nothing but read frames and push them into a bounded channel.

use tokio::sync::mpsc;
use futures_util::StreamExt;
use tokio_tungstenite::{connect_async, tungstenite::Message};

async fn read_loop(url: &str, tx: mpsc::Sender<Frame>) -> anyhow::Result<()> {
    let (ws, _) = connect_async(url).await?;
    let (_, mut rx) = ws.split();
    while let Some(msg) = rx.next().await {
        if let Message::Text(t) = msg? {
            let frame = serde_json::from_str::<Frame>(&t).unwrap_or(Frame::Other);
            if tx.send(frame).await.is_err() {
                break; // consumer gone
            }
        }
    }
    Ok(())
}

Use a bounded mpsc channel on purpose. If your strategy cannot keep up, you want backpressure and a metric, not an unbounded queue that eats memory until the OOM killer ends the process.

Reconnect with jittered backoff

Wrap read_loop in a supervisor. On any error, wait, then reconnect. Double the delay up to a cap, add random jitter so a fleet of collectors does not reconnect in lockstep, and reset the delay after a connection has stayed healthy for a minute.

On every reconnect, discard local book state and wait for a fresh snapshot. This is the single most common source of silent errors in prediction market bots.

Sequence numbers and book drift

A book built from deltas is only correct if you applied every delta in order. Track the seq of the last update per market. If the next one is not last + 1, you have a gap: mark the book stale, stop quoting from it, and resubscribe for a snapshot. We wrote a full treatment of this failure mode in fixing order book drift, and the orderbook data integration guide covers how to store depth for later analysis.

fn apply(book: &mut Book, u: &BookUpdate) -> Result<(), Gap> {
    if u.seq != book.seq + 1 {
        return Err(Gap { expected: book.seq + 1, got: u.seq });
    }
    book.merge(u);
    book.seq = u.seq;
    Ok(())
}

Cross-venue logic: where canonical IDs pay off

The reason to normalize is the join. With a canonical ID you can hold a HashMap<CanonicalId, HashMap<Venue, Book>> and ask one question per event: what is the best executable price on each side, across venues, after fees?

A minimal gap check looks like this:

  1. For each event with books on at least two venues, take the best ask for YES on one venue and the best ask for NO on another.

  2. Add the fee model for each venue.

  3. If the combined cost is under 1.00 by more than your threshold, you have a candidate.

  4. Check depth. A candidate that exists at 5 contracts and vanishes at 500 is not a signal.

  5. Check book freshness. Reject any book whose last update is older than your staleness limit.

Step 5 is where most paper-profitable strategies die. Our arbitrage strategy guide walks through the economics, and execution and slippage tracking shows how to measure the gap between the price you saw and the price you got.

Resolution rules deserve a line of their own. Two markets that look identical can resolve on different sources or different edge cases. A canonical ID tells you the venues list the same event, not that the contracts pay out identically. Read the rules for both before you treat a pair as a hedge.

Backtesting with the same types

Because pm-types has no I/O, a replay source can produce the same Frame values from stored history. The loop is simple: read events in timestamp order, feed them through the same apply function, and let the strategy react.

Three rules keep a backtest honest:

  • Replay by exchange timestamp, then by sequence. Never by arrival order, which is a property of your collector, not the market.

  • Model latency. Delay your strategy’s orders by a realistic round trip before matching them against the replayed book.

  • Count partial fills. If your order is larger than displayed depth, the fill is partial at worse prices.

Long history is what makes this useful. Strategies tuned on a few weeks of data overfit quickly; the historical and real-time feed guide describes how to pull depth across a long window, and our piece on market microstructure covers what that data shows about spreads and liquidity.

Production checklist

Concern

What to do

Secrets

Load API keys from the environment or a secrets manager, never from source. Wrap them in a type that redacts on Debug.

Observability

Export counters for frames received, parse failures, gaps, reconnects and staleness per market.

Rate limits

Put a token bucket in front of every REST client. Honor 429 responses with backoff.

Kill switch

A flag the execution crate checks before every order, flippable without a deploy.

Clock

Run NTP. Compare your clock against exchange timestamps and alert on drift.

Idempotency

Generate client order IDs yourself so a retry cannot double-submit.

Panics

Set panic = "abort" under a supervisor, or catch at task boundaries. Never let one market’s bad data stop the rest.

Our perspective: normalize upstream, not in your bot

We build the data layer, so we have a bias, and we will state it: the schema mapping between venues is the least interesting part of your system and the most expensive to keep correct. Every hour spent reconciling how three venues name the same event is an hour not spent on the signal.

Our position is that data infrastructure should be venue-neutral. A feed owned by an exchange has reasons to favor that exchange’s view of the market. A neutral layer can put Polymarket, Kalshi and Limitless side by side with the same identifiers, the same units and the same history, and leave the judgment to you. That is why we built canonical IDs first, and why on-chain coverage matters for the venues that settle on-chain: it lets you see wallet behavior, not just prices.

For Rust developers specifically, the practical advice is this. Keep your types small and strict. Treat the network as hostile and the schema as something that will change. Make data replayable so every bug becomes a test. And keep execution in a separate crate with a kill switch, because the best data layer in the world does not protect you from your own strategy code.

— Dean

Getting data into your Rust client

If you want to skip the per-venue adapters, the Assymetrix Data API gives you REST and WebSocket access to cross-venue prediction market data through one integration, with free and paid tiers and academic access for researchers. Pricing is not published on the page, so check it directly before you plan capacity. For signals built on top of the raw feed, such as Smart Money wallet tracking and divergence analysis, see prediction market intelligence.

FAQ

Is there an official Rust SDK for prediction markets?

Polymarket publishes an open-source Rust client, rs-clob-client, focused on its order book API with optional WebSocket and data features. Other venues may rely on REST documentation and community crates, so check each venue’s developer pages before depending on one. For reading several venues, a unified data API with your own thin typed client is often simpler.

Should I use REST or WebSocket from Rust?

Use REST for market discovery, metadata and historical pulls. Use WebSocket for live order books and trades. A common setup loads a snapshot over REST, then applies streamed deltas, and re-snapshots whenever a sequence gap appears.

How do I avoid floating-point errors with prices?

Parse every price and size into a decimal type such as rust_decimal::Decimal at the deserialization boundary, and convert venues to one canonical unit there. Never compare prices as f64 for equality, and never accumulate PnL in floats.

Can I run the same code for backtests and live trading?

Yes, if your strategy depends on a trait that yields typed frames, not on a socket. A live implementation reads from the network and a replay implementation reads stored history. The strategy cannot tell them apart, which is the point.

Sources

Other Blog