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

# Market stream

Subscribing to the traktor.market WebSocket, keyless or under an API key, v1 to v4, and what its frames and closes mean.

`wss://api.traktor.fun/v1/market/stream` keeps a market view current: each change arrives as the whole view again, or on v2 as a patch to the view you hold.

### `WS /v1/market/stream`

A market resource's whole view, kept current: a snapshot, then a replacement each time it changes. Keyless, or under a key that meters it in stream units.

- URL: `wss://api.traktor.fun/v1/market/stream`
- Credential: None
- Subprotocol: `traktor.market.v1`

Limits:

- 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

Upgrade refusals:

| Status | Body | Retry-After | When |
| --- | --- | --- | --- |
| 400 | `invalid_market_handshake` |  | Neither `traktor.market.v1` nor, where the server serves them, `traktor.market.v2`, `traktor.market.v3` or `traktor.market.v4` is among the offered subprotocols, or the URL carries a query string. |
| 429 | empty | 3 s | Keyless, your IP holds its connections; either way, the server is stopping, or at capacity with no room to make: no network's keyless connections (an IPv6 /48 or an IPv4 address) are two more than yours, none counted as yours under a key. |
| 400 | empty |  | The request offers `traktor.market.v1`, has no query string, and your IP and the server have room, but it is not a WebSocket upgrade. |
| 401 | `invalid_key` |  | The key is malformed or unknown, or your IP has spent its refused-key budget. |
| 401 | `key_revoked` |  | The key was revoked or has expired, or its account was closed. |
| 402 | `subscription_inactive` |  | The key's account is suspended. |
| 429 | `quota_exhausted` |  | The month's stream units are spent; `error.details` has `used`, `included` and `resetsAt`. |
| 429 | `connection_limit` |  | The key's account holds as many sockets as its plan allows, its trade stream sockets and these together; `error.details` has `limit` and `inUse`. |
| 503 | `try_later` | 5 s | The key or the account's sockets could not be checked or, while market data needs a sign-in, the sign-in or the ticket. |
| 401 | `sign_in_required` |  | Only while market data needs a sign-in, which it does not yet: a socket with no API key offered a ticket that is not one a Traktor sign-in minted, or already spent, as a `traktor.ticket.` subprotocol beside the market ones, or a Traktor or Telegram Mini App sign-in as a bearer that is unknown, expired or revoked. A ticket is spent by the attempt that offers it, whatever comes of it, so each attempt asks for a new one. A socket that offers neither opens, for the front door's three reads alone. |
| 429 | `rate_limited` | 60 s | Only while market data needs a sign-in: with no API key, your IP has spent its sign-in checks. |
| 429 | `rate_limited` |  | Only while market data needs a sign-in and the account budgets are enforced: the sign-in's account opened its sockets' worth for the minute, a ticket minted counting as one; `retry-after` says when the next may open. |

## Subscribing

Connect with the subprotocol `traktor.market.v1` and no query string. With no credential the socket is keyless; with an API key on the handshake it is metered, as [Under a key](#under-a-key) says. `hello` arrives at once, with `maxSubscriptions`, the subscriptions one socket may hold, `heartbeatMs`, the heartbeat interval, and `at`, the server's clock in milliseconds, which each heartbeat carries too. Judge a frame's `updatedAt` by that clock rather than your own.

Send `subscribe` with a `query`: a resource, and its parameters as a URL-encoded query string.

```json
{"op":"subscribe","id":"board","query":{"resource":"flow","params":"chain=bnb&window=240&views=tokens&sort=mcap&limit=40"}}
```

The resources and their keys are those [`GET /v1/public/market`](https://traktor.fun/docs/public-market.md#get-v1-public-market) takes. The command's `id` names the subscription. The server answers `subscribed`, with the query in its canonical form, then `reset` with `reason: "snapshot_required"`, then a `snapshot`. `unsubscribe`, with the subscription's name in `sub`, closes it. A refused command gets an `error` frame and leaves the socket open.

Without a key, your IP's connections together may read an eighth of the server's market queries, the count on the endpoint block. A query counts toward every IP whose connections without a key read it, and toward the account of every key whose connections read it. Past your share, a `subscribe` that would count toward it is refused `subscription_limit`. When every query the server holds has a reader, it makes room for a new query by closing a subscription of an IP or account that reads at least as many queries as the new reader allows: its whole share for an HTTP read, two for a metered REST read under a key, and two more than the asking connection's IP or account for a connection. A query only that IP or account reads goes first, the biggest reader's that has one; then a query every reader of which reads that many, the one the fewest share, and for a connection never one its own IP or account reads. That subscription receives `error` with its `id` and `stream_capacity`, and nothing more. Subscribe again after a wait.

## Under a key

Send `Authorization: Bearer <key>` with the handshake to open the socket under an API key. A browser's WebSocket cannot set that header, so this is for a client that can. Any other `Authorization` header leaves the socket keyless. The key is checked before the upgrade, and a refusal is an HTTP answer with an `error` code, such as `401 invalid_key` for a key we do not know; the endpoint block lists every one.

- The socket is one of the account's sockets: the plan's `wsConnections` counts these and the trade stream's together. Your IP's keyless connections neither cap it nor give up a slot to it, but it shares the server's own connection cap with them: on a full server it opens by closing the oldest keyless connection of the network holding the most, an IPv6 /48 or an IPv4 address, when that network holds at least two, and is refused `429` when none does. Its subscriptions count toward your account, not your IP: the plan caps them rather than a share, but when the server holds as many queries as it can, it makes room from your account's queries as it would from an IP's (above), and a new query of yours takes room only from an IP or account reading at least two more.
- `hello`'s `maxSubscriptions` is the plan's subscriptions per connection, at most the server's own cap. A plan change holds the next subscription to the new plan's; those open stay.
- A subscription takes only the views the paid API reads: `flow`, `tape`, `stocks`, `assets`, `feed`, `token`, `tokenSignals`, `traderDirectory`, `trader` and `traderPortfolio`. Another resource is `invalid_query`, and so is a chain, app, DEX signal, launchpad or signal type name the grammar does not know, which a keyless socket drops.
- It draws stream units by the formula on [Plans](https://traktor.fun/docs/plans.md#stream-units): its seconds open count as connection seconds, and each `snapshot`, `update` or `patch` it is sent as one event delivered. Every other frame draws nothing.
- Every minute the server checks the key and the month's stream units. A key that stopped authenticating closes the socket `4003`, and a spent allowance `4006`. A check refused because your IP spent its refused-key budget also closes `4003`, as on the trade stream; a key that is still valid can reconnect within a minute.

The views are the ones a keyless socket receives, built once for every reader: a key meters the connection and holds it to the account, and never changes the data. So a `flow` view with `series` carries `seriesLeaders`, each Net flow bar's top inflow and outflow token, under a key as without one, where the REST routes under a key leave it out.

## Views

`snapshot` and `update` carry `sub`, `revision`, `cursor`, `updatedAt` and `data`, the whole view. An `update` replaces the view you hold; never merge it into the old one.

- `revision` rises each time the view changes. It numbers the server's shared copy of the view, not anything in the database, so compare revisions only within one subscription.
- `updatedAt` is when the build finished, in Unix milliseconds.
- A rebuild that changed nothing sends `current`, without `data`. It renews the view's `updatedAt`, but not a quote's own observation time inside `data`: a quote that has expired stays expired.
- `cursor` is the position in the server's change notifications that the build began from. It does not replay trades: to receive trades as Traktor stores them, use [the stream](https://traktor.fun/docs/stream.md).

## Patches (v2 and v3)

Offer `traktor.market.v2` beside `traktor.market.v1` to receive what changed instead of the whole view, where the server serves v2. Offer `traktor.market.v3` first to receive a shorter order, described below. `hello`'s `protocol` says which you got.

On v2 and v3, `snapshot` also carries `seq`, and a `patch` carries `sub`, `seq`, `revision`, `cursor`, `updatedAt` and `ops`. Apply a patch only when its `seq` is one more than the last frame's. On a gap, or an op that does not fit what you hold, unsubscribe and subscribe again for a fresh snapshot.

- `["set", path, value]` sets the field at `path`, a list of field names from the view's root.
- `["del", path]` removes it.
- `{"rows": path, "key": [...], "add": [...], "change": [...], "order": [...]}` changes an array of rows with an identity. `key` names the fields whose values, joined by `:`, name a row. `add` holds new rows whole, `change` holds `[key, ops]` pairs for rows that changed, their paths starting at the row and their ops any of these three (a `stocks` group's `tokens` are a rows op inside its change), and `order` lists every row's key in its new order whenever rows came, went or moved; a row `order` leaves out is gone.

v3 is v2 with one difference: a rows op names its new order with `spans` where v2 sends `order`. `spans` lists runs of the rows you held before the patch, each `[start, count]`, the `count` rows from position `start` in the order you held them, and the key of each added row as a string, all in the new order. A board of 800 rows where one row moved names at most four runs, not 800 keys.

A v2 or v3 subscription takes a whole snapshot again at most a minute after its last one, and whenever a patch would be no smaller than the view. A `traderPortfolio`, `tokenHolders`, `tokenSwapTraders`, `tokenCandles` or `pool` subscription is the exception: it is built once when it opens and not rebuilt while it is held, so resubscribe for a newer one.

A tape turns over too fast for a patch to help. On v2 and v3, add `"rows"` (1 to 500) to `subscribe` to receive only the first rows of the view's `tape`, sent whole with each build; a v1 socket refuses the field.

## Binary frames (v4)

Offer `traktor.market.v4` first, where the server serves it, to receive v3 with each `snapshot` and `patch` sent as one binary frame. Everything above that holds for v3 holds for v4. The frame is four bytes giving the header's length in bytes (big-endian), then the header, the frame's JSON object without its `data` or `ops`, then that field's value as gzipped JSON. In a browser, inflate it with `DecompressionStream("gzip")`. Every other frame stays text, and no frame on a v4 socket is compressed with permessage-deflate.

## Stale and unavailable

When a rebuild fails while an earlier build of the view still stands, you keep that view and receive `stale`, with `sub`, `revision`, `updatedAt`, a `code` and `retryAfterMs`. Keep the subscription: the next `snapshot`, `update` or `current` ends the stale state. With no view built yet, the frame is `unavailable`, with a `code` and `retryAfterMs`, the wait before the next try. [Frames](#frames) lists the codes.

After the server loses its feed of changes, each subscription receives `reset` with `reason: "upstream_reconnect"`, then a fresh `snapshot` once its view is rebuilt: replace what you hold with it. Subscriptions to `tokenHolders`, `tokenSwapTraders`, `traderPortfolio`, `tokenCandles` and `pool` are exempt, because other providers answer them, and so is `tokenPoolSwaps`, which the every-swap feed rebuilds on its own changes.

When you reconnect after a failure, add random jitter to the delay, so one outage does not reopen every socket at once.

## Frames

You send:

| Op | Fields | What it is |
| --- | --- | --- |
| `subscribe` | `id, query: { resource, params }, rows?` | Opens a subscription, named by this frame's `id`, on one public market resource; `params` is its URL-encoded query. On v2, v3 and v4, `rows` (1 to 500) asks for only the first rows of the view's `tape`, sent whole each build. |
| `unsubscribe` | `id, sub` | Closes a subscription. |
| `ping` | `id` | Asks for a pong, and counts as a sign of life. |

The server sends:

| Op | Fields | What it is |
| --- | --- | --- |
| `hello` | `protocol, reset, maxSubscriptions, heartbeatMs, at` | Sent on connect: the protocol the server chose, how many subscriptions the connection may hold (under a key, the plan's subscriptions per connection, at most the server's), how often heartbeats come, and the server's clock, as on a heartbeat. |
| `subscribed` | `id, sub, query` | The subscription is open, under the canonical form of its query. |
| `reset` | `sub, reason` | A fresh snapshot follows: `snapshot_required` after subscribing, `upstream_reconnect` after the server lost its feed. |
| `snapshot` | `sub, revision, seq?, cursor, updatedAt, data` | The whole view, the first time or after a reset. On v2, v3 and v4 it carries `seq`, and comes again at most a minute after the last, except on a `traderPortfolio`, `tokenHolders`, `tokenSwapTraders`, `tokenCandles` or `pool` subscription, which is built once when it opens and not rebuilt while it is held. On v4 it arrives as a binary frame, its `data` gzipped. |
| `patch` | `sub, seq, revision, cursor, updatedAt, ops` | Sent on v2, v3 and v4: the changes since the frame before, to apply to the view you hold. Apply it only when `seq` is one more than the last frame's. On v3 and v4 a rows op names its new order with `spans`, runs of the rows you held, where v2 sends `order`. On v4 it arrives as a binary frame, its `ops` gzipped. |
| `update` | `sub, revision, cursor, updatedAt, data` | The whole view again, replacing the last one; `revision` grew. |
| `current` | `sub, revision, cursor, updatedAt` | The view you hold is still the newest. |
| `stale` | `sub, revision, updatedAt, code, retryAfterMs` | The view you hold stands, but its rebuild failed; `code` says why. |
| `unavailable` | `sub, code, retryAfterMs` | No view could be built yet; `code` says why and `retryAfterMs` when the next try is. |
| `error` | `id?, code` | A refused frame. |
| `pong` | `id` | The answer to `ping`. |
| `unsubscribed` | `id, sub` | The subscription is closed. |
| `heartbeat` | `at, ready, cursor` | Every heartbeat interval: whether the feed is ready and its newest cursor. |

Codes on `stale` and `unavailable`:

| Code | Meaning |
| --- | --- |
| `market_fold_pending` | A committed batch of trades is still folding into the view; the next try is a second away. |
| `market_warming` | The view has not been built yet. |
| `filter_too_broad` | The query's `minBalance` floor selects more wallets than the views screen; raise it. No retry changes it before the wallet scan refreshes, up to a minute away. |
| `market_unavailable` | The build failed for another reason. |

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

Answer each `heartbeat` with a `ping`. Heartbeats confirm the connection and never advance a view's `updatedAt`.

## Close codes

The market socket sends no `bye`. After any close, reconnect, subscribe again, and replace what you hold with each new `snapshot`: nothing carries over. Do what the code's row says before you reconnect.

A frame over 16,384 bytes closes the socket `4007`. One over 65,536 bytes never reaches the server: the transport drops the connection with no close frame, and your client sees `1006`.

Safari, and every browser on iOS, can report the server's close as `1006` rather than its code: treat a `1006` you did not cause as any other close, and reconnect.

| Code | Reason | Meaning | What to do | `bye` first |
| --- | --- | --- | --- | --- |
| 1001 | `deploy` | The server is stopping. | Reconnect; every subscription starts again from a snapshot. | no |
| 1011 | `connection_failed` | Sending to the socket failed. | Reconnect. | no |
| 1013 | `stream_capacity` | Your IP holds as many keyless connections as it may, or the server holds as many connections as it may, or the server is stopping. Sent to a socket already open, the server was full and closed it to make room for a new one: it was the oldest keyless connection of the network holding the most, an IPv6 /48 or an IPv4 address. | Reconnect after a wait. | no |
| 4003 | `auth_expired` | Under a key: the minute's check refused the key. 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. While market data needs a sign-in, which it does not yet: the minute's check found the Traktor or Telegram Mini App sign-in the socket opened on revoked or expired, or its account suspended or closed. | Use a live key. A key that is still valid can reconnect once its IP's budget refills, within a minute. Under a sign-in, sign in again and open a new socket. | no |
| 4006 | `quota_exhausted` | Under a key: the month's stream units are spent, checked every minute against the usage rollup, which runs every fifteen minutes, so the socket can run past the allowance by up to that long. | Wait for the period to reset, or move to a larger plan. | no |
| 4007 | `command_limit` | Too many commands in a short time, a binary frame, or a frame over the inbound size limit. | Send fewer, smaller text frames. | no |
| 4008 | `snapshot_required` | A view outgrew the frame limit or the socket fell behind; nothing was queued behind it. | Reconnect; each subscription starts from a fresh snapshot. | no |
| 4009 | `heartbeat_timeout` | Nothing arrived from you for three heartbeats: no frame and no WebSocket pong. The server sends no WebSocket ping here, and one you send does not count. | Answer each `heartbeat` by sending the `ping` op. | no |

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