# Live feed

Trades against a book that tracks the exchange right now: the CLI holds the exchange socket and feeds the simulator.

The live feed lets your bot trade against a book that tracks the real market right now. The CLI holds the exchange socket and pushes every frame into the simulator through `POST /_replay/feed`. So fills, positions and the WebSocket feed behave exactly as in [tape replay](https://www.rystic.ai/docs/simulators/kalshi/features/tape-replay.md). Nothing you send the simulator reaches Kalshi. The simulator holds no exchange socket. Its one call to Kalshi fetches the public record of a carried market it hasn't seen yet, and `RYSTIC_REPLAY_METADATA=off` turns that off.

<!-- example: skip network -->
```bash
rystic run kalshi-twin --live --name live
rystic run kalshi-twin --live -d --name live
rystic feed kalshi-twin --url http://localhost:8080
curl -s localhost:8080/_replay/status
# -> {"mode":"live","playing":true,"frames":18213,"last_ms":...,"lag_ms":412,...}
```

The first command boots the simulator, opens the live session and feeds it from the CLI process (Ctrl-C stops both). The second detaches, and `rystic ps` shows `live · 1 carried · lag 120 ms`. `rystic feed` feeds a simulator that already runs, on any runner and any machine. Only a simulator whose manifest declares a feed takes `--live`: today, `kalshi-twin`. `rystic describe kalshi-twin` lists it among its features as **Live feed**. [Live feed settings and Compose](https://www.rystic.ai/docs/simulators/kalshi/features/live-feed-settings.md) covers every flag and running it in a container.

### How the feed behaves

1. **No key setup.** The CLI carries a read-only exchange key, and `rystic doctor kalshi-twin` shows `live feed … built into this release`. To use your own key instead, set `KALSHI_KEY_ID`, or `KALSHI_KEY_ID_FILE` with a path to a file holding it. Pair it with `KALSHI_PRIVATE_KEY_FILE` (a path) or `KALSHI_PRIVATE_KEY_B64` (the PEM, base64). Use one form of each, never both. With none of them set, the CLI reads `/run/secrets/kalshi_key_id` and `/run/secrets/kalshi_private_key`, both or neither. The environment wins over `/run/secrets`, and `/run/secrets` over the built-in key. None of these is your Rystic API key or license.
2. **Nothing is carried until you pick.** The feeder offers the 500 busiest open markets to the Markets card in the [state UI](https://www.rystic.ai/docs/simulators/kalshi/features/browser-uis.md), and a market's frames start when you add it. `--live-carry N` pre-carries the busiest N, and `--live-markets A,B` carries exactly those. `--live-events E1,E2` carries every open market of those events, and `--live-series KXBTC15M,KXETH15M` narrows the offer to those series. Naming markets or events turns `--live-carry` off: the names are the whole selection, and the feeder log says the carry is ignored. The exchange is rescanned every five minutes, so a market that just opened shows up within that.
3. **`lag_ms` is the number to watch.** It's wall now minus the exchange stamp of the last applied frame. It includes the exchange's own delivery delay, which reaches tens of seconds at peak load on every socket at once. So more `--live-shards`, fewer markets or a reconnect doesn't lower it. The feeder's `[status]` line in `rystic logs --feed <name>` splits it. `upstream_lag_ms` is receive time minus the exchange stamp. A `lag_ms` that tracks it is the exchange running late, and one well above it is the feeder or the simulator falling behind. `post_ms` is each batch's round trip to the simulator, apply included.
4. **Paper-trading ceiling.** Your orders fill against the simulator's copy of Kalshi's live book and prints, but never move the real ones. Queue position is what replay already claims ([what replay does not model](https://www.rystic.ai/docs/simulators/kalshi/features/tape-format.md#what-replay-does-not-model)). Liquidity you take stays taken in the simulator's copy until recorded decreases account for it.
5. **What changes from replay.** There's no tape, so `status` has no `total`, `position` or `duration_ms`. It carries `frames`, `dropped`, `last_ms` and `lag_ms` instead. `seek` returns `409`, and `pause` drops frames instead of queuing them. `speed` is `1`.
6. **A carried market the simulator hasn't seen is minted on its first frame.** Its record is fetched then, so `close_time` and the expiration fields are the exchange's own ([where a replayed market's metadata comes from](https://www.rystic.ai/docs/simulators/kalshi/features/tape-format.md#where-a-replayed-markets-metadata-comes-from)). A bot that lists an event's markets right after startup can see only part of the event for a few seconds. So wait until every market has its book.
7. **The exchange's lifecycle frames reach your `market_lifecycle_v2` subscription, but don't settle the simulator's copy.** After a live `determined` and `settled`, your position stays open and unpaid. The market isn't `finalized` until you `POST /_settle` it yourself.

   **Differs from Kalshi:** a market Kalshi settles stays open in the simulator, unpaid, until you `POST /_settle` it.
8. **Delivery is best-effort.** A slow simulator costs frames: `dropped` in the `[status]` line, with the queue depth as `queued` and `peak`. A dropped delta leaves that market's book behind until its next snapshot. Exchange-wide frames (lifecycle, `pyth_value`, `cfbenchmarks_value`) go first. Once the feeder's queue is half full they're dropped, so they never take the room a delta needs.
9. **Gaps and errors are counted.** A frame the exchange never delivered shows as a sequence gap: the feeder resubscribes that socket for fresh snapshots and counts it in `gaps` and `lost`. `reconnects` counts sockets the exchange dropped, and `errors=25:3` counts the exchange's error frames by code.

### The feed route and the watch list

The feed is `POST /_replay/feed`: NDJSON, one capture line per row, in the `{"recv_ms":…,"event":{…}}` shape the capture writes. It returns `{"accepted":n,"dropped":n,"drops":{reason:n}}`. A market's first frame must be an `orderbook_snapshot`, because a delta before any book is dropped as `no_baseline`. This matters only if you hand-roll a feeder.

The carried list is `/_replay/watch`, the same list the Markets card edits, so a script can change it at runtime. `GET` returns `{"available":[…],"watch":[…],"seen":[…]}`: the feeder's offer, the picks and the markets that frames have arrived for. An empty list is `null`. `POST {"markets":["A","B"]}` replaces the picks and returns `204`, and the feeder resubscribes within about two seconds. `{"markets":[]}` clears them.

<!-- example: skip network -->
```bash
curl -s localhost:8080/_replay/watch | jq '{watch, seen}'
curl -s -X POST localhost:8080/_replay/watch -d '{"markets":["KXBTC15M-26AUG010000-00"]}'
```

Only markets in `available` are carried, and a pick outside it is ignored without an error. Markets and events named at startup are added back to the picks on every rescan, so a `POST` can add to them but not drop them for good. With nothing named, `--live-carry N` refills an empty list with the busiest N. Events have no HTTP form: name them at startup with `--live-events` or `RYSTIC_LIVE_EVENTS`.

| You send | The simulator returns |
|---|---|
| a body that names neither `markets` nor `available` | `400` |
| a `POST` outside a live session | `409` |

---

Source: https://www.rystic.ai/docs/simulators/kalshi/features/live-feed · Markdown: https://www.rystic.ai/docs/simulators/kalshi/features/live-feed.md
