Skip to main content

Which keys may connect

The stream checks the key before the upgrade. A refusal is an HTTP status with the error body every REST call uses, listed under Upgrade responses. While free keys are switched off, a key on a free plan gets 403 PERMISSION_DENIED. The stream checks the key again before every minute charge, through an authentication cache that holds a key for up to 30 seconds. A key that was revoked or expired, or lost the scope or the plan, gets an error frame and the socket closes with 1008.

Server frames

Every frame is a JSON object with a type; schema StreamServerFrame. Ignore types and fields you do not know: new ones arrive as additive changes.

ready

Once, after the upgrade and the first minute charge. connectionId names this connection in our logs, and error frames carry it as requestId.

subscribed

After every accepted subscribe. subscription is what you receive from now on, normalised: duplicates dropped, EVM token addresses lowercased, and platforms or tokens set to null when they do not filter. replaced is the subscription it replaced, or null.

unsubscribed

After every unsubscribe. replaced is the subscription it cleared, or null.

post

One per delivered post. item is the post envelope, the same object GET /v3/posts returns (schema PostEnvelope). cursor is the stream cursor of this post: store the last one you processed.

replay_done

After the last replayed post and before the first live one. cursor is the newest cursor the replay sent, or your own since when nothing was replayed.

error

A frame the stream rejected, or the moment before the stream closes the socket. Act on code; message is for people. requestId is the connection id.

Error frames

At most 10 error frames in 10 seconds answer rejected frames; past that a rejected frame changes nothing and gets no answer. The replay window error, OVERLOADED for a subscribe, and the error before a close always arrive.

Replay with since

  • Every post frame carries a cursor (s_…). Store the last one you processed and send it back as it is; do not parse it.
  • A subscribe with since sends the posts published after that cursor which match the new filters, oldest first, then replay_done, then live posts. Live posts published while the replay loads wait and follow replay_done; a post in both arrives once.
  • The replay window is the last 5 minutes by publication time. A since older than that gets the error frame INVALID_ARGUMENT “The cursor is older than the replay window.”, then subscribed, and no replay_done: live posts flow, and you fill the gap from GET /v3/posts, deduplicated on item.id.
  • A since that names no post gets INVALID_ARGUMENT “The cursor is not valid.”, and the subscription does not change.
  • Stream cursors work only here. The c_ cursors of REST collections are a different thing.

Ordering and duplicates

  • Posts arrive in the order we published them, which can differ from canonical.postedAt. Sort by postedAt when you need the author time order.
  • On one connection a post arrives once. A subscribe without since continues after the newest post the connection already sent, so it repeats nothing.
  • A subscribe with since replays everything after that cursor, posts this connection already delivered included. Reconnect with the last cursor you processed and nothing repeats; an older cursor repeats posts.
  • Deduplicate on item.id wherever you merge the stream with REST pages or with a replay from an older cursor.
  • The stream sends a post once, when we publish it. Later edits, counts and hides send no frame; read the post from REST for its current state.

Heartbeats

  • The server sends a WebSocket ping every 30 seconds. Browser and Node ws clients answer with a pong on their own; a hand-written client must answer itself.
  • The stream closes a socket it heard nothing from (no pong, no frame) for 90 seconds with 1011.
  • Watch the other direction too: when neither a ping nor a frame arrives for 75 seconds, the path is dead. Terminate the socket and reconnect.

Limits

Close codes

Any other code, or a drop without a close frame (1006 in most libraries), means the network path broke: reconnect with backoff and since.

Reconnect correctly

  1. Store the cursor of each post once you have processed it.
  2. On close, read the code. 1008: stop and fix the key or the balance. 1009: fix the client. Anything else: reconnect.
  3. Reconnect at once after 1001. Otherwise wait 1, 2, 4 and up to 30 seconds between attempts, with jitter. A refused handshake with 429 or 503 names the wait in Retry-After; 401, 402 and 403 need a fix before a retry.
  4. After ready, send the same subscribe with since set to the stored cursor.
  5. Process the replayed posts, then replay_done, then live posts.
  6. When the replay window error arrives instead, fill the gap from GET /v3/posts and deduplicate on item.id.

Upgrade responses

The stream checks the key before the upgrade and answers a refusal with the error body every REST call uses.