Skip to main content
You get 100 requests per minute on a sliding window. The limit is per user, shared across every key and every endpoint. Batch read endpoints (e.g. POST /api/v1/traders/batch, POST /api/v1/markets/intel/batch) get a separate budget in item units, also 100 per minute. A 25-item batch reserves 25 item units before it runs. GET /api/v1/usage does not spend primary quota or log into daily usage. It has its own 100 reads/minute per-user guard so checking quota can’t itself blow up usage-count load.
SettingValue
Requests per minute100
Usage reads per minute100
Batch item units per minute100
Window typeSliding (not bucket-of-60-seconds)
ScopePer user (sum of all keys)
Header on successX-RateLimit-* (+ X-Batch-RateLimit-* on batch endpoints)
Header on 429Retry-After

Headers you should read

Every authenticated response carries:
X-RateLimit-Limit:     100
X-RateLimit-Remaining: 87
X-RateLimit-Reset:     1774450631
X-Request-Id:          req_550e8400
HeaderMeaning
X-RateLimit-LimitMax requests per minute.
X-RateLimit-RemainingRemaining slots in the current window.
X-RateLimit-ResetUnix timestamp (seconds) when the window resets.
X-Request-IdRequest identifier to include when reporting a support issue.
Batch responses additionally carry:
HeaderMeaning
X-Request-CostItem units this single call charged (one per batch item).
X-Batch-RateLimit-LimitMax item units per minute.
X-Batch-RateLimit-RemainingItem units remaining in the window.
X-Batch-RateLimit-ResetUnix timestamp when the batch window resets.
Browser clients can read these headers on public REST /api/v1/* responses because the public API exposes them through CORS. The public API also exposes Retry-After, ETag, X-Request-Id, X-Request-Cost, Mcp-Session-Id, and X-Mcp-Error-Code. Remote MCP at /api/v1/mcp still validates Origin against the 0xinsider/localhost allowlist. Do not use credentialed CORS for public API calls; send only the Bearer token header. If you go over either budget, you get a 429 plus a Retry-After header in seconds:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{
  "object": "error",
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 12s.",
    "doc_url": "https://docs.0xinsider.com/rate-limits"
  }
}

A client that respects the retry

async function fetchWithBackoff(url, opts = {}, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch(url, opts);
    if (res.status !== 429) return res;

    const wait = Number(res.headers.get("retry-after") ?? 1) * 1000;
    await new Promise(r => setTimeout(r, wait));
  }
  throw new Error("Rate limit retries exhausted");
}

const res = await fetchWithBackoff(
  "https://api.0xinsider.com/api/v1/whale-trades?limit=50",
  { headers: { Authorization: `Bearer ${process.env.OXINSIDER_API_KEY}` } }
);
const body = await res.json();

Staying well under the limit

  • Cache. Most of this data updates every 30-120 seconds. Re-fetching every second wastes budget.
  • Paginate, don’t re-list. Use cursors instead of refetching the full leaderboard each loop.
  • Ask for more per request. Bump limit to 100 instead of making 10 calls of limit=10.
  • Batch when you know your IDs. One batch call for 25 traders costs 25 batch-item units but only one regular request.
  • Watch X-RateLimit-Remaining. Below 10, pause polling until the next reset.
  • Use /usage for budget checks. GET /api/v1/usage reads the current primary limiter window without incrementing it. Respect its own 100 reads/minute cap; don’t probe quota with another endpoint.
  • One process owns the polling. Don’t have every container in a fleet poll the same endpoint with the same key.

Conditional GETs

Deterministic read endpoints return an ETag header. Send it back as If-None-Match to get 304 Not Modified with an empty body when nothing changed. This covers trader, positions, whale-trades, whale-trades/history, leaderboard, markets/explore, market intel, market snapshot, insider-radar, position-timeline, and health responses. Report snapshots aren’t conditional yet because their body includes a request-time generated_at.