TraktorAPI
Get API key

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

wss://api.traktor.fun

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

StatusBodyRetry-AfterWhen
400invalid_market_handshakeNeither 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.
429empty3 sKeyless, 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.
400emptyThe request offers traktor.market.v1, has no query string, and your IP and the server have room, but it is not a WebSocket upgrade.
401invalid_keyThe key is malformed or unknown, or your IP has spent its refused-key budget.
401key_revokedThe key was revoked or has expired, or its account was closed.
402subscription_inactiveThe key's account is suspended.
429quota_exhaustedThe month's stream units are spent; error.details has used, included and resetsAt.
429connection_limitThe key's account holds as many sockets as its plan allows, its trade stream sockets and these together; error.details has limit and inUse.
503try_later5 sThe key or the account's sockets could not be checked or, while market data needs a sign-in, the sign-in or the ticket.
401sign_in_requiredOnly 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.
429rate_limited60 sOnly while market data needs a sign-in: with no API key, your IP has spent its sign-in checks.
429rate_limitedOnly 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 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: 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.

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 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

OpFieldsWhat it is
subscribeid, 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.
unsubscribeid, subCloses a subscription.
pingidAsks for a pong, and counts as a sign of life.

The server sends

OpFieldsWhat it is
helloprotocol, reset, maxSubscriptions, heartbeatMs, atSent 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.
subscribedid, sub, queryThe subscription is open, under the canonical form of its query.
resetsub, reasonA fresh snapshot follows: snapshot_required after subscribing, upstream_reconnect after the server lost its feed.
snapshotsub, revision, seq?, cursor, updatedAt, dataThe 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.
patchsub, seq, revision, cursor, updatedAt, opsSent 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.
updatesub, revision, cursor, updatedAt, dataThe whole view again, replacing the last one; revision grew.
currentsub, revision, cursor, updatedAtThe view you hold is still the newest.
stalesub, revision, updatedAt, code, retryAfterMsThe view you hold stands, but its rebuild failed; code says why.
unavailablesub, code, retryAfterMsNo view could be built yet; code says why and retryAfterMs when the next try is.
errorid?, codeA refused frame.
pongidThe answer to ping.
unsubscribedid, subThe subscription is closed.
heartbeatat, ready, cursorEvery heartbeat interval: whether the feed is ready and its newest cursor.

Codes on stale and unavailable

CodeMeaning
market_fold_pendingA committed batch of trades is still folding into the view; the next try is a second away.
market_warmingThe view has not been built yet.
filter_too_broadThe 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_unavailableThe 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.

CodeReasonMeaningWhat to dobye first
1001deployThe server is stopping.Reconnect; every subscription starts again from a snapshot.no
1011connection_failedSending to the socket failed.Reconnect.no
1013stream_capacityYour 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
4003auth_expiredUnder 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
4006quota_exhaustedUnder 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
4007command_limitToo many commands in a short time, a binary frame, or a frame over the inbound size limit.Send fewer, smaller text frames.no
4008snapshot_requiredA view outgrew the frame limit or the socket fell behind; nothing was queued behind it.Reconnect; each subscription starts from a fresh snapshot.no
4009heartbeat_timeoutNothing 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.

    Market stream · Traktor API