Transport
SEND exposes a single multiplexed WebSocket. You subscribe to topics over one connection rather than opening a socket per feed.
The two directions do not share an encoding. Client frames are JSON text. Server frames are MessagePack binary. There is no JSON mode for server frames, so a client that cannot decode MessagePack will see nothing but bytes.
- GET /api/v1/ws?ticket=<uuid> -- the authenticated feed.
- GET /api/v1/ws/public -- no authentication. The connection is given a guest identity and may subscribe to public topics only.
- POST /api/v1/auth/ws-ticket -- mints the single-use ticket the authenticated endpoint requires.
Connecting
Authentication happens before the upgrade, not on it. There is no Authorization header on a WebSocket handshake, so a short-lived ticket stands in for one.
- POST /api/v1/auth/ws-ticket with your bearer token. The response data is { ticketId }.
- Open the socket at /api/v1/ws?ticket=<ticketId>.
- The ticket is consumed atomically on first use and expires 30 seconds after it is issued.
- A ticket that is expired, unknown, or already used fails the upgrade with 401 and the message "invalid or expired WebSocket ticket".
- Mint a fresh ticket for every connection attempt, including every reconnect.
Sending and Receiving Frames
Four client message types exist, each tagged by a type field.
The server answers with data, subscribed, unsubscribed, error and pong frames. A data frame is { type: "data", topic, seq, data }, where seq is a per-topic monotonic counter you can use to detect a gap. It cannot be used to resume: there is no replay and no message history.
- {"type":"subscribe","topic":"balances"} -- subscribe. An optional filter object narrows parameterized topics, and tokenAddress is the only filter field, used by candles and prices.
- {"type":"unsubscribe","topic":"balances"} -- unsubscribe.
- {"type":"ping"} -- answered with a pong frame.
- {"type":"viewport","walletSetIds":[],"walletIds":[]} -- declares what your client currently displays so those balances stay warm. Fire and forget, with no acknowledgement.
Topics
Topic names are camelCase on the wire. Public topics are available on both endpoints; private topics are delivered only to the account that owns the underlying record.
- Public: swaps, prices, candles, tweets, publicSwaps, dexActivity, platformStats.
- Private: balances, portfolio, aging, volume, notifications, exchanges, launches, walletGeneration, projectUpdates, swapResults, swapSubmissions.
- balances and portfolio are routed by wallet ownership; the remaining private topics are routed by the user id carried on the record.
- Subscribing to a private topic on the public endpoint is refused with error code TOPIC_FORBIDDEN. A handful of further topics are reserved for operators and are refused with TOPIC_ADMIN_ONLY.
- An unrecognised topic name fails the frame parse itself, so it surfaces as INVALID_MESSAGE rather than a topic-specific error. exchangeStatus is one of these -- it was retired, and all exchange progress now arrives on exchanges.
The Socket Is a Delta Stream
Subscribing does not hand you current state. Only swaps sends an initial snapshot, and it is a single frame carrying the latest cached trade. Every private topic sends nothing at all until its next live update, which may be seconds or hours away.
Fetch a REST baseline first, subscribe, then apply frames on top of it. A client that starts from an empty object and waits for the socket to fill it renders blanks.
Delivery Guarantees
The socket is a performance layer, not a system of record. Design the consumer accordingly.
- Frames are best-effort and at-least-once. They can arrive twice, and they can be missed entirely across a reload, a disconnect, or a throttled background tab.
- Terminal swap results are re-emitted by the backend on purpose, so duplicates are expected. Dedupe on a stable id.
- Anything that has to be correct -- balances, batch outcomes, ledgers -- must be reconciled against a REST read.
Limits and Disconnects
The server pings every 30 seconds and drops a connection that has not answered within 10. A stalled peer is dropped after 10 seconds on a single send, and a connection that keeps failing to accept frames is closed as a slow consumer after 50 consecutive failures. Sessions are revalidated every 5 minutes, so a deactivated account's socket is torn down rather than left open.
- Five concurrent connections per user. At the cap a new connection is admitted and your oldest one is evicted -- the sixth connection does not fail, it displaces the first.
- Close code 4000, server shutting down -- reconnect with normal backoff.
- Close code 4001, session no longer valid -- do not reconnect; re-authenticate first.
- Close code 4002, slow consumer -- reconnect with extra backoff.
- Close code 4003, connection limit -- reconnect only after a hard minimum delay.
- Every close is preceded by an error frame naming the reason, and the public endpoint enforces its own connection limits, answering 429 RATE_LIMITED on the upgrade when they are exceeded.