> The index of these docs, for agents: https://traktor.fun/llms.txt

# Plans and limits

Each plan's limits, and how the API meters requests and stream units against them.

Every key belongs to an account on a plan, and the plan sets how fast and how much the account may ask.

## Plans

The table is read live from `GET /v1/plans`, which takes no key.

| Limit | Free | Pro |
| --- | --- | --- |
| Price | $0 a month | $99 a month |
| Requests | 30 a minute | 600 a minute |
| Burst | 30 | 600 |
| REST units | 20,000 a month | 5,000,000 a month |
| Stream units | 60,000 a month | 1,000,000 a month |
| History | 60 min | 24 h |
| Sockets | 1 | 10 |
| Subscriptions per socket | 2 | 50 |
| Events delivered | 5 a second | 300 a second |
| Event burst | 25 | 1,500 |
| Frames you send | 5 a second | 20 a second |
| Addresses per list | 10 | 50 |
| Replay window | None: cannot resume | 120 s |
| Filter score | 10 | 120 |
| API keys | 2 | 20 |

The per-minute budget counts requests: each costs one, whatever its units. The monthly allowance counts units.

Stream units, per key and clock hour: ⌈connection seconds ÷ 60⌉ + ⌈filter-score seconds ÷ 60,000⌉ + ⌈events delivered ÷ 1,000⌉.

## Upgrading

Pro is bought on the web, from the API tab of your Traktor profile, and only while that tab offers **Upgrade to Pro**: it opens Stripe Checkout, and the account moves to Pro once Stripe confirms the payment. An account Stripe bills changes its card, reads its invoices or cancels from **Manage billing** on the same tab, which opens Stripe's billing portal; a cancelled plan stays Pro until the period paid for ends. The Traktor app sells nothing.

## Requests and units

Each route's cost in units is on its endpoint block. The per-minute budget and the monthly allowance belong to the account, so every key it holds draws on the same two. A `304` draws no units and still counts as a request. The alert, notification and Telegram routes draw no units; their per-account limit is on their endpoint blocks and under [Every limit](#every-limit).

## Limit headers

A metered route sets these headers on every answer to a request it admits, `internal_error` aside, and on a `429` from its budgets:

| Header | What it says |
| --- | --- |
| `RateLimit-Limit` | The per-minute budget, in requests. |
| `RateLimit-Remaining` | Requests you can send now before the per-minute budget refuses one. |
| `RateLimit-Reset` | Seconds until the per-minute budget is full again; on `quota_exhausted`, until the period's units return. |
| `RateLimit-Policy` | The per-minute budget, its window and its burst. Its `unit="rest-units"` counts requests, not units. |
| `X-Traktor-Cost` | Units the request drew: `0` when its budgets refused it or its units were refunded. |
| `X-Traktor-Quota-Remaining` | Units left this period; absent on a `429`. |
| `X-Traktor-Plan` | The plan's `id`, as `GET /v1/plans` lists it. |

A script in a browser can read every one but `X-Traktor-Plan` and `RateLimit-Policy`.

A refusal raised before admission carries none of these headers and draws on neither budget. One raised after admission counts against the per-minute budget, and its units come back: `X-Traktor-Cost: 0`. Where each refusal falls:

- `GET /v1/market` and its views refuse every fault in the query before admission: a repeated key, the grammar's and paging's refusals, `plan_forbids` and `route_not_found`. What fails once the read starts comes after: `filter_too_broad`, `data_unavailable`, `market_busy` and `market_fold_pending`.
- `GET /v1/signals` refuses a fault in its query before admission, and a cursor's refusals after: `invalid_cursor`, `cursor_query_mismatch` and `cursor_expired`.
- An `internal_error` after admission gives its units back too, and its answer carries none of these headers.

## When a budget runs out

- [`429 rate_limited`](https://traktor.fun/docs/errors.md#rate_limited): the per-minute budget is spent. Wait for `Retry-After`, then retry.
- [`429 quota_exhausted`](https://traktor.fun/docs/errors.md#quota_exhausted): the period's units are spent, or fewer remain than the request costs. `details.resetsAt` says when they return, and `RateLimit-Reset` counts down to it; waiting a minute will not help. The refusal still counts against the per-minute budget.
- [`503 rate_limit_unavailable`](https://traktor.fun/docs/errors.md#rate_limit_unavailable): the limiter could not be asked. Retry after `Retry-After`.

Neither `429` draws units.

## Stream units

The trade stream, and the market stream under a key, draw on the account's monthly allowance of stream units, counted per key and clock hour by the formula under the plans table. A keyed market socket's seconds open count as connection seconds, and each view it is sent as an event delivered. The count it is checked against is refreshed every 15 minutes, so it trails your use by up to that long. The trade stream checks it when you authenticate, when you resume with `from`, and every minute, the market stream before the upgrade and every minute, and each closes with `4006 quota_exhausted` once the count passes the allowance.

## Every limit

Each route's limits, as its endpoint block states them; routes with the same limits share a row. A route that takes an API key also draws on its account's per-minute budget and units, above.

| Routes | Limits |
| --- | --- |
| `GET /v1/plans` | Requests, shared with GET /v1/meta/filters: 60 a minute, per IP |
| `GET /v1/meta/filters` | Requests, shared with GET /v1/plans: 60 a minute, per IP |
| `GET /v1/public/market` | Requests: 120 a minute, per IP; Reads waiting on a build: 64 at once, per server process; Reads waiting on a build: 8 at once, per IP; Reads with a sign-in, once enforced, every /v1/public/ route together: 1,000 a minute by default, per account |
| `GET /v1/public/search` | Searches: 60 a minute, per IP; Token directory lookups: 10 a minute, per IP; Searches building: 16 at once, per server process; Reads with a sign-in, once enforced, every /v1/public/ route together: 1,000 a minute by default, per account |
| `GET /v1/stream/position` | Invalid keys: 30 a minute, per IP; Trades the stream retains: 200,000 entries by default, per server process |
| `GET /v1/usage`, `GET /v1/market/summary`, `GET /v1/market/tokens`, `GET /v1/market/series`, `GET /v1/market/wallets`, `GET /v1/tokens/{chain}/{address}`, `GET /v1/tokens/{chain}/{address}/signals`, `GET /v1/traders/directory`, `GET /v1/traders/{wallet}`, `GET /v1/traders/{wallet}/portfolio`, `GET /v1/stocks`, `GET /v1/stocks/registry`, `GET /v1/signals/board` | Invalid keys: 30 a minute, per IP |
| `GET /v1/market`, `GET /v1/market/tape`, `GET /v1/market/rotation`, `GET /v1/market/whales` | Invalid keys: 30 a minute, per IP; Wallets a balance floor may select: 5,000 wallets, per request |
| `GET /v1/signals` | Invalid keys: 30 a minute, per IP; Events examined: 10,000 rows, per request; Cursor lifetime: 24 hours, per request; Newest arrivals an ascending page holds back: 15 seconds by default, per request |
| `GET /v1/leaderboard` | Invalid keys: 30 a minute, per IP; Accounts listed: 1,000 rows, per request |
| `GET /v1/search` | Invalid keys: 30 a minute, per IP; Token directory lookups on each server: 10 a minute, per account; Searches building: 4 at once, per account |
| `POST /v1/browser-sessions/exchange` | Exchanges: 10 a minute, per IP; Invalid keys: 30 a minute, per IP; Session lifetime: 12 hours, per request; Live sessions: 10 kept; the oldest is revoked, per account |
| `GET /v1/browser-sessions/current`, `DELETE /v1/browser-sessions/current`, `GET /v1/alerts`, `GET /v1/alerts/{id}`, `PATCH /v1/alerts/{id}`, `DELETE /v1/alerts/{id}`, `POST /v1/alerts/{id}/duplicate`, `GET /v1/notifications/unread-count`, `POST /v1/notifications/read`, `GET /v1/webhooks`, `DELETE /v1/webhooks/{id}`, `GET /v1/telegram/links`, `DELETE /v1/telegram/links/{chatId}` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP |
| `POST /v1/alerts` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Alerts: 100 kept, per account |
| `POST /v1/alerts/{id}/test` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Alert tests: 6 a minute, a burst of 3, per account |
| `POST /v1/alerts/preview` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Alert tests: 6 a minute, a burst of 3, shared with testing a saved alert, per account |
| `GET /v1/notifications` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Detailed notifications: 300 a clock hour, per account; Kept for: 90 days, per account |
| `POST /v1/webhooks` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Webhooks: 3 kept, per account |
| `POST /v1/webhooks/{id}/test` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Webhook tests: 6 a minute, a burst of 3, per account |
| `POST /v1/telegram/link-codes` | Requests: 120 a minute, per account; Session checks: 120 a minute, per IP; Invalid keys: 30 a minute, per IP; Code lifetime: 10 minutes, per request; Live codes: 1 at once, per account |
| `WS /v1/stream` | Sockets not yet authenticated: 10 at once, per IP; Sockets not yet authenticated: 500 at once, per server process; Frames before auth: 5 frames, per connection; Time to finish auth: 5 seconds, per connection; Frame size before auth: 4,096 bytes, per connection; Frame you send: 65,536 bytes, per connection; Refused frames before close 4007: 100 in 10 seconds, per connection; Events queued: 4,096 events, per subscription; Unread buffer before close 4008: 30 seconds, per connection; Invalid keys: 30 a minute, per IP; Stream-unit count refreshed every: 15 minutes, per server process |
| `WS /v1/market/stream` | Connections without a key: 8 at once, per IP; Queries your connections without a key read: 32 at once by default, per IP; Connections: 256 at once, per server process; Subscriptions: 16 at once, per connection; Commands: 60 in 10 seconds, per connection; Frame you send: 16,384 bytes, per connection; Openings with a sign-in, once enforced: 60 a minute by default, per account; Invalid keys: 30 a minute, per IP; Stream-unit count refreshed every: 15 minutes, per server process |

Every per-IP limit counts an IPv6 caller by its /64.

The per-IP limits of `GET /v1/public/market` and `GET /v1/public/search`, the market stream's keyless connections and the trade stream's sockets not yet authenticated are counted by each API server on its own, and the API runs on more than one, so a client past one of those limits can still be answered by a server whose count is below it. Each server refuses an IP once its own count passes the limit, so keep to the number in the table. Every other per-IP limit, invalid keys included, and every per-account one, is counted across all of them.

Those two reads count an IP's requests in a minute that starts at its first one, not a rolling minute, so up to twice the limit can be answered across the turn of a minute. Each server counts at most 10,000 IPs on each of them; while it holds that many, a request from an IP it is not counting yet takes the place of the IP whose minute started first, which starts a new minute at its next request.

Going past "Invalid keys" never answers `429`: the refusal stays `401 invalid_key`, even for a valid key the server has to look up again, until the budget refills ([When a key is refused](https://traktor.fun/docs/authentication.md#when-a-key-is-refused)).
