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.
- Credential
- None
- Subprotocol
traktor.market.v1
Limits
- Connections without a key8 at once, per IP
- Queries your connections without a key read32 at once by default, per IP
- Connections256 at once, per server process
- Subscriptions16 at once, per connection
- Commands60 in 10 seconds, per connection
- Frame you send16,384 bytes, per connection
- Openings with a sign-in, once enforced60 a minute by default, per account
- Invalid keys30 a minute, per IP
- Stream-unit count refreshed every15 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 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.
{"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 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
wsConnectionscounts 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 refused429when 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'smaxSubscriptionsis 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,traderandtraderPortfolio. Another resource isinvalid_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: its seconds open count as connection seconds, and each
snapshot,updateorpatchit 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 allowance4006. A check refused because your IP spent its refused-key budget also closes4003, 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.
revisionrises 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.updatedAtis when the build finished, in Unix milliseconds.A rebuild that changed nothing sends
current, withoutdata. It renews the view'supdatedAt, but not a quote's own observation time insidedata: a quote that has expired stays expired.cursoris 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.
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 atpath, 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.keynames the fields whose values, joined by:, name a row.addholds new rows whole,changeholds[key, ops]pairs for rows that changed, their paths starting at the row and their ops any of these three (astocksgroup'stokensare a rows op inside its change), andorderlists every row's key in its new order whenever rows came, went or moved; a roworderleaves 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 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.