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

# Rate limits

> Per-endpoint, per-IP, per-wallet rate-limit table and retry guidance.

Rate limits are enforced at the `core-api` edge. Every authenticated
request passes through **two** independent buckets:

* **`ip`** — per source IP (behind the ingress; respects
  `X-Forwarded-For` set by the trusted load balancer). Cheap edge
  defence that applies to anonymous and authenticated traffic alike.
* **`wallet`** — per your key's `associatedWallet` (single\_wallet
  partners) or per the `X-User-Wallet` header value (multi\_wallet
  partners). Prevents a single wallet identity from exceeding the
  platform's per-user ceiling regardless of how the calls are
  distributed across your keys.

Both must pass; a single rejection returns 429 and sets
`Retry-After` to the tightest bucket's reset delta. Response headers
(`X-RateLimit-Limit`, `-Remaining`, `-Reset`) expose the tightest
applicable bucket.

A per-partner organisation-wide bucket lives on the partner record
(`rateLimitPerMin`) but is not currently enforced at the request
boundary. If you need a partner-wide ceiling, operations can lower
your per-wallet cap as a stop-gap — contact your integration
manager.

## Limits

Trading and write paths use **per-second** wallet windows (anti-burst);
read paths use **per-minute** windows. The two scopes (`ip` and `wallet`)
are independent — each request must pass every applicable bucket.

### Trading (writes)

| Endpoint                                                                    | Scope  | Limit  | Window |
| --------------------------------------------------------------------------- | ------ | ------ | ------ |
| `POST /api/orders/*` (shared ip bucket)                                     | ip     | 6000   | 60s    |
| `POST /api/orders/place`                                                    | wallet | **20** | **1s** |
| `POST /api/orders/place-batch`                                              | wallet | **8**  | **1s** |
| `POST /api/orders/cancel`                                                   | wallet | **40** | **1s** |
| `POST /api/orders/cancel-all`                                               | wallet | **1**  | **1s** |
| `POST /api/orders/cancel-batch`                                             | wallet | **5**  | **1s** |
| `POST /api/vault/split-signature`, `/merge-signature`, `/convert-signature` | wallet | **3**  | **1s** |
| `POST /api/vault/*` (umbrella)                                              | ip     | 100    | 60s    |

### Order reads

| Endpoint                            | Scope  | Limit | Window |
| ----------------------------------- | ------ | ----- | ------ |
| `GET /api/orders/open`              | wallet | 300   | 60s    |
| `GET /api/orders/history`           | wallet | 300   | 60s    |
| `GET /api/orders/{id}`              | wallet | 300   | 60s    |
| `GET /api/orders/{id}/fills`        | wallet | 300   | 60s    |
| `GET /api/orders/{id}/fees-summary` | wallet | 300   | 60s    |

### Market data

| Endpoint                                                                   | Scope | Limit | Window |
| -------------------------------------------------------------------------- | ----- | ----- | ------ |
| `GET /api/markets`, `/api/markets/:slug`, `/api/markets/:symbol/orderbook` | ip    | 600   | 60s    |
| `GET /api/markets/:symbol/trades`                                          | ip    | 240   | 60s    |
| `GET /api/markets/:symbol/price-history`                                   | ip    | 240   | 60s    |
| `GET /api/markets/:symbol/traders`                                         | ip    | 120   | 60s    |
| `GET /api/markets/:symbol/ohlc` *(deprecated)*                             | ip    | 120   | 60s    |

### Portfolio (`/me/*`)

| Endpoint                                                                                                                           | Scope  | Limit | Window |
| ---------------------------------------------------------------------------------------------------------------------------------- | ------ | ----- | ------ |
| `GET /api/me/vault`, `/me/balances`, `/me/positions`, `/me/portfolio`, `/me/fee-tier`, `/me/trades`, `/me/fees`, `/me/auto-redeem` | wallet | 600   | 60s    |
| `GET /api/me/deposit-limits`                                                                                                       | wallet | 120   | 60s    |
| `POST /api/me/vault/emergency`                                                                                                     | wallet | 120   | 60s    |
| `GET /api/me/withdrawals`, `/me/withdrawals/{id}`, `/me/withdrawals/deposit-sources`                                               | wallet | 60    | 60s    |
| `GET /api/me/withdrawals/fee`                                                                                                      | wallet | 30    | 60s    |
| `POST /api/me/withdrawals/{id}/cancel`                                                                                             | wallet | 10    | 60s    |
| `POST /api/withdrawals/request`                                                                                                    | ip     | 60    | 60s    |

### Discovery

| Endpoint                           | Scope | Limit | Window |
| ---------------------------------- | ----- | ----- | ------ |
| `GET /api/leaderboard`             | ip    | 600   | 60s    |
| `GET /api/leaderboard?search=…`    | ip    | 120   | 60s    |
| `GET /api/search`, `/api/search/*` | ip    | 60    | 60s    |

<Warning>
  Limits above are **testnet defaults**. Staging and mainnet may run
  tighter or looser values — your onboarding runbook contains the
  authoritative table for the environment you are connected to.
</Warning>

## Counter behaviour

Buckets are fixed-window with a Redis-backed counter that re-applies
its TTL on every access where the key has lost it (Redis maxmemory
eviction races, manual `PERSIST`, replica-failover key loss, snapshot
reload). Earlier deploys had a class of stuck-counter behaviour where
a key without a TTL kept incrementing without ever expiring; clients
saw `X-RateLimit-Remaining` flat-line at 0 and `Retry-After` never
counting down. That class is fixed — every request you make now
either decrements `Remaining` or surfaces a real countdown to refill.

`ip` buckets key on the **real client IP**, not the immediate TCP
peer. We honour `X-Forwarded-For` from the trusted ingress hop, so
partners behind shared edges (Cloudflare, corporate egress proxies)
no longer collide on a single bucket. If your traffic transits an
extra proxy hop your onboarding manager hasn't seen, raise it
during integration so we can adjust the trusted-hop count.

## Response headers

Every rate-limited response includes:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 60
```

* `X-RateLimit-Reset` is **delta-seconds** until the bucket refills
  — i.e. the value `60` means "wait 60 seconds and the limit
  resets". This deliberately matches `Retry-After` semantics; both
  are seconds-until-refill, not Unix epoch.
* The HTTP header `Retry-After` is set to the same delta-seconds on
  every `429`.
* On `429 Too Many Requests`, the response body uses the standard
  error envelope:

```json theme={null}
{
  "status": "error",
  "error": {
    "code": "rate_limited",
    "message": "place-order budget exhausted",
    "trace_id": "..."
  }
}
```

Read the seconds-to-wait from the `Retry-After` header — it is
not duplicated inside the body.

## Retry guidance

1. **On `429`**: wait `Retry-After` (header value, in seconds) before
   retrying. Do **not** retry sooner — the bucket has not refilled.
2. **Backoff on 5xx**: exponential backoff with jitter, starting at
   200ms, capped at 10s. Maximum 3 attempts for idempotent calls
   (GET, DELETE). **Do not retry non-idempotent writes (`POST /orders`,
   `POST /withdrawals/request`) automatically** — they accept a
   `clientOrderId` / `nonce` for deduplication if your design requires
   retries.
3. **Circuit-break on persistent 5xx**: if your error rate exceeds
   50% over 30 seconds, stop sending new requests for 60 seconds.

## Higher limits

Partners with justified high-throughput needs (market makers, volume
traders) can request bespoke limits at onboarding:

* `wallet`-scoped ceilings can be lifted 10–50× on the place / cancel
  endpoints.
* `ip`-scoped limits are lifted on a per-whitelisted-IP basis.
* Platform-wide ceilings cannot be exceeded; they protect the shared
  matcher.

Contact [partners@predictstreet.com](mailto:partners@predictstreet.com)
with your expected peak throughput profile.
