> ## Documentation Index
> Fetch the complete documentation index at: https://takeprofit.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> The wire contract for feed servers: WebSocket and SSE record formats, heartbeats, timestamp rules, replay requests, replay_done and platform limits.

# Feed protocol and limits

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](/docs/indie/External-data/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.

```json One record (WebSocket) theme={null}
{"type": "record", "time": 1723456789, "data": {"price": 101.5}}
```

* `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:

```txt One record (SSE) theme={null}
data: {"time": 1723456789, "data": {"price": 101.5}}

```

* 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:

```json Replay requests (WebSocket) theme={null}
{"replay": {"count": 100}}
{"replay": {"from_time": 1723450000}}
{"replay": {"from_time": 1723450000, "to_time": 1723460000}}
```

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:

```json End of replay (WebSocket) theme={null}
{"type": "replay_done"}
```

```txt End of replay (SSE) theme={null}
event: replay_done
data: {}

```

One window from start to finish, over WebSocket:

```txt A date window, start to finish theme={null}
platform → {"replay": {"from_time": 1723450000, "to_time": 1723460000}}
server   → {"type": "record", "time": 1723451200, "data": {"signal": 0.4}}
server   → {"type": "record", "time": 1723455900, "data": {"signal": 0.7}}
server   → {"type": "replay_done"}
           the connection closes, and no live records follow
```

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:

```py One connection, one answer (pseudocode) theme={null}
def on_connection(conn):
    replay = conn.first_frame_or_query_params()   # there may be no request at all
    if replay:
        for record in my_storage.range(replay):
            conn.send(record)
        conn.send({"type": "replay_done"})
        if replay.to_time:                        # a window: answer, then close
            conn.close()
            return
    stream_live(conn)                             # then just send new records
```

## Limits

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

| Limit               | Current value                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Maximum record size | 64 KiB; larger records are dropped                                                                                        |
| Clock skew          | A record timestamped more than 5 minutes into the future is dropped                                                       |
| Silence             | A stream quiet for longer than `stale_after` is marked stale; the connection is not closed                                |
| Replay phase        | Ends at your done frame, or at the first of: 10 seconds of silence, a 60-second deadline, 20,000 records, 32 MiB received |
