> ## 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.

# Python client

> pip install 0xinsider: one typed method per API operation, typed errors, cursor pagination, and a sandbox client that needs no key.

`0xinsider` on PyPI is the official Python client: one method for every operation in the [OpenAPI document](https://0xinsider.com/api/v1/openapi.json), typed from that document's own schemas, a typed exception per API error, and `paginate`, which follows cursors for you. Source at [`0xinsider/0xinsider-python`](https://github.com/0xinsider/0xinsider-python).

```bash theme={null}
pip install 0xinsider
```

You install `0xinsider` and import `oxinsider`, because a Python module name cannot start with a digit. It needs Python 3.9 or newer and has one dependency, `httpx`. This page describes the released 0.4.0, which covers all 67 operations in the OpenAPI document it was generated from.

## Run it without a key

```python theme={null}
import oxinsider

with oxinsider.Client.sandbox() as client:
    page = client.list_leaderboard(limit=5)
    print(page["data"][0])
```

`Client.sandbox()` points at `https://0xinsider.com/sandbox` and sends no credential. Every documented operation answers there with its example payload. [Start without a key](/sandbox) has the rules, including `sandbox_status` for error branches:

```python theme={null}
with oxinsider.Client.sandbox() as client:
    try:
        client.request("GET", "/api/v1/leaderboard", query={"sandbox_status": 429})
    except oxinsider.RateLimitedError as error:
        print(error.code, error.retry_after)  # rate_limited 60
```

## Then live data

```python theme={null}
import oxinsider

client = oxinsider.Client()  # reads OXINSIDER_API_KEY
trader = client.get_trader("swisstony", expand=["strategy", "categories"])
print(trader["data"].get("grade"), trader["data"]["pnl"].get("realized"))
```

`Client()` with no argument reads `OXINSIDER_API_KEY`, the same name the [CLI](/integrations/cli) and the [MCP server](/integrations/mcp) use. Pass `api_key=` to supply it yourself, from your secret manager rather than a file in the repository. A live key needs an account with an active Pro subscription; see [Authentication](/authentication).

Nothing else changes between the two clients. The methods, the arguments, the envelopes, and the cursor keys are the same.

## Keep financial values exact

The trader and positions responses keep their display-safe numeric fields and add exact decimal atoms when their source values are verified. Use Python's `Decimal` on the atom's `value`; do not convert it through `float`.

```python theme={null}
from decimal import Decimal

profile = client.get_trader("swisstony")
realized_atom = profile["data"]["pnl"].get("exact", {}).get("realized")
if realized_atom is not None:
    realized_usd = Decimal(realized_atom["value"])
    print(realized_usd + Decimal("0.01"))

positions = client.list_positions(wallet=["swisstony"], min_size=0)
current_atom = positions["data"][0].get("exact", {}).get("current_value_usd")
current_usd = Decimal(current_atom["value"]) if current_atom is not None else None
```

Each atom also carries `unit`, `scale`, and `basis`. The exact block or an optional member can be absent when its source is unavailable; an absent value is not zero.

## Method names

Every operation is a method named after its `operationId` in snake case: `listLeaderboard` becomes `list_leaderboard`, `getMarketIntel` becomes `get_market_intel`. Each returns the decoded JSON body, envelope included.

```python theme={null}
oxinsider.OPERATIONS        # every operation id the release implements
oxinsider.OPERATION_COUNT   # 67 in 0.4.0
oxinsider.OPENAPI_SHA256    # the document bytes the release was generated from
oxinsider.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.

## Types

Every operation is typed from the same document it is generated from. `oxinsider.types` holds a `TypedDict` for each request body, response envelope, and schema, and your editor infers them with no annotation of your own.

```python theme={null}
trader = client.get_trader("swisstony")          # GetTraderResponse
trader["data"]["pnl"].get("realized")            # float | None
client.list_positions(min_grade="A")             # min_grade takes only a real grade
client.get_trader_context_markdown("swisstony")  # str, not an envelope
client.with_response.get_trader("swisstony")     # ApiResponse[GetTraderResponse]
```

Nothing is validated, converted, or copied at runtime. The methods return the decoded JSON exactly as they always have, so upgrading changes what your type checker sees and nothing that runs.

| The document says                    | The type says                                                                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| The field is in `required`           | A required key.                                                                                                                |
| The field is not in `required`       | An optional key. `trader["data"]["grade"]` is an error; `.get("grade")` is not, because an ungraded wallet carries no `grade`. |
| The field is nullable                | `\| None`. The key is there and its value can be `null`.                                                                       |
| The field is an enum the API returns | `Literal[...] \| str`, so a value the API adds after your release is not an error.                                             |
| The field is an enum you send        | A strict `Literal`, so a typo fails before the call spends a request.                                                          |
| The field is a `const`               | Exact, so a union narrows on it: a ledger entry narrows on `entry["state"]` to its sealed, opened, or uncommitted shape.       |

An omitted key and a `null` value are different facts, and neither is a zero. Read an optional field with `.get()` and say so when it is absent.

A filter that guarantees a field does not change its type: `min_grade="A"` means every row has a `grade`, but the type still calls it optional, because the schema does. Read it with `.get()`, or `cast` the row when you are sure.

A field this release does not know yet is still in the dictionary you get back. To read one, call `client.request(...)`, which is typed `Any`, or upgrade the package.

Resolving these annotations at runtime with `typing.get_type_hints` needs Python 3.10 or newer; reading the dictionaries does not.

## Pagination

```python theme={null}
for trade in client.paginate("list_whale_trades", min_grade="A", limit=100):
    handle(trade)
```

`paginate` follows `next_cursor` to the last page, keeping your filters fixed and moving only the cursor. [Pagination](/concepts/pagination) has the cursor rules.

It checks each page before yielding it and before spending another request, so a broken walk stops instead of looking like a finished one:

```python theme={null}
try:
    for trade in client.paginate("list_whale_trades", min_grade="A"):
        handle(trade)
except oxinsider.PaginationError as error:
    resume = oxinsider.pagination_checkpoint(error)
    print(error.reason, error.request_id, resume.pages_fetched)
```

`error.reason` is `invalid_envelope` (the response is not a cursor-paginated list), `invalid_data` (`data` is not a list), `missing_cursor` (`has_more` is true with nothing to continue from), or `repeated_cursor` (a cursor this walk already requested, caught before the duplicate request). A `PaginationError` is a malformed response, never an exhausted collection.

Pass `progress=oxinsider.PaginationProgress()` to read where a walk got to, and resume from `progress.cursor` (refetch the last page) or `progress.next_cursor` (continue past it). `progress.stopped_by` is set only when the walk ended on its own terms, which is what tells a partial walk from a complete one.

## Headers on a successful call

```python theme={null}
resp = client.with_response.list_whale_trades(min_grade="A")
resp.data          # the same body the plain method returns
resp.etag          # pass to if_none_match= on the next read
resp.rate_limit    # Budget: limit, remaining, reset_at, reset_after
resp.request_id    # quote this when you report a bad response
```

`client.with_response.<method>(...)` calls the same operation and returns an `ApiResponse` instead of the body alone. Without it, a `200` discards everything but the body, including the `ETag` you need to revalidate with. `monthly_quota`, `batch_rate_limit`, `request_cost`, `retry_after`, and `header(name)` are there too.

A header the API did not send reads `None`, never `0`. A missing budget is unknown, not exhausted.

## Errors

```python theme={null}
try:
    client.get_trader("0x0000000000000000000000000000000000000000")
except oxinsider.SubscriptionRequiredError:
    print("Pro is not active on this key")
except oxinsider.RateLimitedError as error:
    print(error.status, error.code, error.retry_after)
```

Every non-2xx response raises. The class follows the status, and `status`, `code`, and `retry_after` are attributes on it.

| Exception                   | Status                                               |
| --------------------------- | ---------------------------------------------------- |
| `BadRequestError`           | `400`                                                |
| `AuthenticationError`       | `401`                                                |
| `SubscriptionRequiredError` | `402`                                                |
| `PermissionDeniedError`     | `403`                                                |
| `NotFoundError`             | `404`                                                |
| `RateLimitedError`          | `429`, with `retry_after` in seconds                 |
| `ServerError`               | `500`, `502`, `503`, `504`                           |
| `OxinsiderConnectionError`  | The request never reached the API                    |
| `InsecureTransportError`    | A base URL that would send the key over plain HTTP   |
| `DownloadError`             | A redirect-backed download that did not complete     |
| `PaginationError`           | A page that broke the cursor protocol, with `reason` |

All of them inherit `oxinsider.OxinsiderError`. The ones that carry an API body also inherit `oxinsider.OxinsiderApiError`. [Errors](/errors) lists every `error.code` behind them.

## Other behavior worth knowing

| Concern         | Behavior                                                                                                                                                              |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Timeout         | 30 s per call, `timeout=` on the constructor.                                                                                                                         |
| Conditional GET | `if_none_match="<etag>"` returns `{"object": "not_modified", "data": None, "etag": ...}` on `304`. The `ETag` to send comes from `with_response`.                     |
| Idempotency     | `idempotency_key=` on the webhook mutations that document it.                                                                                                         |
| Stream          | `client.request("GET", "/api/v1/stream", stream=True)` returns the open `httpx.Response`.                                                                             |
| Redirect routes | `download_trader_export` and `redirect_api_openapi_spec` return a streaming `Download`. The redirect is followed once and the credential never goes to the file host. |

## What this client does not do

* Place an order or hold a Polymarket key. Every method is a read except your own webhook and export calls.
* Round anything. Money and price fields keep the API's precision, so round them only when you display them.
* Treat a missing field as `0`. Missing means the provider did not report it.
* Regenerate itself. A release names the document it was built from, and a newer operation needs a newer release.

## 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 same 5 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 these exceptions carry.
  </Card>
</CardGroup>
