Simulators
Live feed
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. 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.
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 covers every flag and running it in a container.
How the feed behaves
-
No key setup. The CLI carries a read-only exchange key, and
rystic doctor kalshi-twinshowslive feed … built into this release. To use your own key instead, setKALSHI_KEY_ID, orKALSHI_KEY_ID_FILEwith a path to a file holding it. Pair it withKALSHI_PRIVATE_KEY_FILE(a path) orKALSHI_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_idand/run/secrets/kalshi_private_key, both or neither. The environment wins over/run/secrets, and/run/secretsover the built-in key. None of these is your Rystic API key or license. -
Nothing is carried until you pick. The feeder offers the 500 busiest open markets to the Markets card in the state UI, and a market’s frames start when you add it.
--live-carry Npre-carries the busiest N, and--live-markets A,Bcarries exactly those.--live-events E1,E2carries every open market of those events, and--live-series KXBTC15M,KXETH15Mnarrows the offer to those series. Naming markets or events turns--live-carryoff: 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. -
lag_msis 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 inrystic logs --feed <name>splits it.upstream_lag_msis receive time minus the exchange stamp. Alag_msthat tracks it is the exchange running late, and one well above it is the feeder or the simulator falling behind.post_msis each batch’s round trip to the simulator, apply included. -
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). Liquidity you take stays taken in the simulator’s copy until recorded decreases account for it.
-
What changes from replay. There’s no tape, so
statushas nototal,positionorduration_ms. It carriesframes,dropped,last_msandlag_msinstead.seekreturns409, andpausedrops frames instead of queuing them.speedis1. -
A carried market the simulator hasn’t seen is minted on its first frame. Its record is fetched then, so
close_timeand the expiration fields are the exchange’s own (where a replayed market’s 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. -
The exchange’s lifecycle frames reach your
market_lifecycle_v2subscription, but don’t settle the simulator’s copy. After a livedeterminedandsettled, your position stays open and unpaid. The market isn’tfinalizeduntil youPOST /_settleit yourself.Differs from Kalshi: a market Kalshi settles stays open in the simulator, unpaid, until you
POST /_settleit. -
Delivery is best-effort. A slow simulator costs frames:
droppedin the[status]line, with the queue depth asqueuedandpeak. 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. -
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
gapsandlost.reconnectscounts sockets the exchange dropped, anderrors=25:3counts 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.
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 |