The trade tape, filtered your way, sold honestly.

Live decoded prints from robinhood, bnb and base — the same tape the traktor dashboard watches — over a cursor-paginated REST history and a push websocket. What you are paying for is not just the decoding: it is a contract about what the numbers mean, kept by software and stated on every row.

basehttp://localhost:8790/v1
coveragerobinhood · bnb · base — fomo and gmgn; axiom on bnb and robinhood
status● checking…
01

The first ten minutes

Keys are hand-minted while the customer count is small — you were given one that looks like tk_live_…. It is shown exactly once; we store only a keyed hash.

Minute one: the last prints over $1,000

# bare numbers are exact dollars; suffixes work: 1k, 1.5m
curl -s "$BASE/v1/flows?minUsd=1k&limit=5" \
  -H "Authorization: Bearer $TRAKTOR_KEY"

Every response is enveloped — { data, meta } — and every row carries its honesty fields: lagSeconds (may be negative), origin, blockTimeSource, flags[].

Minute three: the live socket

const ws = new WebSocket("ws://localhost:8790/v1/stream", ["traktor.v1"]);
ws.onmessage = ({ data }) => {
  const f = JSON.parse(data);
  if (f.op === "hello")
    ws.send(JSON.stringify({ op: "auth", id: "1", key: KEY }));
  else if (f.op === "ack" && f.re === "1")
    ws.send(JSON.stringify({ op: "subscribe", id: "2", sub: "whales",
                             filter: { side: "buy", minUsd: "5k" } }));
  else if (f.op === "ack" && f.re === "2")
    console.log("backfill anchor:", f.result.seq);   // ← §08
  else if (f.op === "events")
    for (const e of f.d) console.log(e.side, e.usdAmount, e.chain);
};

Minute ten belongs to the handoff — how you stitch history and live into one gapless stream. Read it before you build; it is the one integration detail everyone gets wrong on the first try.

02

The honesty contract

Most feeds fabricate the figures this one leaves null, and hide the lateness this one labels. These clauses are contractual: the software conforms to them, and a claim you can catch us breaking is a bug report we want.

2.1

Events are delivered in ascending seq — the order rows became durable in our database. Not transaction order, not block order, not wall-clock order. On this tape chain time cannot order anything: ~40% of rows disagree with arrival order, EVM block time is interpolated (and overshoots — negative lag is real and unclamped), and sixteen rows have shared one millisecond.

2.2

Lateness is a field, not a footnote. A third of rows arrive over a minute after their block time, some hours after (the sweep pass). Every event carries lagSeconds and origin: "live" | "sweep" | "backfill"; the maxLagSeconds filter is your "fresh prints only" knob — a max-age cut, not a liveness oracle.

2.3

A null never satisfies a numeric predicate — floor or ceiling. usdAmount is genuinely null on ~1.4% of robinhood rows; it is never zero-substituted, and it fails both minUsd and maxUsd. Want unpriced rows? Omit the bounds and filter on the explicit null yourself.

2.4

Wire money is exact dollars. ?minUsd=300 means three hundred dollars. Suffixed strings ("300k", "1.5m") are accepted on both transports. Robinhood buy notionals read ~8% low, systematically — flagged rbh_buy_understated, never silently corrected.

2.5

Nothing is dropped silently, and dropped events are never billed. A slow socket sheds oldest-first with counted notice frames and running totals on every heartbeat. Every server-initiated close is preceded by a bye frame carrying the detail the 123-byte close reason cannot.

2.6

A stale chain is a 200 with caveats, never a 503. /v1/meta/freshness states per-chain lag against published thresholds — it is how you distinguish "no trades happened" from "the chain stopped producing". Coverage is three chains, fomo and gmgn on each, axiom on bnb and robinhood; a filter naming anything else is accepted, empty, and caveated.

2.7

Retention is stated, not implied. Trade-level rows: 24 hours. Minute buckets: 7 days. A cursor below the floor answers 410 cursor_expired with a resumeCursor — never a quietly empty page.

2.8

Every figure says whose it is. /v1/flows and the flows stream channel carry only what our own decoding produced — that does not change. The market surface (§09) carries the dashboard's enrichment too, and every field on it is classified by source in /v1/meta/filters under market.provenance, dated by the clock of the source that wrote it. A third party's figure is presented as theirs and never corrected.

03

Authentication

One key is the credential for both transports. On REST it travels in the header, and only the header:

Authorization: Bearer tk_live_…

A key in a query string is a hard 400 key_in_query. Query strings live forever in proxy logs. On the socket, a credential-shaped query parameter on the upgrade URL closes 4015 credential_in_query — rotate that key; it is in log files now.

On the socket the key travels in the first frame after the upgrade (browsers cannot set headers on a WebSocket, and URLs are logged — the frame is what remains). You have 5 frames, 4 KiB and 5,000 ms to authenticate; then 4001.

At rest a key is an HMAC under a server-side pepper. Revocation bites on the next request; the in-process cache bound is 5 seconds, the absolute worst case 300. Unknown and malformed keys answer identically — there is nothing to enumerate.

04

One filter, one meaning

The same filter object drives /v1/flows and a socket subscription, and it means the same thing on both — enforced by one shared evaluator and proven by a replay harness before anything ships. Every predicate is a pure function of the row.

fieldtypedefaultmeaning
windowMinutesint 1–1008060lookback bound on blockTime; plan history gates depth
maxLagSecondsnumber · null3600max ingestedAt − blockTime age; null = unbounded
chainslistallrobinhood · bnb · base · solana — empty means all
platformslistallfomo · gmgn · pump · axiom — pump is in the vocabulary, untracked (Solana only)
sideenumallall · buy · sell
minUsd / maxUsdmoney · nullexact dollars; suffixes ok; nulls fail both (§2.3)
tokens / walletslist ≤ 50address allow-lists
vint1grammar version; a declared unknown version is refused

Flat conjunction only — list fields give OR within a dimension; there are no AND/OR/NOT trees. The machine-readable grammar, with ranges, costs and the caveat vocabulary, lives at /v1/meta/filters.

05

REST /v1

routepurpose
GET /v1/flowskey the tape — cursor-paginated both directions, full filter vocabulary
GET /v1/flows/{id}key one print by natural key chain:tx:leg (URL-encoded)
GET /v1/stream/positionkey current head seq, for backfill-before-connect clients
GET /v1/meta/freshnesskey per-chain lag, counts, state: live|stale|down with stated thresholds
GET /v1/meta/filtersopen the grammar, machine-readable: units, ranges, defaults, coverage, costs
GET /v1/plansopen tiers and limits — the numbers below, from the running service
GET /v1/usagekey this key's consumption in the current period
GET /v1/market…key the dashboard's views — combined or one at a time (§09)
GET /v1/pools/{chain}/{token}key liquidity depth segments for the deepest readable pool (§09)
GET /v1/alert-capabilitiesopen closed alert grammar, work limits and current coverage health
GET·POST·PATCH·DELETE /v1/alerts…key / session private rule CRUD, disabled duplication and bounded dry-run preview
GET·POST /v1/notifications…key / session private keyset feed, unread count and read receipts

Ordering: the arrival axis

Both directions walk (ingestedAt, _id)"the last N trades" means last N by arrival, because on this tape chain time runs backwards for four rows in ten (§2.1). order=desc (default) pages back from now; order=asc tails forward, holding back the newest ~2 seconds so a row can never land beneath a cursor you already hold.

Cursors

Two kinds, one conversion point, all opaque:

kindlooks likewhere it comes from
seq1-1788252174973-0the socket ack, /v1/stream/position — accepted verbatim as cursor=
continuationc1_aa_…​.…meta.page.cursor — signed; carries your filter's fingerprint and the frozen floors

Re-send the identical filter set on every page (400 cursor_query_mismatch otherwise); only limit may change. An empty ascending page echoes its position — poll the same cursor every 1–5 s to tail over REST. Tampering earns a flat 400 invalid_cursor that never says which check failed; a position under the retention floor earns 410 with a details.resumeCursor.

Money, on every answer

RateLimit-Limit / -Remaining / -Reset ride every response; X-Traktor-Cost says what the request billed (a 304 bills zero — ETags hash the data, so conditional polling is nearly free). /v1/flows costs 1 + ceil(limit/100) units; point reads cost 1; meta routes are free but still rate-armed.

06

Private alerts

Build up to 100 account-scoped rules from the closed vocabulary at /v1/alert-capabilities. Rules combine up to 12 conditions, scope up to 50 token or wallet addresses, and choose crossing, repeat or one-shot delivery. Matches arrive in the private notification feed; edits use an optimistic revision, and duplicates start disabled.

routemeaning
POST /v1/browser-sessions/exchangeexchange an API key for a random 12-hour bearer
GET|DELETE /v1/browser-sessions/currentinspect or revoke the presented browser session
GET|POST /v1/alertslist or create rules
GET|PATCH|DELETE /v1/alerts/{id}read, revision-update or delete one rule
POST /v1/alerts/{id}/duplicate|testcopy disabled or preview a bounded current sample
GET /v1/notificationsnewest-first feed on (createdAt,id)
GET /v1/notifications/unread-countexact unread count
POST /v1/notifications/readmark selected IDs or all account rows read
POST /v1/telegram/link-codesmint a one-time code and its t.me deep link for the Telegram bot
GET|DELETE /v1/telegram/links[/{chatId}]the Telegram chats linked to the account; unlink one
POST /v1/browser-sessions/telegrama Mini App signs in: verified Telegram initData for a linked chat mints the 12-hour bearer

A browser bearer is stored only as a SHA-256 hash, is bound to its source API key, and becomes invalid as soon as that key is revoked or expires. The web proxy keeps it in an HttpOnly, SameSite=Strict, production __Host- cookie. Preview is read-only and bounded to six calls/minute, burst three, 250 candidate subjects and a three-second Mongo budget.

Coverage loss is visible. Per account: 300 detailed feed rows/hour and 10,000 exact event/rule evaluations/fixed minute. Per stateful rule: 1,000 tracked subjects. Overflow becomes a cumulative unread feed summary; it is never silently reported as a non-match. Feed rows and dormant state are retained for 90 days.

07

The socket

/v1/stream, subprotocol traktor.v1. Text frames, one JSON object each, every frame { op, id }, responses echo re. Exactly one terminal answer — one ack or one error — per client frame. The server refuses unknown fields (a typo must not silently widen a filter); you ignore unknown server ops.

client → server
auththe key, first frame, nowhere else
subscribefilter + optional from seq + delivery shape
unsubscribeone name or an atomic list
subsfull live state — ask, don't guess
pingkeepalive; payload echoed
byeclean close; queued events flush first
server → client
hellopre-auth: budgets, heartbeat params
ack / errorthe terminal answers
eventsbatches; seq = last element — persist it
heartbeat15 s; per-sub seq/dropped/queued
noticedrops, pressure, plan changes — never silent
byeprecedes every server-initiated close

Resume

Reconnect and subscribe with from: <last seq you processed>. A same-epoch, in-window span replays exactly — no gap, no duplicate, because every v1 predicate is pure. A trimmed span, a stale epoch, or a resume outside your plan's replay window is answered on the ack (resumed: false, gap: { reason }) — never a silent hole. Backfill the gap over REST from the new anchor and dedupe on id.

Flow control

If your socket cannot keep up, the oldest queued events for that subscription drop — counted, noticed within a second, cumulative on every heartbeat, and never billed. Shedding never closes the connection; 4008 is reserved for a socket that stops reading TCP entirely. Under pressure the server may widen your batching (and says so); it never narrows it. Send any frame each 30 s — a ping works; 45 s of silence closes 4009, retry at once.

08

The handoff

How to stitch history and live into one stream without a gap — the one integration detail three separate designs got wrong in three different ways, settled:

1. subscribe FIRST — the ack's result.seq is your anchor
2. page GET /v1/flows?cursor=<that seq> backwards, same filter
3. dedupe on id across the seam

Gap-free by construction: the socket is attached before the backfill query runs, so nothing inserted during the backfill can fall between the halves. Not duplicate-free, on purpose: the seam is at-least-once. Size your seen-set honestly — twenty minutes of paging at the measured peak (~1,750 rows/min) is roughly 35,000 ids, not a handful.

Prefer backfill-before-connect? GET /v1/stream/position hands you the head seq with the same dedupe obligation. The socket-first recipe above remains the documented, tested path.

09

The market surface

Everything the dashboard shows, on the same key. GET /v1/market serves several views in one body — views= any of summary, tokens, series, tape, rotation, wallets — and every view is also its own route:

routeunitsserves
/v1/marketΣ viewsthe views you name; summary is always served and always billed
/v1/market/summary1header counters over the filter
/v1/market/tokens3the token board, volume-ranked — limit 1–1000 (200), offset to 50,000, meta.page
/v1/market/series2net flow per minute by chain and trading app
/v1/market/tape2trades with names, tickers, market cap and wallet stats — limit 1–500 (120)
/v1/market/rotation5sell-then-buy links — withinMinutes 1–240 (30), limit 1–200 (40)
/v1/market/wallets3the wallet board with Cielo's imported opinion — limit 1–500 (120)
/v1/market/whales3wallets at or above a minWalletBalanceUsd floor — required here, and not a views member
/v1/pools/{chain}/{token}10depth segments for the deepest readable pool — robinhood or bnb

One grammar — chains, platforms, windowMinutes, side, minTradeUsd, minWalletBalanceUsd, launchpads, dexSignals, feeSharing, stockPairs and every screen by its key — published with units, bounds, costs and provenance at /v1/meta/filters under market. Each route reads only what it can act on and refuses the rest by name (400 unknown_parameter, with details.accepts): side and minTradeUsd screen single trades, so only tape reads them. dexSignals takes paid (a paid DexScreener profile and social update), boost (boost spend), and cto (a GMGN-reported community takeover); the choices are OR within the facet, and every view that reads launchpads reads it too. Bucket-backed views take every window on every plan; tape, rotation and wallets read raw trades and are gated by your plan's history (403 plan_forbids). A balance floor selecting more than 5,000 wallets is 422 filter_too_broad before anything runs.

curl -s "$BASE/v1/market?views=tokens,series&windowMinutes=60&launchpads=flap&minNet=5k" \
  -H "Authorization: Bearer $KEY" | jq '.data.tokens[:3] | map({ticker, netUsd, marketCap, metadataAt})'

Fresh market reads and ranking

Market REST and public subscriptions share one committed-data producer. Token rows refresh after committed changes; advancing the minute rebuilds the rolling window. Responses use private, max-age=1 and date their build with meta.asOf. Token routes accept sort, dir=asc|desc, named=1 and tail=1; tail cannot combine with a nonzero offset. Supported market windows are 5, 15, 30, 60, 240, 1440 and 10080 minutes.

Public market streaming

GET /v1/public/market?resource=flow&query=... returns bounded public resource JSON. Resources are flow, quote, token, saved, stocks, assets and feed. The query uses the web vocabulary (chain, platform, window, screens and ranking), capped at 24 hours.

Connect to /v1/market/stream with subprotocol traktor.market.v1, without credentials or URL parameters. Subscribe with the same query. The server sends reset, then a complete snapshot; update replaces the whole view. Each carries sub, revision, cursor, updatedAt and data. An unchanged rebuild sends current with the same revision and a new freshness time. Send ping on heartbeats. Reconnect and resubscribe after a reset or lost connection; market cursors do not promise execution-level replay. The authenticated traktor.v1 protocol continues to serve raw trades, and rejects market filters such as maxMcap.

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

Identical public queries share a producer. Limits per process: 256 sockets, eight per IP, 16 subscriptions per socket, 96 groups, three simultaneous reads and 4 MiB response frames. Slow readers reconnect for a new snapshot. Trading commands retain authenticated, idempotent HTTP endpoints.

Two clocks on the row

Current valuation carries valuationSource, marketPriceUpdatedAt and valuationExpiresAt, covering verified pool prices and provider fallback. liquidity, ticker and logo are DexScreener's, dated by metadataAt; launchpad, graduated, athPrice and both social signals are GMGN's, dated by gmgnAt (dexscreenerSocialsUpdated is named for what GMGN reports on, not for who reported it). Our own numbers — flow, buyers, holders, holder-reward verification — are as fresh as the tape. The whole table is market.provenance.

Pool depth

GET /v1/pools/{chain}/{token} returns the deepest readable pool as exact per-range segments — USD price bounds, the tokens sitting in the range, and the USD that must flow to cross it — so binning it to a chart never refetches. Ten units, because a cold read is ~40 RPC calls; the 10-second cache makes the second reader nearly free. Failures name their cause: 404 pool_not_found, 422 pool_unreadable, 502 upstream_failed with details.source.

10

Plans & limits

These numbers are read live from /v1/plans when you load this page; the ack's limits object is authoritative at runtime — read it, don't hard-code this table.

limitFreePro
price / month$0$99
REST units / minute30600
REST units / month20,0005,000,000
history window60 min24 h
max rows / page1001,000
socket connections110
subscriptions / connection250
events / second5300
stream units / month60,0001,000,000

The feed's measured mean is ~15.6 events/s with bursts to 29/s: Pro carries the firehose; Free deliberately cannot on a wide filter — the ack warns (rate_cap_below_feed) and drop notices make the shedding visible, never silent. Non-payment downgrades entitlement to Free; it never revokes your key.

11

Errors & close codes

Every REST error is { error: { code, message, requestId, details, docs } }code is stable forever and the only thing to branch on. The load-bearing ones:

httpcodewhat to do
400key_in_querymove the key to the Authorization header; consider it leaked
400invalid_cursor / cursor_query_mismatchstart over / re-send the minting filter set
401invalid_key / key_revokedcheck the key; revoked keys stay revoked
402subscription_inactivefix billing; the key still exists
400unknown_parameterthis route does not read that key; details.accepts lists what it does
400screen_conflict / window_out_of_rangethe band crosses itself / that window is not one of the six
403plan_forbidsnarrow windowMinutes to your plan's history
410cursor_expiredresume from details.resumeCursor
422filter_too_broadnarrow the filter or the window before it runs
429rate_limitedwait Retry-After — it is exact
429quota_exhaustedwaiting will not help; see details.resetsAt
404 · 422 · 502pool_not_found / pool_unreadable / upstream_failedno pool yet / the chain would not read it / DexScreener or the RPC failed — all refunded
401session_required / session_expiredconnect or renew the browser alert session
409alert_limit_reached / alert_revision_conflictdelete a rule / refresh before retrying the edit
409alert_once_completedchange the rule or duplicate it to arm a fresh one-shot
429preview_rate_limitedwait Retry-After before another dry run

Socket close codes

Every server-initiated close is preceded by a bye frame with detail and retryable. An unknown 4xxx code is not retryable unless its bye said so.

coderetry?
1001deploy — resume from the bye's lastSeqafter retryAfterMs
4001no auth within the deadlineat once
4002auth failedno
4003subscription lapsed mid-connectionafter billing
4005connection allowance heldclose another first
4006monthly stream allowance spentat resetsAt
4008socket stopped reading for 30 snarrow the filter
400945 s inbound silenceat once
4015credential in the upgrade URLrotate the key