0xinsider Rust SDK
The official Rust client for the 0xinsider Developer API: Polymarket analytics for sports and esports. Every tracked wallet is graded S to F from settled P&L; the API returns grades, large trades, sharp-money flow, positions, market snapshots, reports and search.
- Website and API keys: https://0xinsider.com/developers
- Authentication (API keys and OAuth 2.1): https://0xinsider.com/auth.md
- OpenAPI 3.1: https://0xinsider.com/api/v1/openapi.json
- Documentation: https://docs.0xinsider.com
- API reference for this crate: https://docs.rs/oxinsider (from the first crates.io release;
cargo doc --openuntil then) - MCP server: https://api.0xinsider.com/api/v1/mcp
Install
The first release to crates.io is pending. Until it lands, add the crate from this repository:
The crate is oxinsider, because a crate name cannot start with a digit. It is async on Tokio, uses rustls by default (default-features = false, features = ["native-tls"] switches to the platform TLS), and needs Rust 1.87 or later.
Try it without a key
The sandbox answers every documented operation with example data. It needs no credential and never touches production data.
use ;
async
Live data
Data operations need an API key from 0xinsider.com/developers or an OAuth 2.1 access token (auth.md), plus an active Pro subscription. Discovery, health and platforms are public.
use ;
use Pager;
use ;
async
- One method per operation. Every operation in the OpenAPI document is an async method named after its operationId in snake_case (
listLeaderboardislist_leaderboard). Path parameters and required query parameters are arguments; optional ones go in the operation's...Paramsstruct, built withDefault::default()and chainable setters.OPERATIONSlists them all with method, path and retry class. - Typed responses. Each method returns the typed envelope for its operation (
ListWhaleTradesResponse { object, data, has_more, next_cursor, meta, .. }). Every schema in the document is inoxinsider::models. A body that does not match returnsError::Decode, which names the JSON path that failed and keeps the body. - Absent is not zero. A field the API did not report is
None. Serve a truthful unavailable state; never read it as0. An ungraded wallet is uncovered, not unskilled. - Additive changes do not break you. Unknown JSON keys are ignored, and an enum value this release does not know lands in
Other(String)instead of failing the response. - Numbers. Money and price fields are
f64, the precision the API sends. Do not round before you display them. - Conditional reads. Set
if_none_matchon the params; an unchanged resource returnsError::NotModified { etag }. - Writes. Webhook writes take
idempotency_keyon their params. Reuse the same key when you retry by hand, and never reuse it with a different body. - Markdown and files.
get_trader_context_markdownandget_market_context_markdownreturnString.download_trader_exportreturns a streamingDownload.
Errors
A non-2xx answer is Error::Api(ApiError) with status, code, reason, message, param, retry_at, retry_after and request_id. Branch on code and reason, never on message; ApiError::kind() maps the status to BadRequest, Authentication, SubscriptionRequired, PermissionDenied, NotFound, RateLimited, Server and the rest.
match client.get_trader.await
The other variants: NotModified, InsecureTransport (see below), Transport (no HTTP response), Decode, Stream, Download, Pagination and Config.
Retries
A read (every GET, and the batch reads batch_get_traders and batch_get_market_intel, which store nothing) is retried on 408, 429, 502, 503 and 504, and on a failed connection. A webhook write is retried only when it carries an idempotency_key; every other write is sent once. The client waits for Retry-After (seconds or an HTTP date) plus up to 250 ms of jitter, or a jittered backoff from 500 ms up to 8 s without one. A Retry-After longer than 60 seconds is not waited out: the error comes back with retry_after for you to schedule. max_retries defaults to 2; Client::builder().max_retries(0) sends each request once. Each attempt has a 30-second deadline (timeout, no_timeout).
Pagination
List operations answer { object: "list", data, has_more, next_cursor, meta }. Pager follows next_cursor until has_more is false; pagination::collect_all gathers every row. A page that says has_more with no usable next_cursor, or that repeats a cursor the walk already requested, is a broken response rather than the end: the walk stops with Error::Pagination (MissingCursor or RepeatedCursor) instead of truncating or looping. Feeds move while you read them: follow the cursor to completion, never page by offset, and never total a partial walk. Pager::params() holds the next cursor if you stop early and want to resume.
Stream
GET /api/v1/stream is an unbounded Server-Sent Events stream. Read it with open_stream, which delivers each frame as it arrives, holds at most max_frame_bytes (1 MiB by default) for one undelivered frame, and closes the connection when the reader is dropped:
use StreamOptions;
let mut cursor: = None; // the last seq you processed
let mut options = default.events;
options.last_event_id = cursor;
let mut reader = client.open_stream.await?; // Error::Api for a 401, a 429 with retry_after, ...
while let Some = reader.next.await?
// The server closed the stream: reconnect with `cursor`.
A frame that breaks the SSE contract (not JSON, not an object, no usable sequence, a malformed resync marker, a 200 that is not text/event-stream, or a frame past the byte ceiling) ends the stream with Error::Stream(StreamProtocolError) whose reason, last_seq and frame_id say which frame and where to resume from; a malformed frame never moves last_seq. Keep-alive comments are consumed silently, retry: is exposed as retry_hint(), and the response headers are on headers(). The stream has no deadline and is never retried for you. StreamFrame::decode::<T>() turns a frame into your own type.
Exports
use TraderExportJobDataStatus;
let job_id = client.submit_trader_export.await?.data.job_id;
loop
let download = client.download_trader_export.await?;
println!;
let saved = download.save.await?;
println!;
The API answers the download with a redirect to a short-lived file location. The client follows it once, with a fresh request that carries no credential and no cookies, and only to https:// (or http:// on a loopback host). chunk() streams the file and save() writes it with bounded memory; bytes(max) reads it into memory and refuses more than max. No checksum is published for an export: save returns the SHA-256 of exactly what it wrote, and etag() is the file host's object identity, not a content hash. A failed hop is Error::Download with a reason (InsecureLocation, UnexpectedRedirect, Unavailable, TooLarge, Interrupted, NotRedirected, MissingLocation) and never the signed URL.
Webhooks
use ;
// Verify the exact bytes you received, before parsing them.
let ok = verify_signature?;
if !ok
The API signs every delivery as v1=hex(HMAC_SHA256(signing_secret, "<timestamp>.<raw_body>")). verify_signature enforces the published 300-second replay window and compares every candidate in the header in constant time, so it accepts both signatures during a staged secret rotation. It returns Ok(false) for a bad delivery and Err only for an empty secret, which is your configuration bug, not the sender's. verify_signature_at takes an explicit tolerance and clock; compute_signature signs a payload for your tests.
Where the key goes
The key is sent over https:// only, or over http:// to a loopback host (localhost, 127.0.0.1, [::1]) for a backend you run yourself. A base_url that would send it anywhere else fails build() with Error::InsecureTransport, and every credentialed request is checked again before it is sent; the error names the destination, never the key. A keyless client may still call the public operations on such a base. The client this crate builds follows no redirects, so an https:// answer cannot downgrade a request to http://. A live key (oxi_sk_live_...) is refused on the sandbox so it is never sent there. Debug on a Client redacts the key.
Bring your own reqwest::Client (a proxy, custom roots) with Client::builder().http_client(...); this crate re-exports reqwest so the versions match.
How it is built
src/models.rs and src/operations.rs are generated from the published OpenAPI document by cargo codegen (the Rust program in generator/, then rustfmt), and a weekly workflow opens a pull request when the contract changes. cargo codegen --check fails when the generated files are stale. The generator understands every construct the document uses and stops on one it does not, rather than emitting an untyped value that hides the gap.
src/provenance.rs says which document a release was generated from: OPENAPI_SHA256 (the SHA-256 of the document bytes), OPENAPI_VERSION, OPERATION_COUNT, and APP_COMMIT, the 0xinsider/0xinsider commit that last changed web/public/api/v1/openapi.json (or None when it could not be resolved). Compare oxinsider::provenance::OPENAPI_SHA256 with shasum -a 256 of the live document to see whether a release is behind the API.
The tests decode every example the document publishes into its generated type, run the request path against a scripted local server, and (with OXINSIDER_SANDBOX_TESTS=1) decode the live sandbox's answer for every JSON operation.
OXINSIDER_SANDBOX_TESTS=1
OXINSIDER_API_KEY=oxi_sk_live_...
Other official tools
- Python SDK:
pip install 0xinsider(0xinsider/0xinsider-python) - Go SDK:
go get github.com/0xinsider/0xinsider-go(0xinsider/0xinsider-go) - Node.js and TypeScript SDK:
@0xinsider/sdk(0xinsider/0xinsider-node; its first npm release is pending, and the README shows how to build it from source) - CLI and MCP server:
npm install --global @0xinsider/mcporbrew install 0xinsider/tap/oxinsider - Remote MCP server:
https://api.0xinsider.com/api/v1/mcp - Agent Plugin and skills: 0xinsider/agent-plugin
Immutable whale datasets
submit_whale_dataset creates a bounded NDJSON snapshot. Read get_whale_dataset_status and follow next_action and poll_after_s; cancel_whale_dataset cancels work before its completion fence.
When the job is ready, download_whale_dataset returns a credential-safe streaming Download. Its save(path) writes the transferred content and returns its SHA-256; compare it with the artifact manifest. The default client decodes gzip, so use the content SHA-256 for that client.
Resume get_event_replay_since with the manifest's continuation cursor and non-null filters. Pass the exact decimal string to GetEventReplaySinceParams::min_size; numeric builder calls remain supported. The finite snapshot window does not restrict later deltas; deduplicate snapshot and expanded replay rows by their wt_ trade ID. Coverage is detected whale alerts, not all provider fills or updates and deletions.
License
MIT