# Rate limits

> Every limit is per key or per account, never per page view. Read the remaining count from the answer's headers rather than hard-coding it.

Source: https://firmtape.com/docs/developers/limits

Every limit is per key or per account, never per page view. Read the remaining count from the answer's headers rather than hard-coding it.

| Product | What | Limit | Headers | Notes |
| --- | --- | --- | --- | --- |
| LIVE | Levels API | 120 requests a minute per key | `X-RateLimit-Limit`, `X-RateLimit-Remaining`; `Retry-After` on a 429 | A 304 (`If-None-Match`) is not counted. A `live` key answers from at most 3 client addresses an hour. History: the last 90 days; older with DATASET. |
| ORDERS | Orders API and POSITIONS API | 20,000 calls a day on Orders API, 100,000 on Orders Team, per account | `X-RateLimit-Limit`, `X-RateLimit-Remaining` | The count resets at midnight ET. The Orders MCP server counts against the same budget. |
| LAB | LAB API | Credits: a backtest or an optimize pass is 1 credit, an OPTIONS archive run 10 | none | Developing an idea is free but needs a positive balance. One idea holds the conveyor at a time. A failed job refunds its credit. |
| REPLAY | Archive JSON and the session files | Distinct days per address: a visitor that walks many sessions in an hour is slowed | 429 with `Retry-After` | Every finished session is free to read one at a time. Bulk copies are the DATASET licence, which lifts the breadth limit. |
| REPLAY | MCP server, free tools | `get_gex_profile`: 50 distinct days an hour and 150 in 24 hours per client address | a tool error that says when the window rolls | Days already opened in the window keep answering. The other free tools have no per-address limit. |
| DATASET | DATASET files | No breadth limit for a licence holder | none | Mirror the archive in one pass, then take each new session the evening it closes. |
| GEO | Geo API and Geo MCP server | 20,000 calls a month on Geo API, 100,000 on Geo Team, REST and MCP together | `Retry-After` on a 429; `GET /v1/usage` is free | The month is the calendar month in UTC. A malformed call is not counted. |

## Backoff

On a 429 wait `Retry-After` seconds; on a 5xx or no answer, wait 1, 2, 4, 8 seconds (with random jitter) and give up after five tries.

```js
async function call(url, opts, tries = 5) {
  for (let i = 0; ; i++) {
    const r = await fetch(url, opts);
    if (r.status !== 429 && r.status < 500) return r;
    if (i + 1 >= tries) return r;
    const after = Number(r.headers.get("Retry-After"));
    const wait = after > 0 ? after * 1000 : 2 ** i * 1000 * (0.5 + Math.random());
    await new Promise((ok) => setTimeout(ok, wait));
  }
}
```

```python
import random, time, requests

def call(url, tries=5, **kw):
    for i in range(tries):
        r = requests.get(url, **kw)
        if r.status_code != 429 and r.status_code < 500:
            return r
        after = r.headers.get("Retry-After")
        time.sleep(float(after) if after else 2 ** i * (0.5 + random.random()))
    return r
```
