- 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 withAccept: text/event-stream; the response body is the stream.
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 atype field. Frames without type, and types other than the
ones below, are ignored.
One record (WebSocket)
timeis the record’s timestamp: Unix seconds, UTC. A fractional part is allowed.dataholds 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 ornull.- An optional
idfield 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.
{"type": "heartbeat"}resets the silence clock and does nothing else. Send heartbeats when the gaps between records can exceed thestale_afteryour consumers declare.{"type": "error", "code": "...", "message": "..."}refuses the stream. On a live connection the platform stops serving this feed and subscribed indicators fail withexternal_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.
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:andretry:fields are ignored. Theevent: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:- Time does not decrease. Each record’s
timeis equal to or later than the previous one. A record older than the previous one is dropped. - An equal timestamp is a revision. A record with the same
timeas the previous one replaces it. It is treated as a new version of the same logical record, not as a separate event.
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)
?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_timeis inclusive,to_timeis exclusive. countasks for the last N records.- A request with
to_timeasks for a closed window: send the window, no live part follows.
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)
A date window, start to finish
- 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_timewindow: 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.
- The first connection the platform opens to your feed carries a
countrequest. 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, whereto_timeis 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.
One connection, one answer (pseudocode)