> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xinsider.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust client

> The oxinsider crate: a typed async method per API operation, a sandbox client that needs no key, retries that honor Retry-After, and a bounded stream reader.

`oxinsider` is the official Rust client, generated from the [OpenAPI document](https://0xinsider.com/api/v1/openapi.json). Every operation is a typed async method named after its `operationId` in snake case, so `listLeaderboard` is `list_leaderboard`. Source at [`0xinsider/0xinsider-rust`](https://github.com/0xinsider/0xinsider-rust).

<Note>
  The first crates.io release is pending, so `cargo add oxinsider` does not resolve yet. Until it lands, add the crate from GitHub with the command below.
</Note>

```bash theme={null}
cargo add oxinsider --git https://github.com/0xinsider/0xinsider-rust
cargo add tokio --features macros,rt-multi-thread
```

The crate is named `oxinsider` because a crate name cannot start with a digit. It runs on Tokio, uses rustls by default, and needs Rust 1.87 or later. `default-features = false, features = ["native-tls"]` switches to the platform's TLS.

## Run it without a key

```rust theme={null}
use oxinsider::{Client, ListLeaderboardParams};

#[tokio::main]
async fn main() -> oxinsider::Result<()> {
    let client = Client::sandbox()?;
    let board = client
        .list_leaderboard(&ListLeaderboardParams::default().limit(5))
        .await?;
    for entry in &board.data {
        // An absent grade is an ungraded wallet, not an F.
        let grade = entry.grade.as_deref().unwrap_or("ungraded");
        println!("{grade:>8}  {}", entry.address);
    }
    Ok(())
}
```

No credential, no account. `Client::sandbox()` sends every operation to the sandbox, which answers with its documented example. [Start without a key](/sandbox) has the rules, including `sandbox_status` for error branches.

## Then live data

```rust theme={null}
let client = oxinsider::Client::from_env()?; // reads OXINSIDER_API_KEY
```

That one line is the whole change. The key can be an API key (`oxi_sk_live_...`) or an OAuth 2.1 access token (`oxi_at_...`), and the base URL is `https://api.0xinsider.com`.

A live key needs an account with an active Pro subscription. See [Authentication](/authentication).

With `OXINSIDER_API_KEY` unset, the client has no credential and still reads the [public routes](/authentication#what-needs-a-key). `Client::new(key)` takes the key directly, and `Client::builder()` sets the base URL, the timeout, and the retry budget.

## Calling an operation

| What you pass                                    | Where it goes                                                                                                                                                  |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Path parameters and required query parameters    | Method arguments, for example `client.get_trader("swisstony", &params)`.                                                                                       |
| Optional query parameters and documented headers | The operation's `...Params` struct, built with `Default::default()` and a setter per field: `ListWhaleTradesParams::default().min_grade(Grade::A).limit(100)`. |
| A request body                                   | A typed `body: &T` argument.                                                                                                                                   |
| `If-None-Match`                                  | `if_none_match` on the params. An unchanged resource returns `Error::NotModified { etag }`.                                                                    |
| `Idempotency-Key`                                | `idempotency_key` on a webhook write's params. Reuse the same key when you retry by hand.                                                                      |

Each method returns the typed response for its operation, and every schema in the document is a type in `oxinsider::models`. `GET /api/v1/stream` has no generated method, because `open_stream` reads it. The two Markdown routes return a `String`, and the export download returns a streaming `Download`.

A field the API did not report is `None`, never `0`. An enum value this release does not know lands in `Other(String)`, and an unknown JSON key is ignored, so an additive API change does not break a response.

## Errors

| Variant                                                 | When                                                                                                                                                    |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Error::Api(ApiError)`                                  | The API answered with a non-2xx status. `ApiError` carries `status`, `code`, `reason`, `message`, `param`, `retry_at`, `retry_after`, and `request_id`. |
| `Error::NotModified`                                    | A `304` to a request that sent `If-None-Match`.                                                                                                         |
| `Error::InsecureTransport`                              | The key would have gone over plain HTTP to a host that is not loopback. Nothing was sent.                                                               |
| `Error::Transport`                                      | No HTTP response: DNS, TLS, connect, timeout, or reset.                                                                                                 |
| `Error::Decode`                                         | A `2xx` body did not match its type. It names the JSON path that failed and keeps the body.                                                             |
| `Error::Stream`, `Error::Download`, `Error::Pagination` | The stream broke its contract, a download could not finish, or a page could not be continued.                                                           |

Branch on `code` and `reason`, never on `message`. `ApiError::kind()` maps the status to `BadRequest`, `Authentication`, `SubscriptionRequired`, `PermissionDenied`, `NotFound`, `RateLimited`, `Server`, and the rest. See [Errors](/errors) for every code.

## Retries

| Rule                 | Behavior                                                                                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What is retried      | Every `GET`, the two read-only batch posts (`batch_get_traders` and `batch_get_market_intel`), and a webhook write that carries an `idempotency_key`. Every other write is sent once. |
| On what              | `408`, `429`, `502`, `503`, `504`, and a failed connection.                                                                                                                           |
| How long it waits    | The `Retry-After` value, in seconds or as an HTTP date, plus up to 250 ms. Without one, a backoff from 500 ms up to 8 seconds.                                                        |
| A long `Retry-After` | Over 60 seconds, the client does not wait. The error comes back with `retry_after` for you to schedule.                                                                               |
| Budget               | 2 retries by default. `Client::builder().max_retries(0)` sends each request once. Each attempt has a 30-second deadline.                                                              |

## Following cursors

List operations answer `{ object: "list", data, has_more, next_cursor, meta }`. `Pager` follows `next_cursor` until `has_more` is `false`:

```rust theme={null}
use oxinsider::models::Grade;
use oxinsider::pagination::Pager;
use oxinsider::ListWhaleTradesParams;

let mut pager = Pager::new(ListWhaleTradesParams::default().min_grade(Grade::A).limit(100));
while let Some(page) = pager.next_page(async |params| client.list_whale_trades(params).await).await? {
    for trade in &page.data {
        println!("{:?} {:?}", trade.size_usd, trade.market.title);
    }
}
```

A page that says `has_more` without a usable `next_cursor`, or that repeats a cursor already requested, stops the walk with `Error::Pagination` instead of truncating it or looping. `pagination::collect_all` gathers every row when the list is short. See [Pagination](/concepts/pagination).

## The live stream

`client.open_stream(&StreamOptions)` reads [`GET /api/v1/stream`](/api-reference/endpoint/get-stream) frame by frame and holds at most 1 MiB for one undelivered frame. Pass the last `seq` you processed as `last_event_id` to resume after a disconnect. A `resync` frame means the resume point is outside the retained window, so refetch current state before you continue.

A frame that breaks the stream's contract ends it with `Error::Stream`, whose `last_seq` says where to resume. The stream has no deadline and is never retried for you.

## Credential safety

| Rule                          | Behavior                                                                                                                                                            |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Transport                     | The key goes over `https://` only, or `http://` to a loopback host for a backend you run.                                                                           |
| A base URL that would leak it | `build()` fails with `Error::InsecureTransport`, and every credentialed request is checked again before it is sent. The error names the destination, never the key. |
| Redirects                     | The client follows none. The export download follows its one redirect with a fresh request that carries no credential.                                              |
| The sandbox                   | A live key (`oxi_sk_live_...`) is refused there, so it is never sent.                                                                                               |
| Logs                          | `Debug` on a client redacts the key.                                                                                                                                |

## Which document a release implements

```rust theme={null}
oxinsider::provenance::OPENAPI_SHA256  // the document bytes it was generated from
oxinsider::provenance::OPERATION_COUNT // 67
oxinsider::provenance::APP_COMMIT      // the app commit that last changed that document
```

Compare `OPENAPI_SHA256` with `shasum -a 256` of `https://0xinsider.com/api/v1/openapi.json` to see whether your release is behind the API.

## What this client does not do

* Place an order or hold a Polymarket key. Every operation is a read except your own webhook and export calls.
* Round anything. Money and price fields are `f64`, the precision the API sends.
* Treat a `None` as `0`. Missing means the provider did not report that value.
* Retry a write that the API does not replay, such as starting an export.

## Go next

<CardGroup cols={2}>
  <Card title="Start without a key" icon="play" href="/sandbox">
    Everything that works without a key.
  </Card>

  <Card title="Quickstart" icon="rocket" href="/quickstart">
    The first reads in `curl`, Python, and Go.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Where a key belongs, and OAuth for an app with users.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/errors">
    Every code a non-2xx body carries.
  </Card>
</CardGroup>
