# API Reference — Rate limits

The API is public and needs no key. Two things are metered so that one script cannot starve
everyone else: **how many requests an IP sends**, and — more tightly — **how many signed quotes
it asks for**.

## Limits

| Scope | Limit | Key |
| --- | --- | --- |
| Every `/api/*` route | 600 requests / minute | client IP |
| `POST /api/quote` | 10 quotes / minute | client IP |
| `POST /api/quote` | 30 quotes / minute | `sender` address (canonical form) |

Limits are **token buckets**: the allowance refills continuously (600/min ≈ one request every
100 ms, with the full 600 available as a burst from idle). CORS preflight (`OPTIONS`) and the
long-lived `GET /api/orders/stream` connection are not counted.

There is also a coarser per-IP cap at the edge (nginx), meant for floods: about 30 requests/s on
`/api/*` with a burst of 20, and 2 requests/s on `POST /api/quote` with a burst of 5. It answers
`429` with a non-JSON body and no `Retry-After` — pause for a second or two and retry.

## What a rejection looks like

Every metered response carries the allowance of the **last bucket checked**: the per-IP bucket on
most routes; on `POST /api/quote` with a `sender` in the body, the per-sender bucket (30) — the
10/min per-IP bucket only shows up in its own `429`.

**Response headers**
```txt
X-RateLimit-Scope: anonymous
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
```

When you are over the limit you get **`429 Too Many Requests`**:

**HTTP response**
```txt
HTTP/1.1 429 Too Many Requests
Retry-After: 6
Content-Type: application/json

{ "error": "RATE_LIMITED", "retryAfterSec": 6 }
```

`Retry-After` (seconds, whole) is the earliest moment the next request will pass. Sleep for it,
then retry — do not tight-loop; every rejected attempt is itself a request.

> **Tip: Quote once, pay once**
>
> A quote is valid for 180 s — you do not need to re-quote while a quote is still live, and you
> should not request a quote before you are ready to send the transaction. Batches (multisend) are
> one quote regardless of the recipient count, so 10 quotes/min is 10 *swaps*, not 10 recipients.

> **Tip: Running at volume?**
>
> If the anonymous allowance is too small for what you are building, reach out on
> [Telegram](https://t.me/drip_tools) or [Discord](https://discord.gg/Zn3M3gag2W).

## Not rate limits

Two other `5xx` answers look similar but mean something else:

| Status | Body | Meaning |
| --- | --- | --- |
| `503` | `{ "error": "WARMING_UP", "retryAfterSec": 5, "waitingFor": [...] }` | The backend just restarted and is loading prices / balances (`/api/quotes`, `/api/prices`, `POST /api/quote`). Retry after `Retry-After`. |
| `503` | `{ "error": "Quoting is disabled", "code": "SIGNING_DISABLED" }` | Quoting is paused by the operator — see [POST /api/quote](https://docs.drip.tools/api/quote.md). |

## Summary of error shapes

| Status | Body | Meaning |
| --- | --- | --- |
| `429` | `{ "error": "RATE_LIMITED", "retryAfterSec": n }` | per-minute allowance exhausted (IP or sender) |
| `429` | *(empty)* | edge flood protection — back off harder |
| `503` | `{ "error": "WARMING_UP", "retryAfterSec": n, "waitingFor": [...] }` | backend warming up — retry |
