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

# Stream

Connecting, subscribing, resuming and keeping up on the traktor.v1 trade stream.

`wss://api.traktor.fun/v1/stream` delivers `origin: "live"` trades as Traktor stores them, chosen by a [flow filter](https://traktor.fun/docs/filters.md#flow-filter).

### `WS /v1/stream`

Trade rows matching your filters as they land, metered in stream units.

- URL: `wss://api.traktor.fun/v1/stream`
- Credential: API key
- Subprotocol: `traktor.v1`
- Also takes the flow filter: https://traktor.fun/docs/filters.md

Limits:

- 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

Upgrade refusals:

| Status | Body | Retry-After | When |
| --- | --- | --- | --- |
| 400 | `unsupported_subprotocol` |  | A `Sec-WebSocket-Protocol` header is sent and does not offer `traktor.v1`; a request with no such header is accepted. |
| 429 | `try_later` | 5 s | Too many sockets have not authenticated yet: from your IP, or on the server when it cannot make room, because no network holds two more of them than yours (an IPv6 /48 or an IPv4 address) or your upgrade URL carries a credential. Also sent while the server is draining for a deploy. |
| 400 | `upgrade_failed` |  | The request offers `traktor.v1` or no subprotocol, the server is not draining, and your IP and the server have room, but it is not a WebSocket upgrade. |

## Lifecycle

1. Connect with the subprotocol `traktor.v1`, and put nothing in the URL: a query parameter named like a credential closes the socket `4015`. `hello` arrives at once, before you authenticate, with the protocol version, the heartbeat and the frame sizes; `hello.auth` says how many frames you may send before you authenticate, how many bytes each may hold, and how many milliseconds you have.
2. Send `auth` as your first frame, with your key in `key`. Authentication must finish within `hello.auth.deadlineMs` of connecting, the server's check of the key included, or the socket closes `4001`; a server full of connections still authenticating can close the oldest from the network holding the most with `4001` sooner, to make room. The `ack` holds your plan's limits, the chains and apps tracked, and `head`, the stream's position; read the limits there rather than copying the plans table. A refused key closes the socket instead: `4002`, or `4003` for a suspended account. Frames you send after `auth` and before its `ack` wait for the key check, and past ten of them each is refused `rate_limited`, so send your subscriptions once the `ack` arrives.
3. Send `subscribe` with a [flow filter](https://traktor.fun/docs/filters.md#flow-filter) in `filter`; left out, the filter's defaults apply. Its other keys are optional: `sub`, your name for the subscription, which the server picks when you leave it out; `delivery`, with `maxBatch`, the most events one frame carries, and `maxDelayMs`, the longest the server holds an event to fill a frame; and `from`, to [resume](#resume). The `ack`'s `result.seq` is the stream's position when the subscription opened.
4. Trades arrive in `events` frames. `d` holds a batch, each element a [trade row](https://traktor.fun/docs/trades.md) without `tx` and `leg`, which `id` holds. `seq` is the position of the batch's last trade, an opaque string: keep the latest for each subscription.

The stream carries only `origin: "live"` rows: a row Traktor recovers later, with `origin` `sweep` or `backfill`, never reaches it. The same trade can arrive twice, under two `seq`s, so deduplicate on `id`. The [Quickstart](https://traktor.fun/docs/quickstart.md#open-the-stream) has a client in TypeScript and Python.

Every frame is one JSON object with `op` and `id`. Each frame you send gets one answer, which carries the frame's `id` as `re`: an `ack`, an `error`, or a `pong` for a `ping`. Answers come in the order you sent the frames, with one exception: while a replay is being sent or `auth`'s key is being checked, the frames you send wait for it, and one sent while twice your plan's per-second inbound rate of them already wait (during the key check, ten sent after `auth`) is refused `rate_limited` at once, ahead of the frames before it. An `error` for a frame with no readable `id` has no `re`, and a fault that closes the socket answers with a `bye` instead. Ignore a server op you do not know. A field the server does not know is refused, so a typo never widens a filter.

One channel is served, `flows`; a `subscribe` that names another answers `channel_denied`. A `chains` or `platforms` name outside the vocabulary answers `filter_invalid`, with `detail.pointer` at the key you sent: `/filter/chains`, or `/filter/chain` if you spelled it so. Both spellings of one key answer `filter_invalid` at the plural's pointer, `/filter/chains`. Either way no subscription opens.

## Frames

You send:

| Op | Fields | What it is |
| --- | --- | --- |
| `auth` | `id, key, client?` | The first frame: the API key, and an optional client name of up to 128 characters. |
| `subscribe` | `id, sub?, channel?, filter?, delivery?, from?` | Opens a subscription on a flow filter, under the name `sub` or one the server gives it; `from` resumes after a `seq`. `delivery` sets batching: `maxBatch`, the events one frame may carry, 1 to 500 and 50 by default, and `maxDelayMs`, the milliseconds an event may wait to fill one, 0 to 2,000 and 250 by default, or `mode: "frame"` for one event a frame; a value out of range is `bad_frame`. |
| `unsubscribe` | `id, sub` | Closes one subscription, or a list of them at once; an unknown name removes none. |
| `subs` | `id` | Lists the connection's subscriptions and where each one stands. |
| `ping` | `id, payload?` | Asks for a pong; a `payload` of up to 64 characters comes back in it. |
| `bye` | `id` | Flushes what is queued, acknowledges, and closes with 1000. |

The server sends:

| Op | Fields | What it is |
| --- | --- | --- |
| `hello` | `id, protocol, server, now, auth, heartbeat, frame` | Sent on connect: the pre-auth budget, the heartbeat, and the frame size limits. |
| `ack` | `id, re, result` | The answer to a frame that succeeds, `ping` aside: to `auth`, the account, its limits and the feed; to `subscribe`, the subscription's `seq` and `warnings`, and with `from` whether it `resumed` and what `gap` there is; to `unsubscribe` and `bye`, the names removed; to `subs`, the subscriptions. |
| `error` | `id, re?, code, message, retryable, sub?, detail?` | A refused frame; it never closes the socket. |
| `events` | `id, sub, seq, sentAt, d` | Trade rows for one subscription; `seq` is the position of the last one. |
| `heartbeat` | `id, now, lagMs, subs, partial` | Every heartbeat interval: each subscription's `seq`, dropped and queued counts, fifty at a time. |
| `notice` | `id, kind, sub?, message?, detail` | Something changed for a subscription or the connection; the notice kinds say what. |
| `pong` | `id, re, payload?, now` | The answer to `ping`. |
| `bye` | `id, code, reason, message, retryable, retryAfterMs?, detail?` | Sent before the server closes the socket, with the close code it is about to use. |

Notice kinds:

| Code | Meaning |
| --- | --- |
| `dropped` | Events were dropped for backpressure, oldest first; `detail` has `count`, `since`, `reason`, `policy` and `queued`, and dropped events are never billed. |
| `batch_pressure` | Batching widened under pressure; `detail` is the batch size and delay now in force. |
| `replay_incomplete` | A resume span was not fully replayed; `detail.reason` says why. The subscription carries on from the ack's `seq`, and the rest of the span is not sent. |
| `plan_changed` | The account's plan or billing period changed; `detail` has the plan codes `from` and `to`, equal when only the period rolled over, and the new limits apply from now. Subscriptions already open stay; a lower cap on subscriptions, allow lists, filter score or connections applies to the next subscribe and the next connection. |

Ack warnings:

| Code | Meaning |
| --- | --- |
| `usd_amount_nullable` | A USD bound leaves out rows whose `usdAmount` is null. |
| `blocktime_interpolated` | Some EVM block times are interpolated; each event says so in `blockTimeSource`. |
| `rbh_buy_understated` | Robinhood buy notionals read about 8% low, systematically. |
| `replay_reorder` | `seq` is monotonic and `blockTime` is not: a live row can arrive long after its block. |
| `coverage_not_tracked` | A chain the filter names has no tracked app, or an app it names is tracked on no chain, so that name matches no trade; `message` names each. Sent today only for `elysium`, which is in the vocabulary while no app is read there. |
| `rate_cap_below_feed` | The plan's event rate is below the feed's mean on a wide filter, so drops are expected. |

A heartbeat arrives every 15 seconds; the socket closes after 45 seconds without a frame from you.

The `ack`'s `warnings` qualify a subscription as a whole; each event's `flags` qualify its own row ([Flags](https://traktor.fun/docs/trades.md#flags)).

## Resume

After a disconnect, subscribe again with `from` set to the last `seq` you processed. If the stream has not been rebuilt since, and that `seq` is younger than the plan's replay window, the span after it replays in order with no gap, each event judged by the filter as it was live, and the `ack` says `resumed: true`. Otherwise the `ack` says `resumed: false`, and `gap.reason` says why:

- `trimmed`: the span is older than the plan's replay window, or than the stream retains.
- `epoch_stale`: the stream was rebuilt after that `seq`.
- `replay_unavailable`: the server could not read the span.

The subscription still opens, and carries on from the `ack`'s `seq`: the trades in the gap are not sent. A `from` past the stream's newest event opens nothing: it is refused `bad_frame` at `/from`, so subscribe again without it. A plan with no replay window cannot resume: its `from` answers `trimmed`, or `epoch_stale` once the stream has been rebuilt. Each plan's window is in [Plans and limits](https://traktor.fun/docs/plans.md).

A span cut short after the `ack` arrives as a `replay_incomplete` notice, with `detail.reason` and `detail.oldestAvailable`. The subscription carries on from the `ack`'s `seq`, and the rest of the span is not sent.

## Flow control

Each subscription queues up to 4,096 matching events. When events match faster than your plan's event rate delivers them, or faster than your socket reads, the queue fills and stays full: the oldest are dropped and counted, and each event you do receive has waited about 4,096 ÷ the matches a second, minutes on a wide filter and longer still on one that matches only a little faster than the plan delivers. To stay current, narrow the filter until a heartbeat's `queued` stays low.

A `dropped` notice arrives within about a second of a drop, with `detail` `{ count, since, reason: "backpressure", policy: "drop_oldest", queued }`, and every `heartbeat` carries each subscription's running total. Dropped events are never billed.

Shedding never closes the socket. `4008` closes one that stops reading: its unread buffer stayed over the server's limit for 30 seconds.

While a subscription's queue is long, the server widens its batching, never narrows it, and says so in a `batch_pressure` notice whose `detail` is the batching in force. Your own batching returns, without a notice, once the queue shortens.

## Liveness

The silence limit under [Frames](#frames) also counts the pong your client returns to the server's WebSocket ping, which comes at least twice within the limit; most libraries send that pong on their own. If yours does not, send a `ping` frame more often than the limit. After a `4009`, reconnect at once.

## Plan changes and revocation

The server checks your key once a minute. A change of plan or billing period arrives as a `plan_changed` notice, with the plan codes in `detail.from` and `detail.to`, and the new limits apply from then. Subscriptions already open stay as they are: a lower cap on subscriptions, allow lists, filter score or connections applies to your next `subscribe` and your next connection. A key that no longer authenticates, because it was revoked or expired or its account was suspended or closed, closes the socket `4003` at that check; the `bye` before it names, in `detail.code`, the code a REST request would get ([When a key is refused](https://traktor.fun/docs/authentication.md#when-a-key-is-refused)). Once the month's stream units are spent, the socket closes `4006` ([Stream units](https://traktor.fun/docs/plans.md#stream-units)).

## Close codes

Every close the server starts is preceded by a `bye`, with `retryable` and the close's detail. `1013`, the one close that would not be, is not sent today. When a `bye` carries `retryAfterMs`, wait that long before you reconnect. After your own `bye`, the server sends what is queued, as far as your socket keeps reading, acknowledges it, and closes `1000`; what your socket had no room for is not sent, so resume from the last `seq` you processed.

A frame over 65,536 bytes never reaches the server: the transport drops the connection with no `bye` and no close frame, and your client sees `1006`.

A deploy closes `1001`. Its `bye` carries `retryAfterMs`, each subscription's resume point in `detail.subs[].seq`, and `detail.lastSeq`, the earliest of them. Before that `bye` the server sends each subscription's queued events, as far as your socket keeps reading, and any pending `dropped` notice, so a resume point never lies past an event you were neither sent nor told was dropped. Reconnect after `retryAfterMs`, then resume each subscription with `from` set to its own resume point, or every one from `lastSeq` and deduplicate on `id`; a `null` resume point means subscribe afresh.

Treat a `4xxx` code the table below does not list as not retryable, unless its `bye` says it is.

| Code | Reason | Meaning | What to do | `bye` first |
| --- | --- | --- | --- | --- |
| 1000 | `bye` | The server closed after your `bye` and its ack. | Nothing to do. | no |
| 1001 | `deploy` | The server is redeploying; the `bye` carries `lastSeq` and each subscription's `seq`. | Reconnect after `retryAfterMs` and resume each subscription with `from`. | yes |
| 1003 | `unsupported_data` | A binary frame arrived; the protocol is text only. | Send JSON text frames. | yes |
| 1009 | `message_too_big` | A frame sent before `auth` was larger than the pre-auth limit. | Send a small `auth` frame first. | yes |
| 1013 | `not ready` | Not sent today: the server attaches its socket runtime before it listens. A socket opened before then would close here, before `hello` and with no `bye`. | Reconnect after a short wait. | no |
| 4000 | `protocol_error` | More frames than the pre-auth budget arrived before `auth`. | Send `auth` as the first frame. | yes |
| 4001 | `auth_required` | Authentication had not completed by the deadline: no `auth` frame arrived, or the one that did was still being checked. Also sent sooner when the server holds as many connections still authenticating as it may and closed this one to make room for another: yours was the oldest of the network holding the most, an IPv6 /48 or an IPv4 address. | Send `auth` as soon as `hello` arrives. | yes |
| 4002 | `auth_failed` | The key in `auth` is invalid or revoked (a key past its expiry, or one whose account is closed, counts as revoked), or your IP had spent its refused-key budget when the key's cached record had lapsed; `detail.code` says which. | Use a valid key. A key that is still valid can reconnect once its IP's budget refills, within a minute. | yes |
| 4003 | `auth_expired` | The account was suspended at `auth`, or the minute's check refused the key mid-stream: either it stopped authenticating (revoked or expired, or its account suspended or closed), or your IP had spent its refused-key budget when the key's cached record had lapsed; `detail.code` says why. | Use a live key. A key that is still valid can reconnect once its IP's budget refills, within a minute. | yes |
| 4005 | `connection_limit` | The account already holds as many sockets as its plan allows; `detail` has `limit` and `inUse`. | Close another socket, or reuse one. | yes |
| 4006 | `quota_exhausted` | The month's stream units are spent; `detail` has `used`, `included` and `resetsAt`. The count is the usage rollup's, which runs every fifteen minutes, so it trails what the socket has used by up to that long. The allowance is checked at `auth`, on a resume with `from`, and every minute. | Wait for `resetsAt`, or move to a larger plan. | yes |
| 4007 | `command_flood` | Too many frames were refused for rate in a short time. | Send fewer frames, then reconnect. | yes |
| 4008 | `slow_consumer` | The socket stopped reading: its buffer stayed over the limit too long; `detail` has the dropped total. | Read faster or narrow the filters, then reconnect and resume. | yes |
| 4009 | `heartbeat_timeout` | Nothing arrived from you within the silence limit: no frame, and no pong to the server's WebSocket pings, which come at least twice within it. | Answer the server's pings, as most WebSocket libraries do on their own, or send `ping` more often than the limit. Reconnect at once and resume. | yes |
| 4015 | `credential_in_query` | The upgrade URL carried a credential-shaped query parameter. | Rotate that key: it is now in logs. Send keys only in `auth`. | yes |

Every code an `error` frame carries is on [Errors](https://traktor.fun/docs/errors.md#stream-codes).
