Skip to main content
This page is for the author of the feed server. The Indie side — declaring the source and what the script sees — is in Live data from a feed. A feed is a service you host at a public address. The platform connects to it, reads records and delivers them to every subscribed indicator. One address can serve many indicators of many users: the platform shares connections and fans records out on its side, so your server sees connections, never the audience. Do not assume exactly one connection: platform replicas and redeploys can hold several at a time. There are two capability levels:
  • Level 1, required: accept a connection and send records.
  • Level 2, optional: answer replay requests. They fill the platform’s record buffer on a cold start, and they are how a run launched from a date, or over a date window, gets that range.

Transports

The URL scheme in the source descriptor selects the transport:
  • WebSocket (wss://). The connection itself is the subscription. The platform sends no greeting; the only frame it can ever send is a replay request (level 2).
  • SSE (https://). A GET request with Accept: text/event-stream; the response body is the stream.
Requirements for both: a secure scheme (ws:// and http:// are rejected), a valid certificate, and a publicly reachable address — private and internal networks are refused. Credentials cannot be embedded in the URL (wss://user:pass@... is rejected); to restrict access, use a signed token in the query string. On ordinary connections the URL is used unchanged. A connection that carries a replay request (level 2) re-assembles the query to add the replay_* parameters, which can reorder your parameters and normalize their encoding — so sign parameter values, not the raw query string, and do not use query parameter names starting with replay_ for your own purposes. The platform reads your HTTP answer on the SSE response and on the WebSocket handshake. A 404 or 410 means the stream is gone for good: subscribed indicators stop with external_feed_gone. A 401 or 403 means unauthorized: they stop with external_feed_auth_failed. A 429 makes the platform back off and retry. Any other failure is retried with increasing delays and never reaches the indicator as an error.

Records

Over WebSocket, every frame is a JSON object with a type field. Frames without type, and types other than the ones below, are ignored.
One record (WebSocket)
  • time is the record’s timestamp: Unix seconds, UTC. A fractional part is allowed.
  • data holds the record fields as one flat object. Names match the dataclass schema declared in the script, and a field of a nested dataclass uses a dotted name, exactly like a CSV column: "data": {"signal": 0.42, "risk.beta": 1.1}. A record that does not match the schema — an unknown field, a missing non-optional field, a nested object, a wrong value type — is dropped at intake, and the stream continues. An optional field may be absent or null.
  • An optional id field is read only for the platform’s diagnostics. When present, it must be a JSON string; any other type makes the whole record undecodable, and it is dropped.
Two other frame types exist:
  • {"type": "heartbeat"} resets the silence clock and does nothing else. Send heartbeats when the gaps between records can exceed the stale_after your consumers declare.
  • {"type": "error", "code": "...", "message": "..."} refuses the stream. On a live connection the platform stops serving this feed and subscribed indicators fail with external_feed_gone; use it when you want the platform to stop trying — for example, for a stream that no longer exists. On a short window connection (level 2) it only ends that window request.
Over SSE, each event carries the same JSON in data:, without the type wrapper:
One record (SSE)
  • The blank line after each event is required: it terminates the event.
  • Comment lines (starting with :) count as heartbeats.
  • The id: and retry: fields are ignored. The event: field is ignored too, with one exception: event: replay_done (level 2, below).

Two rules about timestamps

The platform orders, resumes and deduplicates the stream by the record timestamp, so two rules are hard requirements:
  1. Time does not decrease. Each record’s time is equal to or later than the previous one. A record older than the previous one is dropped.
  2. An equal timestamp is a revision. A record with the same time as the previous one replaces it. It is treated as a new version of the same logical record, not as a separate event.
Both rules cover everything one connection sends: live records, and replayed records together with the live records that follow them.

Answering replay requests (level 2)

The platform can ask for recent past. A feed that ignores these requests is a valid level-1 feed: everything works, indicators just get less warm-up. The one hard compatibility rule: never close the connection because of an unfamiliar frame or query parameter. Over WebSocket, when the platform wants past records, it sends one frame right after connecting — otherwise it sends nothing. The frame takes one of three forms:
Replay requests (WebSocket)
Over SSE, the same requests arrive as query parameters added to your URL: ?replay_count=100 or ?replay_from_time=1723450000&replay_to_time=1723460000. The semantics are identical for both transports:
  • Timestamps are integer Unix seconds, UTC. from_time is inclusive, to_time is exclusive.
  • count asks for the last N records.
  • A request with to_time asks for a closed window: send the window, no live part follows.
Today the platform sends two of the three forms: count, on the first connection it opens to you, and from_time + to_time for a launch from a date or a run over a date window. The from_time-only form is part of the protocol but is not currently sent; handle every form the same way, as in the pseudocode below. A request is also never repeated: after a disconnect the platform redials without a new replay request. Your answer is the matching past records, oldest first, in the ordinary record form — followed by a done frame:
End of replay (WebSocket)
End of replay (SSE)
One window from start to finish, over WebSocket:
A date window, start to finish
Rules around the answer:
  • Send what you have; nothing is a valid answer. With nothing to send, send the done frame right away.
  • For a window, a done-framed answer is final — an empty answer with a done frame means an empty window, and the platform does not substitute its own buffer. Without a done frame, the records that arrived are used as they are; the platform falls back to its buffer only when nothing usable arrived at all.
  • Always finish with the done frame. It is how the platform tells a whole answer from a cut-off one. Without it a warm-up waits out the silence timeout, and a run over a date window ends with an error instead of computing. The platform treats the frame as a signal, not a guarantee: it also ends the replay phase on its own silence timeout, overall deadline and size caps.
  • After the done frame, keep the connection open and continue with live records. The exception is a to_time window: answer it, send the done frame, and the connection closes — either side may close it first.
  • Records outside the requested bounds, or beyond the requested count, are dropped by the platform.
What your server actually sees. The platform turns user launches into a small set of requests, and the users themselves are invisible to you:
  • The first connection the platform opens to your feed carries a count request. It fills the platform’s own record buffer, which then serves live subscribers without asking you again. A reconnect does not repeat it.
  • A launch from a date arrives as a separate short connection with from_time + to_time, where to_time is the present moment. It closes after the answer.
  • A run over a date window arrives the same way, carrying the user’s own two dates.
  • Warm-up of a launch by history depth is served from the platform’s buffer and never reaches your server.
The whole level-2 server logic fits in a few lines:
One connection, one answer (pseudocode)

Limits

These are platform settings, not constants of the protocol; treat them as current defaults.