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

# TypeScript client

> Call the 0xinsider API with typed responses, pagination, stream recovery, and webhook verification.

Use `@0xinsider/sdk` in a Node.js application to call the API, paginate lists, read the event stream, and verify webhooks. It includes TypeScript declarations and typed errors. The [source repository](https://github.com/0xinsider/0xinsider-node) contains the client and examples.

## Install

```bash theme={null}
npm install @0xinsider/sdk
```

You need Node.js 18 or newer. The package is ESM only and has no runtime dependencies.

For local SDK development, clone the source repository, run `npm ci && npm run build`, and install that directory from your project.

<span id="start-in-the-sandbox-without-a-key" />

## Make a request without a key

`OxinsiderApiClient.sandbox()` returns example data from the [sandbox](/sandbox). It needs no credential and stores nothing. Successful JSON responses carry `meta.sandbox: true`.

```ts theme={null}
import { OxinsiderApiClient, RateLimitedError, type Trader } from "@0xinsider/sdk";

const sandbox = OxinsiderApiClient.sandbox();
const board = await sandbox.listLeaderboard({ limit: 5 });
console.log(board.data, board.meta.sandbox); // LeaderboardEntry[], true

// sandbox_status is not part of the route's documented query, so this call
// takes the loose form: an explicit type argument in place of the typed one.
try {
  await sandbox.call<Trader>("getTrader", {
    path: { address: "swisstony" },
    query: { sandbox_status: 429 },
  });
} catch (err) {
  if (err instanceof RateLimitedError) console.log(err.retryAfterSeconds);
  else throw err;
}
```

Use `sandbox_status` to request a documented error example. Because that parameter is specific to the sandbox, the example uses the low-level `call<T>()` form. Live keys are refused in sandbox mode; streams and file downloads return `400`.

<span id="read-a-trader-and-search-markets" />

## Switch to live data

Set `OXINSIDER_API_KEY` in your environment and pass it as `apiKey`. Live data requires an active Pro or Max subscription; see [Authentication](/authentication).

```ts theme={null}
import { OxinsiderApiClient } from "@0xinsider/sdk";

const client = new OxinsiderApiClient({
  apiKey: process.env.OXINSIDER_API_KEY,
  // baseUrl defaults to https://api.0xinsider.com
});

// A wallet address, a username, or a trd_ ID
const trader = await client.getTrader("swisstony", {
  query: { expand: ["strategy", "categories"] },
});
console.log(trader.data);

const markets = await client.searchMarkets("NBA", {
  query: { limit: 5, status: "active" },
});
console.log(markets.data);

// Any operation by id, typed by the id: path, query, body and the envelope
const usage = await client.call("getUsage");
const holders = await client.call("getMarketHolders", {
  path: { condition_id: "0x..." },
  query: { outcome: "yes", limit: 25 },
});
```

The default origin is `https://api.0xinsider.com`. A custom `baseUrl` keeps its path prefix and must use HTTPS, except for a loopback development host. Public operations can be called without a key; protected operations reject a missing key before sending the request.

## Read a pick by stable ID

Published SDK `0.15.0` includes `getPickOfTheDayLedgerEntry(pickId)` for the public [ledger entry route](/api-reference/endpoint/get-pick-of-the-day-ledger-entry). Install that version or newer to use the helper; SDK `0.14.0` does not include it.

```bash theme={null}
npm install @0xinsider/sdk@0.15.0
```

```ts theme={null}
import { OxinsiderApiClient } from "@0xinsider/sdk";

const publicClient = new OxinsiderApiClient();
const entry = await publicClient.getPickOfTheDayLedgerEntry("731");
console.log(entry.data.pick_id, entry.data.state);
```

Keep `pick_id` as a decimal string. This request needs no key. Read `state` before accessing disclosure fields: a live sealed entry withholds the side, payload, and nonce; an opened entry includes its canonical payload and nonce.

For committed entries, read `commitment_version` to select the proof format. Version `1` keeps the original payload with `pick_rank`; version `2` uses `pick_id` and integer `version: 2`. Use `is_free_selection` to determine access, rather than deriving it from `publication_order`.

<span id="typed-by-the-operation" />

## Handle tier-dependent pick fields

Treat a current response's pick fields, archive matchup, and archive category as optional. An unauthorized pending row intentionally omits identifying fields; it is not an incomplete game you should enrich through another endpoint.

Public API `scheduled_picks` contains only entitled rows and retains its release and kickoff clocks; unauthorized scheduled ranks appear in `locked_picks`. Read `state` and `locked_picks` before assuming a current-pick response contains selections. Public sealed ledger entries omit `kickoff`, while opened proof payloads retain the kickoff needed for verification.
Use SDK declarations generated from the updated schema when that release is available; this change does not announce a new npm version.

## Pass typed parameters

Each method knows its operation's path parameters, query parameters, body, and response. You do not need to supply a response type. A query the endpoint does not support, or a missing path parameter, produces a TypeScript error.

The remaining snippets use the live `client` created above. Pass transport settings after the method's parameters:

```ts theme={null}
const controller = new AbortController();
const signal = controller.signal;
const trades = await client.listLargeTrades(
  { min_grade: "S", limit: 25 },
  { signal, timeoutMs: 5_000, maxRetries: 0 },
);
trades.data[0].trader.address; // LargeTrade
trades.meta.request_id; // ResponseMeta, always present
```

`call`, `list`, `paginate`, `paginatePages`, and `collect` are typed when you pass a literal operation ID. Their low-level form accepts an operation selected at runtime or an explicit type argument; you then own the asserted response shape.

Use `listSuspiciousTrades()` and `getSuspiciousTrade()` for flagged trades. The `listInsiderRadar()` and `getInsiderRadarFlag()` methods remain as deprecated aliases. Their older event types keep the `"insider_radar_flag_raised"` discriminant for existing subscribers.

<span id="what-the-client-handles" />

## Understand timeouts and retries

| Setting | Behavior |
| - | - |
| `timeoutMs` | The default deadline is 15,000 ms per call. Set `null` to remove it. A client deadline raises `RequestTimeoutError`. |
| `maxRetries` | The default is 2 retries. Set 0 to send one attempt. |
| Retryable failure | The client retries `408`, `429`, `502`, `503`, `504`, and network failures. |
| Retryable operation | `GET`, read-only batch posts, and supported webhook writes carrying an `idempotencyKey` are eligible. |
| `Retry-After` | The client accepts seconds or an HTTP date and waits up to 60 seconds. Longer waits return an error for your application to schedule. |
| Caller cancellation | An aborted request is not retried. |

The client does not retry ordinary request timeouts, `400`, `401`, `402`, `403`, `404`, `409`, or `500`. A write without safe replay support is sent once.

<span id="keep-financial-values-exact" />

## Keep money values exact

Trader and position responses can include an `exact` block containing decimal strings. Use them for arithmetic when available. Parse the string directly with a decimal library; converting through `Number` first can lose precision.

```bash theme={null}
npm install decimal.js
```

```ts theme={null}
import Decimal from "decimal.js";

const profile = await client.getTrader("swisstony");
const realizedAtom = profile.data.pnl.exact?.realized;
if (realizedAtom) {
  const realizedUsd = new Decimal(realizedAtom.value);
  console.log(realizedUsd.plus("0.01").toFixed());
}

const positions = await client.listPositions({ wallet: "swisstony", min_size: 0 });
const currentAtom = positions.data[0]?.exact?.current_value_usd;
const currentUsd = currentAtom ? new Decimal(currentAtom.value) : null;
```

Each value includes `unit`, `scale`, and `basis`. The block or one of its fields can be absent when the source is unavailable. Report that absence instead of substituting zero.

<span id="look-up-1-to-25-traders-in-one-request" />

## Read up to 25 traders at once

`batchGetTraders()` calls [Batch traders](/api-reference/endpoint/batch-get-traders). It returns one item per input in the same order, including duplicates.

```ts theme={null}
const batch = await client.batchGetTraders(["swisstony", "trd_123"], {
  expand: ["strategy", "trust"], // strategy, categories, quant_metrics, trust
});
for (const item of batch.data) {
  if (item.status === "ok") console.log(item.input, item.data?.grade);
  else console.log(item.input, item.error?.code);
}
```

Check each item's `status`. An `ok` item contains `data`; an `error` item contains its own error. One unknown identifier does not fail the other inputs.

`meta.request_cost` measures batch item units. It does not measure the number of HTTP requests.

<span id="paginate-a-list" />

## Read every page

`paginate()` yields each item and follows `next_cursor`. `paginatePages()` yields whole response envelopes when you also need metadata.

```ts theme={null}
import { paginate, paginationResumePoint } from "@0xinsider/sdk";

try {
  for await (const trade of paginate(client, "listLargeTrades", {
    query: { min_grade: "S", limit: 100 },
  })) {
    console.log(trade);
  }
} catch (err) {
  const resume = paginationResumePoint(err); // { cursor, pagesFetched } or undefined
  if (!resume) throw err;
}
```

A missing or repeated continuation cursor raises `PaginationError` before the page is yielded. Its `reason` is `missing_cursor` or `repeated_cursor`, and it includes the page that failed validation.

Use `maxPages` to stop at a positive page count, `signal` to cancel, and `progress` to retain the last paging position. A limit you supplied does not mean you reached the end of the list. `paginationResumePoint()` also returns a continuation point from a paging failure.

Keep filters unchanged during a traversal. See [Pagination](/concepts/pagination) for expired cursors.

<span id="read-conditionally" />

## Reuse a cached response

The client copies a response's `ETag` header to `meta.etag`. Send it back as `If-None-Match` to check whether your cached body is current.

```ts theme={null}
import { isApiNotModifiedResponse } from "@0xinsider/sdk";

const first = await client.call("getTrader", { path: { address: "swisstony" } });

if (!isApiNotModifiedResponse(first) && first.meta.etag) {
  const next = await client.call("getTrader", {
    path: { address: "swisstony" },
    headers: { "If-None-Match": first.meta.etag },
  });
  if (isApiNotModifiedResponse(next)) console.log("unchanged");
}
```

A `304` returns a `not_modified` result with `data: null`. Keep your cached body; do not replace it with `null`.

## Handle errors

API error statuses raise a subclass of `OxinsiderApiError`. The client selects a class by `error.reason` first, then `error.code`. A `304` is handled separately as described above.

| `error.code` or `error.reason` | Class |
| - | - |
| `bad_request` | `BadRequestError` |
| `unknown_query_parameter` | `UnknownQueryParameterError`, itself a `BadRequestError` |
| `invalid_api_key` | `InvalidApiKeyError` |
| `sandbox_api_key` | `SandboxApiKeyError` |
| `api_key_in_query` | `ApiKeyInQueryError` |
| `subscription_required` | `SubscriptionRequiredError` |
| `forbidden` | `ForbiddenError` |
| `not_found` | `NotFoundError` |
| `account_locked` | `AccountLockedError` |
| `request_timeout` | `ServerTimeoutError` |
| `rate_limited` | `RateLimitedError` |
| `rate_limit_unavailable` | `RateLimitUnavailableError` |
| `internal_error` | `InternalServerError` |
| `invalid_response` (raised by the client, not the API) | `InvalidResponseError` |
| `pick_not_released` | `PickNotReleasedError` |
| `read_model_warming` | `ReadModelWarmingError` |
| `database_unavailable` | `DatabaseUnavailableError` |
| `request_accounting_unavailable` | `RequestAccountingUnavailableError` |
| `cursor_expired` | `CursorExpiredError` |
| `trader_not_tracked` | `TraderNotTrackedError` |
| `unknown_endpoint` | `UnknownEndpointError` |
| `idempotency_in_progress` | `IdempotencyInProgressError` |
| `webhook_delivery_in_progress` | `WebhookDeliveryInProgressError` |

Errors expose `status`, `code`, `retryAt`, `error`, `meta`, `requestId`, and the raw `body`. Errors with retry timing also expose `retryAfterSeconds`. The client's automatic retries finish before your code catches the final error.

Read the API's reason from `err.error.reason`. `err.reason` is only populated for subclasses tied to a specific reason, so it may be `null` even when the API body includes a reason.

```ts theme={null}
import { OxinsiderApiError, RateLimitedError } from "@0xinsider/sdk";

try {
  await client.listLargeTrades({ min_grade: "S" });
} catch (err) {
  if (err instanceof RateLimitedError) {
    await new Promise((r) => setTimeout(r, (err.retryAfterSeconds ?? 1) * 1000));
  } else if (err instanceof OxinsiderApiError && err.retryAt) {
    console.log("retry at", err.retryAt.toISOString()); // schedule; never sleep on it
  } else if (err instanceof OxinsiderApiError) {
    console.error(err.status, err.code, err.requestId);
  } else {
    throw err;
  }
}
```

Check `monthly_quota_exceeded` before scheduling another rate-limit retry. Its reset can be next month. A future `retryAt`, including a pick release time, belongs in your scheduler rather than a long-running sleep.

<span id="write-with-an-idempotency-key" />

## Make a safely repeatable webhook write

These 8 operations accept `idempotencyKey`: `createWebhook`, `updateWebhook`, `deleteWebhook`, `rotateWebhookSecret`, `prepareWebhookSecret`, `activateWebhookSecret`, `retireWebhookSecret`, and `redeliverWebhookDelivery`.

```ts theme={null}
await client.call("createWebhook", {
  body: {
    name: "Large trades",
    url: "https://example.com/hooks/0xinsider",
    event_types: ["whale_trades_inserted"],
  },
  idempotencyKey: crypto.randomUUID(),
});
```

Keep the key and body unchanged when retrying the same operation. The client rejects an idempotency key on an unsupported operation. After a timeout, the write may have succeeded; replay the same key and body or read the resource to check.

The first 3 operations and `redeliverWebhookDelivery` have convenience methods. Use `client.call()` for secret rotation operations. [Webhooks](/guides/webhooks) explains setup, verification, and rotation.

<span id="stream-events-and-check-webhook-signatures" />

## Read the stream

| Helper | Use it when |
| - | - |
| `streamFeed()` | You want to read one connection and handle reconnecting yourself. |
| `streamFeedResilient()` | You want automatic reconnection. It stops after `maxReconnects` empty connections in a row, 10 by default. |
| `consumeStreamCheckpointed()` | You need to resume only after an event's work and checkpoint write both finish. |

The helpers support `Last-Event-ID` and the stream's `event`, `condition_id`, and `min_grade` filters. Stream connections have no total request deadline. Supply `signal` to cancel when your task ends; this example reads for at most 30 seconds:

```ts theme={null}
import { streamFeed } from "@0xinsider/sdk";

for await (const frame of streamFeed(client, { signal: AbortSignal.timeout(30_000) })) {
  console.log(frame);
}
```

A malformed frame, a non-SSE success response, or a frame larger than `maxFrameBytes` (1 MiB by default) raises `StreamProtocolError`. Its `lastSeq` identifies the last valid frame; automatic reconnection does not retry a protocol failure.

A `Retry-After` longer than `maxRetryAfterMs` (60 seconds by default) raises `StreamRetryDeferredError` with `retryAt` and `lastSeq`. Schedule reconnection for that time.

### Save a checkpoint after processing

`consumeStreamCheckpointed()` distinguishes the last received frame (`cursor`) from the last completed frame (`checkpoint`). It advances the checkpoint only after both `onEvent` and `onCheckpoint` resolve.

If a handler fails, the connection closes and reconnects from the checkpoint. After `maxHandlerRetries` consecutive failures on one sequence (3 by default), it raises `StreamHandlerFailedError` with the sequence and recovery position. Delivery can repeat, so deduplicate on `seq` or make your side effect idempotent.

`onResync` is awaited before a refreshed state is committed. See [Stream](/api-reference/endpoint/get-stream) for what a resync means.

## Verify webhook signatures

`verifySignature(input)` checks `x-0xinsider-signature` against the raw request body. It uses a constant-time comparison and a 300-second timestamp tolerance, and accepts any valid signature candidate during staged secret rotation.

It returns `false` for an invalid signature or timestamp. An empty secret or invalid `toleranceSeconds` throws instead. `parseWebhookEvent(body)` then parses the typed event; see [Webhooks](/guides/webhooks).

<span id="consume-or-cancel-export-downloads" />

## Finish or cancel downloads

`downloadTraderExport` and `downloadWhaleDataset` return streaming download objects. Consume `response.body` to completion or cancel it when you stop reading.

If a helper fails before it can return the object, it cancels the body and waits up to 2 seconds for cleanup. The original failure remains the main error; cleanup rejection or timeout is retained in `cause`. An `AggregateError` preserves both when the original thrown value cannot carry a cause.

A cleanup timeout means completion is unknown. Do not report the file as downloaded unless you finished reading it and performed the required integrity checks.

<span id="what-it-does-not-do" />

## Limits

* The SDK does not place Polymarket orders or hold a wallet key.
* It can manage your 0xinsider webhooks and exports.
* Missing values remain missing. Keep numeric precision until display.
* Long `Retry-After` and `retryAt` waits need application scheduling.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.