Simulators
Kalshi changelog
What changed in each Kalshi simulator release, newest first.
v0.0.55 — 2026-11-06
-
The page has an Errors and limits section. It shows the
errorbody and the errors shaped otherwise, the 429 bodies of the throttling scenarios and the unenforced limitsGET /account/limitsreports. It lists the routes that page with theirlimitdefaults and caps, the routes that stop atlimitwith no next page, and theclient_order_idrule that keeps a retry from placing an order twice. -
The backtesting guide ships with the simulator. The Kalshi feature pages, in the archive and in
rystic docs kalshi, now carry the whole backtesting guide. Batch backtests covers a first backtest, running every tape with per-run arguments, deriving event and series tickers, reading the results, containers and what batch refuses. Bots and batches covers what your bot reads.
v0.0.54 — 2026-11-06
-
The page keeps only the simulator’s own facts on reset, seeding and sign-in. The shared pages cover the general mechanics.
-
The page states the clock and
--injectrules of a tape. A playing tape or live feed re-pins the clock over any time you set, andrystic uptape services default--injectto$RYSTIC_INJECT.
v0.0.53 — 2026-11-05
-
The call log shows what each call asked for and whether it succeeded. Each
GET /_rystic/callsentry now carriesargs, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), andok, false for an answer that is not 2xx. A path the simulator does not serve is markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.mdandSCENARIOS.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it. Tape replay, the live feed, settlement, scripted events, the WebSocket feed, exchange shards, orders and matching, the order book and price grid, query errors, production latency, grading a run, bots and batches, containers and the browser UIs each have their own page underdocs/features/, and the scenario table is generated from the scenarios themselves. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
-
Scenario descriptions use customer terms. The Scenarios table and
--list-scenariosdescribe each scenario in American spelling, with no internal references. -
The docs walk a batch backtest to its P&L table. They now cover the tapes
-tapesreads, the six values each bot gets (PORTand{{port}}included), a first backtest with its output, what lands under-outand what batch refuses. They also describeapi-check.py, and the missing-tapes hint now sayscapture-setup.shsorts captures you already have rather than recording them. -
The Scenarios table is easier to scan. Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.
-
Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.
-
Limitations is shorter to read. It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind “Show all limitations”.
-
The docs page holds what only the shared control-plane page said about Kalshi. It says a reset stops a scenario’s tape but not one started from replay, that a seed which moves a book sends WebSocket frames, and that the measured latency profile covers nine endpoints and can start switched on.
-
The docs pages link to the pages that own boot inputs and network profiles. Two links that opened stub headings on the control-plane page now open the boot configuration and network profiles pages.
-
The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.
-
The docs read more plainly, and three long pages split. Sentences are shorter, and each behavior that differs from Kalshi is marked “Differs from Kalshi” where it’s described. Rules with several outcomes, such as repeating a settlement, are tables. Replaying several markets at once and the tape format each have their own page beside Tape replay. The live feed’s settings and Compose setup have their own page beside Live feed, and batch backtests have their own page beside the sample bots and the grid.
v0.0.52 — 2026-11-03
-
Many WebSocket subscribers on one market keep up. The simulator encodes each market data message once for all its subscribers and sends each subscriber its pending messages in one write, so hundreds of sockets on one market receive many times more messages a second, and the book keeps updating at full speed while they listen. Every message is byte for byte what it was.
-
Decrease and batched cancel move the orderbook feed, and an amended-up order keeps its true size. A decrease or a batched cancel now sends the
orderbook_deltafor the liquidity it withdraws, carrying yourclient_order_idon your own connection, so a book folded from the feed matchesGET /markets/{ticker}/orderbook. A decrease after an amend that raised the count no longer leaves twice the remaining on the book, so the later cancel or fill no longer drives a folded level below zero, and a fill against such an order takes exactly the filled contracts. -
GET /marketspages like the exchange. It serves 100 markets by default (limitup to 1000), newestcreated_timefirst and returns acursorwhile more remain: pass it back for the next page until the cursor comes back empty. A bot that read every market from one unpaged call now reads the first 100, as it would against Kalshi, and a large catalog no longer goes out whole on every read, so a bot paging it no longer stalls everyone else’s requests. -
Long sessions with expiring orders stay fast. The simulator now finds the orders whose
expiration_timehas passed without reading every order that ever expired, so requests, orderbook reads and WebSocket subscribes no longer slow down as a session’s expired orders pile up. Orders expire at the same moment, in the same order and with the same messages as before.
v0.0.51 — 2026-11-03
- The simulator keeps 1,000,000 WebSocket frames instead of 100,000. That is about 3.5 minutes of a dense multi-market tape, so a reader that falls behind still receives every order-book delta. Set
RYSTIC_EGRESS_RETAIN=0for an unbounded log, or to another frame count to tune it.
v0.0.50 — 2026-11-02
-
Deep order books answer faster. A book read (the REST orderbook, queue positions, a WebSocket orderbook snapshot) no longer re-reads every resting order when the book has not changed since the last read, and placing an order reads its market’s book once rather than four times. On a book 50 levels deep a read takes about half the time it did and an order about a fifth less.
-
A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.
v0.0.49 — 2026-11-02
-
Expiry sweeps stay responsive with large resting books. Order requests and orderbook reads inspect indexed expiration deadlines instead of sorting every resting order. Seeded orders must use RFC3339 strings for
expiration_time; numeric fixture deadlines are no longer swept. -
A lifecycle subscription is confirmed at once, even under load. A
subscribetomarket_lifecycle_v2ormultivariate_market_lifecycleis answered with itssubscribedconfirmation immediately, instead of waiting behind a busy replay or a queue of order requests, so a client that waits for that confirmation at startup no longer times out during a large replay.
v0.0.48 — 2026-10-31
-
A state dump seeds back as it was, and the next record gets a fresh id.
GET /_rystic/inspectposted toPOST /_rystic/seedafter a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds.GET /_rystic/inspect?view=sequenceslists every id counter, and a world document’s newsequenceskey restores them, so ids the simulator renders from a counter continue where the dump left off. -
A replay group of thousands of markets loads in seconds. Grouping many recorded markets into one replay, such as a full election-night set, now merges their events in time order without slowing down as the group grows; the replay order is unchanged, with events that share a millisecond still played in the order the markets were named.
v0.0.47 — 2026-10-31
-
The docs explain exchange shards and the replay controls. A new Exchange shards section covers per-shard cash and
KALSHI_REPLAY_SHARD; replay documentsprepare,streamand whatseek’spositioncounts; and the page links Kalshi’s API reference for parameters and bodies. -
The twin documents its own control plane in Swagger.
/_rystic/swaggernow lists a Rystic control plane spec, served at/_rystic/swagger/spec/rystic-control-plane.json, describing the/_rystic/*routes this twin serves (its scenarios, inspect views and seedable registers by name), its/_settleand/_account/snapshotroutes, and the/_replay/*player. -
Replay loads a scripted-event file on tape time.
POST /_replay/starttakesinject, a file whoseatvalues are tape time, fired as replay reaches each instant, with seed timestamps shifted onto the rebased clock;rystic run --tape --injectsends it, inject files may be JSONL, andGET /_replay/statusreports the schedule underinject. -
Scheduled events at one instant all stamp the same time. When several inject events share an
at, every one now reads the twin clock the first of them read, so twostate_live_dataseeds at the same moment give both milestones the samelast_updated_tsinstead of the second landing a millisecond later. Requests after them still stamp later than any of them.
v0.0.46 — 2026-10-30
- The docs list every route. The Coverage section now names all 113 Trade API routes by method and path, grouped by resource, so you can check a specific endpoint without starting the simulator and reading
GET /_rystic/routes.
v0.0.45 — 2026-10-27
- A replayed market belongs to the event its own record names. A market whose capture carries its record is served under that record’s
event_ticker, so single-market events and markets with multi-segment strikes resolve atGET /events/{event_ticker}instead of 404ing under a ticker-derived name.
v0.0.44 — 2026-10-26
-
Markets serve
settlement_bounds_type. Every market the twin mints, replays or seeds from a scenario pack now carriessettlement_bounds_type, as every live market read does:default(no settlement bounds), or a replayed market’s own value andsettlement_floor_dollarswhen its record names a floor. A client that indexes the field no longer meets its absence. -
Re-posting a schedule never fires an event twice. A
POST /_rystic/injectthat re-sends anidthe running schedule already fired keeps that event fired with its originalfired_at, feed line and bindings, so rolling windows that overlap load the same fired set as one whole document. To fire an edited event again, give it a newid. -
A live_data update moves its milestone’s
last_updated_ts. Astate_live_dataseed that changes a race’s record, by/_rystic/seedor a scheduled inject, stamps that milestone’slast_updated_tswith the twin clock, so a bot pollingGET /milestones?min_updated_ts=sees the update land. A seed that changes the milestone’s ownlast_updated_tskeeps that value. -
A replayed event can be mutually exclusive. A replayed market’s minted event took
mutually_exclusive: falsealways; an archive whosemeta/carries the event’s own record ("kind":"event", keyed by event ticker) now sets it from that record, soGET /events/{t}servestruefor D/R winner pairs and seat buckets. -
A large tape library loads its market records in one pass. The twin reads every market record in the library’s
meta/logs once, instead of re-reading every meta file for each market it mints, so loading thousands of tapes no longer slows quadratically with the library’s size. A meta file added while the twin runs is still picked up.
v0.0.43 — 2026-10-20
-
Milestone lists page and filter like live.
GET /milestonesnow requireslimit(0 or less serves 100, the most is 500), returns a cursor that walks the rest of the list, sorts bystart_datethenid, and appliesminimum_start_dateandmin_updated_ts. A missing or malformedlimit, date or timestamp, or an unreadable cursor, is the 400 live sends. -
/_replay/watchis documented. The live-mode docs give the request and response for reading and setting the carried markets over HTTP, so a startup script can pick markets without the state UI. -
The state UI follows your OS dark setting. With the system set to dark,
/_rystic/uirenders on a dark ground; the Markets card and the rest of the shell follow it. No setting to change. -
The state UI stops re-requesting on every feed update. The header fetched
/_rystic/versionand/_rystic/registerseach time the feed flushed, up to ten times a second each under a live feed or a replay. The version is now fetched once and the clock at most once a second, so the browser’s network tab shows your own calls.
v0.0.42 — 2026-10-16
-
WebSocket fills preserve trade direction. Fill messages use
side: "yes"withaction: "buy"for bids andaction: "sell"for asks, including passive maker fills. Bots that track positions from fill actions can distinguish short fills from long fills; REST fill records keep their existing direction fields. -
Indexed list query parameters.
tickers,ids,milestone_idsandpercentilessent astickers[0]=…&tickers[1]=…, as some SDKs encode arrays, read the same as the repeated parameter. -
Swagger UI to read and call the twin from the browser.
/_rystic/swagger, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin. -
Fill messages carry your client_order_id. WebSocket
fillmessages now include theclient_order_idyour order was placed with, as live Kalshi’s do, so a bot can match a fill to its order before it knows the exchangeorder_id. Orders placed without one still get fills without the field. -
Market lists come back in the exchange’s order.
GET /marketslists the newest market first,GET /markets?tickers=keeps the order you asked for, and an event’s markets are listed by ticker. The order no longer changes with the tape, the seek point or the run.
v0.0.41 — 2026-10-15
-
Setting the clock never stamps records before the timeline events it fired. When
POST /_rystic/clock {"time": ...}fires timeline events due at its target, the next request’s records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before. -
A paced clock keeps wall time, and
rystic run --speed 0stops it. UnderRYSTIC_PACE, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late.rystic runhands the twin only the pace it resolved, so--speed 0, a world without events,--tapeand--livekeep the clock manual even when your shell exportsRYSTIC_PACE. -
Your own exchange key from files.
KALSHI_KEY_ID_FILEreads the key ID from a file, and with no key variables set the CLI reads/run/secrets/kalshi_key_idand/run/secrets/kalshi_private_key, so a sharedcompose.yamlcarries your key as two Compose secrets instead of per-person environment. A path inKALSHI_KEY_IDis refused and pointed atKALSHI_KEY_ID_FILE, and a key file the container cannot read names the uid and the fix. -
A live setting without live mode is refused.
rystic runwith a--live-*flag, alive_*stack key or aRYSTIC_LIVE_*variable but live mode never turned on now stops with a usage error naming the setting, instead of replaying without it.--live=false,RYSTIC_LIVE=0orlive: falsestill turns live off on purpose. A misspelledRYSTIC_LIVE_*variable is refused with the setting it meant, and any variable a typo away from a Rystic setting gets a warning. -
Your Rystic API key from a file.
RYSTIC_TOKEN_FILEreads the key from a file, and with neither it norRYSTIC_TOKENset the CLI reads/run/secrets/rystic_tokenbeforeconfig.jsonand the keyrystic loginstored, so a sharedcompose.yamlcarries the key as a Compose secret instead of per-person environment. A token file that is named or mounted but unreadable or empty is an error naming it.
v0.0.40 — 2026-10-12
-
Scheduled calls reach the Trade API, and a world can carry its own timeline. A
callevent inPOST /_rystic/injector--injectmay name a Trade API route and act as the actor itsasnames, through the same handlers and WebSocket frames as your orders, and an event’satmay be+<duration>. A--seedfile, or a complete world document posted toPOST /_rystic/seed, may carry its owncredentialsandevents, and a top-level key of either name the twin used to ignore now takes effect.RYSTIC_PACE=<speed>plays the clock in real time./_rystic/inject,--injectandRYSTIC_INJECTnow needstate_setinstead ofscenarios. -
A timeline call can hand what it minted to later events. A
callevent may addbind,{"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as${name}in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a${name}no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing. -
A clock jump fires each timeline event at its own time. When one
POST /_rystic/clockor paced tick passes several events, each fires with the clock at itsat, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time. -
A clock jump stamps each timeline event at its exact instant. When a clock move passes timeline events, the records each one makes carry exactly its
at, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn. A live feed batch that passes an event stops for it, so it fires between the frames either side. -
A timeline call can carry raw bytes, XML or a JSON array. A
callevent may sendbody_textorbody_base64instead ofbody; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and${name}fills inbody_text. Abodymay be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as/InitiateMultipartUploadResult/UploadId. -
GET /account/limitsshows the advanced API usage level once you upgrade. AfterPOST /account/api_usage_level/upgrade, the limits read answersusage_tier: "advanced", Kalshi’s advanced read and write buckets, and the advanced grant ingrants, as Kalshi does. A client can now confirm its upgrade through the public API. -
A triggered order group refuses new orders until it is reset. After
PUT /portfolio/order_groups/{id}/trigger, an order naming that group inorder_group_idis refused and does not rest, as Kalshi documents;PUT /portfolio/order_groups/{id}/resetadmits orders again. A bot that relied on the twin accepting orders into a triggered group now sees the refusal.
v0.0.39 — 2026-10-11
-
The verified UI installs and starts with
rystic ui kalshi. Kalshi Accurate, the client built against Kalshi’s own interface, is a Pro and Enterprise deliverable: it ships as the imageregistry.rystic.ai/kalshi-twin-ui:<ver>beside the twin’s, pulled with the samedocker login, andrystic uiruns it against a running instance (or, without Docker, the UI’s archive with its public market snapshot on your Node 22.12+). A key whose plan lacks theverified_uientitlement is refused withfeature_not_in_plan. -
The scripts in this archive are MIT-licensed.
check.shand the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them.LICENSE.txtno longer covers them; it still governs the twin itself.
v0.0.38 — 2026-10-11
- Kalshi is on Pro and Enterprise. A Free API key is refused
product_not_in_planforrystic pull kalshi-twinand a start. On Pro and Enterprise every world, control route and the verified Kalshi UI are open, and without--scenariothe twin boots empty.
v0.0.37 — 2026-10-09
-
The live feeder separates the exchange’s delay from its own. The feeder’s
[status]line now reportsupstream_lag_ms(receive time minus the exchange stamp), the queue depth, how long the simulator takes per batch, reconnects, and every exchange error by code. The feeder also keeps reading the socket while it subscribes, so a reconnect no longer leaves the exchange buffering for it. A frame the exchange skipped resubscribes its socket for fresh snapshots instead of leaving the book behind. Needs a CLI and simulator from this release. -
Replay keeps pace with the feed under order load. Placing, canceling and listing orders no longer slows down as a session’s order history grows, and a replayed frame costs the same however many orders and positions are open in other markets. A large
/_replay/feedbatch no longer holds order requests until it finishes: they run between chunks of at most 1,000 frames. -
The live feeder names a series or market listed as an event, and a carry it ignores. A series (
KXBTCD) or an open market ticker in--live-eventsorRYSTIC_LIVE_EVENTS_FILEis now named in the feeder log with the flag that carries it, instead of being reported as an event not yet open. Setting--live-carrybeside named markets or events logs that the carry is ignored. -
A subaccount transfer is refused when its amount is not positive or exceeds the source balance.
POST /portfolio/subaccounts/transferanswers 400transfer_amount_must_be_positivefor a negative amount and 400insufficient_balancefor more thanfrom_subaccountholds, as Kalshi does; neither moves a balance. A bot that relied on the twin accepting those transfers now sees Kalshi’s refusal. -
Replay keeps pace with more markets per twin. The terminal event feed no longer wakes for every replayed market-data frame it does not print, so one twin applies up to a fifth more replayed events per second before it falls behind the recording. With the default event scope, a burst of market data can no longer push one of your fills out of the terminal feed.
-
Orders no longer wait behind replay catch-up. When playback or the live feed falls behind, an order request that arrives while events are applying runs after the event in flight, not after the whole batch: a cancel waited about 100 ms on average while replay caught up, and now waits about 4 ms. A pause or seek still takes effect between batches. The action log now stamps each request line with
received, when the request arrived, and writes afillline for each of your fills with itsorder_id,trade_idand the recorded time it happened. -
Replay no longer scans the whole book per event to track your orders. Each replayed event checks only the orders you have resting, not every level of the book, so one twin applies about a fifth more replayed events per second on the busiest BTC market before it falls behind the recording.
-
An open state UI no longer slows replay.
/_rystic/eventssends what queued in one write at most every 5 ms instead of waking for every event, so with/_rystic/uiopen a busy replay applies about 17% more events per second. Each event keeps its own frame and ID, and resuming withLast-Event-IDor?cursor=works as before. -
Replayed trades no longer copy the whole book. A replayed trade renders its book changes from the levels it reached, so one twin applies about 40% more replayed events per second on the busiest BTC market before it falls behind the recording. WebSocket frames are byte-for-byte unchanged.
-
Replay refreshes market records once per step, not per event. A market’s record is rebuilt after its first event in a step, after each trade, and at step end, so one twin applies about 14% more replayed events per second on the busiest BTC market before it falls behind the recording. A market read between steps, and every WebSocket frame, is unchanged.
-
A pause or seek no longer waits out replay catch-up. When playback falls behind and catches up, a pause or seek takes effect at the end of the batch applying when it arrived; before, it could wait until the whole backlog had applied.
v0.0.36 — 2026-10-03
-
Query parsing errors match the demo on sixteen GET routes. Declared query fields return the measured flat
400message for malformed integers and booleans, empty values, integer overflow, and duplicate values, preserving which field fails first. Coverage is limited to the documented field prefixes; application range checks and the positions endpoint’s separate integer narrowing remain known gaps. -
Clearer market details and Economics browsing. The standalone coded frontend shows holdings with fee-inclusive average prices in outcome rows, uses series-based market URLs and labels, and adds fixed-scale charts with line/bar views. Economics gains subcategory filters and a featured-event pager, while the shared header shows open-event counts and compact balances with consistent page margins.
-
Keyboard controls and shared trading state in the coded frontend. Use arrow keys across trading controls, inspect seeded YES and NO order intent, and keep UI interaction logs after closing the page. The coded frontend defaults to the recorded frontend’s
ui-traderaccount; setRYSTIC_ACTORto use another account. -
A set of cut games can be stitched into one tape offline.
tapecut stitch -dst <set> <cut>…writes the games already back to back on one session clock, with the replay sequence’s own rule, as one cut archive per game plus asession.jsonwhosereplay_startbody plays them with every offset at 0. The set reads as one continuous tape without the twin.examples/kalshi-sidefeedbuilds its play-by-play feed: each line runs a lead ahead of the book’s reaction on the tape, either one value or drawn per line from a seeded range (the MLB sets draw from 100 ms behind to 1 s ahead), and the plain feed is one{ts_ms, event}line per play. -
Private account settings stay out of recorded UI fixtures. The original-frontend HAR importer and fixture loader replace captured API tiers, grants and limits with fixed synthetic display values, including when loading older fixtures. Unknown fields in those account responses are discarded.
-
MLB set builds refuse linked output directories. The set/game driver rejects symlinks at selected output paths, including dangling links, and stops if old output cannot be removed. Partial builds preserve sibling sets instead of writing through a link.
v0.0.35 — 2026-10-02
-
Replay stops at an exact tape time.
POST /_replay/starttakescheckpointsandPOST /_replay/playtakesuntil_ms, both in absolute tape milliseconds (the axis ofstatus.start_ms). Playback applies every event through the stop, pauses there, andstatus.at_checkpoint_msreports the stop, so the same actions at the same stops give the same fills and balances after a reset. -
A windowed capture archive replays its whole window, quiet markets included. An archive cut to a
[from, to)window carries amanifest.jsonthat declares the window, an opening book per market and a SHA-256 per file, and holds no frame at or afterto. Replaying it, alone or as a group, covers the declared window even when a market has no event near either end, so one quiet outcome no longer ends a grouped replay early. -
A replayed window’s trade history and candlesticks follow the tape. Every recorded print is stamped at its own tape time, even when an accelerated replay applies many events in one batch, so
GET /markets/tradesand the candlestick reads bucket it into the minute it traded. Restarting a replay, or jumping the play head back, removes the recorded prints the head has not reached yet; your own fills stay. -
Replay can use a zero-fee series seed. Before starting playback, seed the market’s series in
state_serieswith itsticker,fee_type: "quadratic"andfee_multiplier: 0. Crossing fills and resting orders filled by recorded prints then charge zero fees; cash changes only by quantity times price. Event fee overrides and series fee changes still take precedence over that series setting. -
Replay steps by a fixed amount of tape or to the next scheduled event.
POST /_replay/playtakesadvance_msto play that many tape milliseconds from the head and pause, oruntil: "next_event"to play to the next injected event and pause right after it. Every injected event inside the step fires at its own time first, andstatus.next_event_msshows the next stop, so a harness can drive a session one step at a time with identical results on every run. -
Independent local Kalshi interface for source checkouts. Run the standalone Kalshi Accurate app against a local twin: browse markets, open price history and the order book, buy and sell with an inline review, and manage positions, resting orders, amendments, cancellations and fill history as the
memberaccount. It reaches only the Trade API routes it uses; the existing state UI remains separate. -
A market can settle on schedule. A scripted-event schedule now takes a
callevent that runs one of the twin’s own routes, such asPOST /_settle, at its sim time, so a replayed game’s markets close and pay the real winner at the final out with no admin request. A windowed archive’smanifest.jsonrecords each market’s outcome, read from its market records, while itsmeta/stays free of it, so nothing the agent reads shows the winner early. -
Liquidity you take during replay stays taken. When your order lifts a replayed level, the next recorded update at that price no longer puts the contracts back: the level shows the recorded size minus what you took, and recorded decreases there pay that down until the book is back on the recording. The same holds for reconnect snapshots and for live-fed markets, and a restart, seek or reset starts clean.
-
The control plane can be locked away from an agent, and every action logged to disk. With
RYSTIC_ADMIN_TOKENset,/_rystic/*,/_replay/*and/_settleneed that value inX-Rystic-Admin-Token(or therystic_admin_tokencookie).GET /_rystic/versionstays open as the readiness probe, and so doesGET /_feed.RYSTIC_ACTION_LOG=<path>appends one JSON line per state-changing request and control op, with the sim clock and the replay head on each line. -
Replayed markets can share one cash pool. A replayed market keeps the exchange shard its own record names, so a seeded account needs presence and cash on that shard; the HOWTO’s “Seeding state” section now shows the seed. To fund every replayed market from one balance instead, start the twin with
KALSHI_REPLAY_SHARD=0: each market minted from a tape or the live feed is placed on shard 0, at the cost of per-shard fidelity. -
A grader can read a whole account, the action log and the run’s configuration over HTTP.
GET /_account/snapshotreturns one account’s cash per shard, positions with average entry price and a mark, portfolio value at those marks, open orders, every fill with its fee and the replay time it happened at, and settlements. It agrees with the Trade API’s own reads.GET /_rystic/actions?since=<seq>pages through this run’s action log, andGET /_rystic/confignames the run: input digests, RNG seed, and the loaded replay session with its archive and cut manifest. All three need the admin token when one is set. -
Replay can play games back to back in any order.
POST /_replay/starttakes asequenceof games, each a market or a list of markets, withgap_msbetween them. Games recorded on different days play one after another on one session clock, each shifted whole (book, trades and market times), so a game plays the same wherever it sits in the order.KALSHI_CAPTURE_DIRcan hold one cut archive per game.GET /_replay/statusandGET /_rystic/configreport each game’s recorded window, session window and offset, so a session time maps back to its recorded time. -
The Enterprise image runs a replayed session with the network off. The HOWTO’s new “Running offline” section shows the
docker create --network nonerecipe for a cut library: license,RYSTIC_REPLAY_METADATA=off, the admin token, the action log and a single cash pool. It also namesGET /_rystic/versionas the readiness probe. A nightly check runs a two-game session, its feed, an order and both settlements through the release image with no network.
v0.0.34 — 2026-10-02
-
check.shfails fast on a port that never answers. Its port check and boot wait give each/_rystic/versionprobe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging. -
Order IDs from the matching engine come from their own range. An order the engine places gets an
order_idsuch asa7cade2e-8c1a-43f4-8000-000000000001rather than00000001-0000-4000-8001-000000000001, the ID other records minted first share. Ids still count up in their last digits and restart on reset; a test that pinned an old order ID needs the new one. -
market_unopenedseeds its market asinitialized. The market reportsstatus: "initialized", Kalshi’s status for a market before its open time, where it reported"unopened", which is aGET /marketsfilter value and never a market’s status. Orders are still refused with 409market_closed, andGET /markets?status=unopenedstill lists the market. -
Scripted events fire on the sim clock. Load a schedule of timed messages and seeds with
POST /_rystic/inject, or--inject <file>/RYSTIC_INJECTat boot. Each event fires once when the clock reaches itsat, between the same two tape events at any replay speed. Fired messages are readable atGET /_feed?since=<seq>, andGET /_rystic/injectshows what fired and when.
v0.0.33 — 2026-09-30
-
The batched cancel is measured against live on the order it places. The test for
DELETE /portfolio/events/orders/batchednow cancels the resting order its own chain creates, so every round compares the simulator’s cancel with live Kalshi’s instead of stopping before the call, and the order no longer outlives the run. -
The state UI takes the rystic.ai look.
/_rystic/ui— the market list, order book, order entry and bot panels included — now reads in the site’s paper, ink and Space Grotesk instead of the dark theme.
v0.0.32 — 2026-09-27
-
Taker fees follow the fee schedule. A taker fill now pays ceil(0.07 × the series’
fee_multiplier× C × P × (1−P)) per fill, at the micro-dollar, as live Kalshi does; the simulator used the multiplier as the rate, so every taker fill on a series that carries one — thebtc15m_tapeworld’s KXBTC15M among them — paid about 14 times the schedule. A backtest’s fees and P&L on the tape world are correct again, and two sealed probes hold the simulator to the schedule. -
A replayed market keeps its ending to itself, and stays settled. While a tape market is active its record no longer shows the recorded
expiration_value(look-ahead data in a backtest); itsexpected_expiration_time,latest_expiration_time,expiration_timeandoccurrence_datetimefollow the market’s own geometry on the replayed clock (5 minutes and 7 days past close on a 15-minute market) instead of all readingclose_time. AfterPOST /_settlethe market quotes bid 0.0000 / ask 1.0000 on an empty book, publishes the recorded settlement value, and the tape’s later frames leave it alone. -
The V1 order writes answer 410, as live Kalshi has since June 2026.
POST /portfolio/orders,POST /portfolio/orders/{id}/amend,DELETE /portfolio/orders/{id}andDELETE /portfolio/orders/batchednow answer410 deprecated_v1_order_endpointwith live’s envelope, so a bot that still calls them fails on the simulator the way it fails in production; trade through/portfolio/events/orders(V2). A v1-era bot you cannot change yet keeps its old wire by starting the simulator withRYSTIC_KALSHI_LEGACY_ORDERS=on. Four sealed probes hold the refusal. -
The portfolio reads publish what live publishes.
GET /portfolio/orderslists newest first by the fullcreated_timeinstant, including fractional seconds, with order ID breaking ties so cursors resume deterministically; an order canceled throughDELETE /portfolio/events/orders/batchedkeeps an RFC 3339last_update_timeinstead of a unix integer; every resolved trade from a replayed tape reachesGET /markets/tradeswith the same ID, price and quantity as its WebSocket trade and any member fill; and balance, fills, positions and settlements no longer carry the simulator’s internalownerfield. -
Error envelopes match the exchange. A path the simulator does not route answers Go’s plain-text
404 page not foundlike live;GET /markets?status=<anything outside unopened|open|paused|closed|settled>is a400 bad_requestwith detailsinvalid status filterinstead of an empty page;GET /markets?limit=1001answers the parameter binder’s{"msg":"Parameter validation failed for GetMarkets: …"}; a routed 404 carries{"error":{"code","message"}}and noservicekey; and therate_limitedworld’s 429 body is the documented{"error":"too many requests"}. -
A repeated WebSocket subscribe merges its market scope into the same subscription. Subscribing again to a market-filterable channel answers
{"type":"ok","sid":…,"seq":…,"msg":{"market_tickers":[…]}}on the samesid, followed by the requested markets’ snapshots. Subscribing to all markets keeps or expands the subscription to all markets; a later scoped subscribe cannot narrow it. Channels that require a market still require one, and keyed channels or channels configured to ignore market filters retain error 6Already subscribed.
v0.0.31 — 2026-09-26
- The login offer starts Kalshi in
btc15m_tape, and HOWTO.md and the smoke scripts start from an API key.rystic loginoffers Kalshi booted intobtc15m_tape, the world every plan can run, instead oftight_book. HOWTO.md leads withrystic login rys_…; HOWTO.md and SCENARIOS.md count all 47 named worlds and say which worlds, control-plane and/_replay/routes Free, Pro and Enterprise reach. HOWTO.md gives Compose onRYSTIC_TOKENwith the Enterprise license second, sends/_settlewithContent-Type: application/json, lists the lease check-in among outbound calls, and says theDELETE /portfolio/events/orderskill switch answers404, as Kalshi’s demo environment does.check.shruns on your API key as well as a license and, on Free, waits forbtc15m_tape’s market and reads it back instead of seeding;trade-check.shsays it needs Pro or Enterprise when the plan refuses its seed.
v0.0.30 — 2026-09-25
-
Self-serve on the metered edition replaces the free edition. A Rystic account’s token runs the metered build:
rystic login rys_…once, thenrystic runordocker run -e RYSTIC_TOKEN=…. It carries every scenario and control-plane route your plan includes, and bills active 30-second blocks against the plan’s allowance. The fixed-world free download and its usage report are gone. -
Live mode carries markets by event.
--live-events E1,E2,RYSTIC_LIVE_EVENTS, or a shared--live-events-file/RYSTIC_LIVE_EVENTS_FILE(stack keyslive_events/live_events_file) carry every open market of each named event, on top of any--live-markets. Each event is looked up again on every rescan, so a strike the exchange adds later is carried and a closed market drops.
v0.0.29 — 2026-09-23
- Live market selections survive rollover. The feeder reapplies
--live-marketsandRYSTIC_LIVE_MARKETS_FILEon every discovery scan, removes closed selections, and picks up newly open named markets. When none of the selected markets is open, it waits instead of falling back to the busiest markets. This fixes gold-market rollover losing gold data while subscribing to hundreds of unrelated markets. A named market removed through the UI returns on the next scan. - WebSocket lifecycle and price-feed channels work.
multivariate_market_lifecycle,pyth_value,cfbenchmarks_value,communicationsandorder_group_updatesnow accept subscriptions. Markets minted by a tape or seed emit lifecycle creation events, and/_settleemits determined and settled events. Live mode forwards exchange-wide lifecycle and price-feed frames, even with no market selected. Price feeds remain silent on recorded tapes; accepting communications and order-group subscriptions does not generate events for those channels. Scheduled market close remains time-based and sends no lifecycle frame. Forwarded live settlement frames do not settle the twin’s own market; exchange-wide frames may be dropped under feeder backpressure to preserve order-book updates.
v0.0.28 — 2026-09-22
- Orders on
btc15m_tapenow fill. In v0.0.27 every order placed on the recorded KXBTC15M market answered404 user_not_found. The market trades on exchange shard 2, as it does on Kalshi, but the world put the $10,000 balance on shard 0 only. The balance now sits on shard 2:GET /portfolio/balanceshows abalance_breakdownof $0.00 on shard 0 and $10,000.00 on shard 2, and an order on the market fills against the replayed book. The free edition, which boots this world, has the same fix.
v0.0.27 — 2026-09-22
- A real recorded session as a named world:
btc15m_tape../kalshi-twin --scenario btc15m_tapeboots into the KXBTC15M market that closed 2026-09-03 10:30 UTC, exactly as the exchange streamed it. It replays all 62,629 WebSocket messages recorded live off the feed, including 2,818 prints, event for event on the exchange clock from the moment the world starts, with no/_replaycall. Your bot fills against the real book, level for level, in a session that runs 0.52 → 0.95 and settles yes. The market is servedactiveon a clock rebased to now, and the 14-minute session loops, so a bot left running never reaches the end. The world seeds the market’s own event and series and a $10,000 balance. It is the 47th named scenario (SCENARIOS.mdlists it under Recorded) and the one world the free edition serves. /_rystic/versionreports more. The control-plane contract moves from 7 to 9.editionis always present: empty on a licensed build andfreeon the free edition, so a caller can tell them apart without probing.--versionprints the same value as anedition:line.initial_clockis the epoch a reset returns the clock to.api_base_paths,service_hosts,preserve_service_urlsandcredential_requiredreport settings other twins declare. Kalshi declares none, so they read empty,falseand0, present so one client reads every twin the same way.- The EULA in the archive gains a Free Edition section.
LICENSE.txtnow defines the Free Edition and sets out its terms as Section 13; General moves to Section 14. The terms of your license are unchanged, and a licensed build still sends nothing over the network.
v0.0.26 — 2026-09-17
- Drop a WebSocket connection on demand.
GET /_rystic/wslists every open socket with its subscriptions;POST /_rystic/ws-dropcuts them bytickers(any socket whose subscription names one), byconnections(IDs from the listing), orall. The default is a bare close with no close frame, what a crashed server or a severed path looks like;graceful: truesends1001 going awayfirst. A reconnect bug in a one-to-many exchange client is fleet-wide, and sockets are split by ticker, so a test drops one stream and asserts the others survive. The drop is recorded on the event stream as a control op. - One tape as many markets.
/_replay/prepareand/_replay/starttakecopies: N, playing one tape as N distinct markets on one clock, named<tape>-1…<tape>-N, each on its own stream;{"tape", "as"}members inmarketspick the names yourself, mixed freely with plain tapes. REST and WS agree on the new ticker, the real record (strike, title, grid) carries over from the tape’s own ticker, andstatusshows which tape each member plays. Each copy is folded separately, so memory scales with copies × tape size.rystic run kalshi-twin --tape <id> --copies Ndoes the same from the CLI, and the copies survive a restart. - A load rig for the reconnect path.
examples/kalshi-loadtestin the rystic-sim repo drives N streams against N copies with a drop on a timer and reports frames/s,pace(tape time advanced per wall second;1.00means the twin kept up) and reconnect latency. Its README records what one twin sustained; run it on your own hardware before trusting a number. - Every live setting from the environment.
--livereadsRYSTIC_LIVE=1and every--live-x-yflag readsRYSTIC_LIVE_X_Y, flag over env over default, so a container starts fed with no argv. New--live-markets a,b(RYSTIC_LIVE_MARKETS) carries exactly those markets before the state UI picks;KALSHI_PRIVATE_KEY_B64carries your own key inline besideKALSHI_PRIVATE_KEY_FILE(the ID and one key form); a stack file’slive: trueservice takeslive_env,live_series,live_max_markets,live_shards,live_carryandlive_markets; andrystic run --live --runner dockerruns the feeder on the host beside the container, ended first bystop. - A scenario swap under a running tape is one step. Moving the twin to another scenario while a tape played could carry the previous world’s records into the new one, because a frame already in flight landed after the reset. The swap now holds the tape off for its whole duration, so a retired tape cannot seed the new world.
- A market always has its event. Seeding a
state_market_catalogrecord (or replaying over one you seeded first, the strike-metadata workflow) now mints thestate_eventit names when none exists, soGET /events/{event_ticker}andGET /events?series_ticker=answer for every market the twin serves — on the exchange a market cannot exist without its event, and the twin used to 404 where Kalshi returns 200. A minted event carries its market’sexchange_indexinstead of a fixed 0, and no longer claims acollateral_return_typethe exchange never serves. An event you seed yourself is left as you seeded it.
v0.0.25 — 2026-09-12
- Pausing a replay inside a quiet stretch of tape no longer loses your
place. Between two recorded events the play head paces the gap on the
clock.
POST /_replay/pausenow keeps that paced position,play(and a speed change mid-flight) resumes from it instead of rewinding to the last applied event, andGET /_replay/statusreports it while paused as well as while playing. Before this, chunked playback — pause, play a little, pause again, the shape an agent-driven clock takes — could never cross a gap longer than one chunk, and a status read taken while paused mid-gap jumped a whole gap forward. An uninterruptedplayis unchanged.
v0.0.24 — 2026-09-06
-
The twin can be fed a live stream, not only a recorded tape.
rystic run kalshi-twin --livestarts the twin and a feeder together: the feeder holds the authenticated exchange socket and pushes every frame to the twin, so your bot trades against a book that is tracking the real market right now.rystic feed kalshi-twin --url <base>attaches a feeder to a twin that is already running — any runner, any machine, which is also how you feed a containerized twin. Pick the exchange with--live-env prod|demo, choose what is carried with--live-seriesand--live-carry, and spread the carried markets over more sockets with--live-shards. Keys come fromKALSHI_KEY_IDandKALSHI_PRIVATE_KEY_FILE, checked before the twin starts so a missing key cannot leave a half-started instance.The twin still dials nothing. The socket lives in the CLI process, never in the twin — that is what keeps the twin’s offline license and hermetic guarantees intact. Frames arrive at
POST /_replay/feedand go through the same fold and the same seam a tape frame takes, so a fed twin ends on the same book a tape replay of the same bytes ends on.What is not claimed: paper-trading fidelity. Your orders fill against the fed book and prints but never move them, and queue position is exactly what apply-mode replay already claims. Delivery is best-effort — a slow twin costs frames (counted and reported), never the recorder a byte, and a dropped delta leaves the book behind until that market’s next snapshot.
-
The state UI carries a watch list. The Markets card lists what the feeder offers — hundreds of open markets, busiest first — and you add, remove and watch several at once; viewing a market implies carrying it. The Replay card reports the live session: frames applied, frames dropped, how many markets are carried, and
lag, which is the age of the last frame that arrived. A climbinglagmeans nothing is feeding the twin. -
The clock steps; it no longer runs. Simulated time used to advance in real time from wherever you pinned it, so two runs of the same request sequence rendered different timestamps, and an order with an
expiration_timeexpired on wall time. The clock now moves one millisecond per request that reads it and stands still between requests: the same sequence from the same reset renders byte-identical stamps,created_timeandtsincluded. What changes for a bot: nothing expires while the twin is idle — advance the clock past the deadline withPOST /_rystic/clock {"advance_seconds": N}(or pin it with{"time": ...}) and the first request after that cancels the order. Two orders placed seconds apart now carrycreated_times one tick apart, not seconds apart, so assert on ordering, never on elapsed time. The HOWTO’s clock section is rewritten to match.
v0.0.23 — 2026-09-02
- The boot banner prints a URL you can paste. 0.0.21 and 0.0.22 announced
http://[::]:8080on all four boot lines — the wildcard bind the listener reports, not a host — while the CLI’s own summary line saidlocalhost:8080for the same port. The banner now rewrites the unspecified address in all three forms ("",::,0.0.0.0) tolocalhost; an explicit host survives untouched.--healthstill answers127.0.0.1, because a distroless image has no resolver. - Two order-entry rejections now come in live’s order and live’s words.
A reused
client_order_idanswers409 order_already_existsbefore the balance check, and afill_or_killthe book cannot cover answers409 fill_or_kill_insufficient_resting_volumebefore it too; the twin used to answer400 insufficient_balanceto both, and the duplicate’s envelope (duplicate_client_order_id) was one live has never sent.good_till_canceledandpost_onlykeep answeringinsufficient_balancepre-match, as live does. One walk of the book per order; nothing is written on a rejection. - The backtest half of the API answers.
/historical/*read registers no scenario ever seeded, so every world booted with a dead archive: cutoff at epoch, 404 onhistorical/markets/{ticker}and on candlesticks at all three intervals, empty fills, orders and trades.--scenario has_historynow boots a world holding both halves —KXDEMO-26-Aopen and tradeable,KXDEMO-25-SETTLEDin the archive behind a 2026-07-20 cutoff — each 404ing on the other’s half, which is the propertyhistorical/cutoffexists to express. - Four reads answer the question asked. A candlestick’s
end_period_tsis the inclusive end of its window, so a trade landing exactly on the boundary files in the period it closes rather than one period late.?event_ticker=on both positions reads narrows through the market’s catalog record instead of returning every event.forecast_percentile_historyis one series per event, with the percentile taken across that event’s markets, not one duplicate-addressed item per market.total_cost_shares_fpcounts seeded contracts once and traded fills as they land, instead of rolling traded shares up against the cost of both.
v0.0.22 — 2026-08-30
- Every world in a
batchgets its own arguments. The bot contract was four environment variables; the argv after--was exec’d byte-identical in every slot, sorystic batch -j 4 -- ./bot --market KXBTC…ran four worlds against one ticker. The same values now substitute into your bot’s own argv as{{world}},{{world_kind}},{{market}},{{addr}}and{{result}}, for a bot that takes them as flags rather than reading the environment. The environment variables are unchanged — this is additive. An unrecognized placeholder is refused before anything boots,-dry-runprints the resolved command per world, and{{market}}is refused ongridandbatch -scenarios, where a world has no market and--market ""is an argument your bot cannot tell from a real one.{{market}}is the market ticker (KXBTC15M-26AUG202330-30); a bot that subscribes by event or series derives those from it — the derivation is in the HOWTO. - Correlated tapes play as one exchange. BTC, gold and silver over the same
session used to mean three processes on three ports, each rebasing its own
first event to now — which erased the offset between the markets, the one
thing a correlated group exists to preserve. Pass
markets: [...]to prepare and start and the group is merge-sorted into a single time-ordered stream: one twin, one port, N tickers, one play head, one rebase offset. The window is trimmed to the overlap; tapes that never overlap are refused with both spans named. - Fills and orders carry the
side/actionfields live still serves. The WebSocket fills channel servesaction_side; REST servesbook_sideandoutcome_side. The deprecated field was absent from the twin, so a bot reading it saw a key error — and stopping one bot and starting another on the same tape surfaced it as apparent state corruption. - 17 documented operations the twin never registered now route, and the read surface answers for what it is served beside: the catalog answers for its book, and the account answers for the trades that moved it. Order entry enforces the bounds it declares, and list endpoints page.
- A miss answers as the exchange. The error contract returns Kalshi’s own shape rather than a generic one, and the parser no longer accepts infinities.
- Smaller surface corrections. Block-trade accept requires the caller to be
a named party.
/exchange/statusomitsexchange_estimated_resume_timerather than sending it null./events/{ticker}/milestonesis served.expiration_timeis enforced as a deadline rather than recorded as a note; an amend keeps what the order was created with; the mutation path sees only resting orders. - Replay accuracy. Tape removals are attributed to trades versus cancels and the queue advances behind-first; a play-head jump re-syncs the applier’s tally. The network profile is re-measured from us-east-2, the region production is served from.
v0.0.21 — 2026-08-27
- The exchange is sharded, the way the real one is. Buying power,
presence and routing are now per exchange shard: an order routes to its
market’s shard and records it (
exchange_indexon the order record), the cash on that shard is what admits it, and the threeportfolio/intra_exchange_instance_transfersroutes move cash between shards. Cancels route too, and the default is shard 0: aDELETE .../events/orders/{id}with no?exchange_indexcannot reach an order resting on any other shard and answers the same 404 as an unknown ID. Pass?exchange_index=<n>, or?exchange_index=-1&market_ticker=…to auto-route by market — wire it now, before your first non-zero-shard market teaches it to you as “my cancels stopped working”. - Post-only meets your own quotes the way live does. A
post_onlyorder underself_trade_prevention_type: makerwhose crossing region — every opposite level at or through its price — holds only your resting orders is now accepted: those orders are canceled in full and the new order rests at its full count, exactly live’s behavior (pinned by an 8-case demo matrix, every order read back to its terminal state). Any other participant’s order in the region, whatever its price priority — or any crossing at all undertaker_at_cross— still answers the exact 400invalid order/post only cross, byte-identical, so a pull-all-quotes trigger keyed on that string keeps firing. An MM re-quoting ask-first over its own resting bid no longer takes a spurious reject. - One trade, one ID, five surfaces. A match reports itself under a single ID everywhere it appears — each party’s REST fill record, each party’s private WS fill frame, and the public WS trade print — live-verified with three subscriptions open across one crossing fill. Correlate the feeds by ID; the heuristics can go.
- Position money at live’s scale.
GET /portfolio/positionsrenders its money fields at six decimal places (“0.114600”), the scale live uses — a client comparing them as strings no longer diverges on every read — and each fill leg is recorded under its own party. - A replay reseed no longer swallows WS frames in flight. On a replayed market, an orderbook delta rendered inside the reseed’s poll window could be dropped as “already reflected” — a resting order’s own delta sometimes never reached the subscriber. The gateway now drains pending frames before and inside the reseed, so the stream is complete across tape snapshots.
- Batched creates are yours alone. The batched order path stamps every order it creates with the caller’s identity exactly as the single-order path does, held in place by its own regression test.
- A fresh install has somewhere to go. The bundled
random-walkerand the “no markets” screen no longer point exclusively at replay — which needs a tape archive a fresh install does not have. Scenarios are compiled into the binary and--list-scenariosnames them, so the quickstart works thirty seconds after the install, tape or no tape.
v0.0.20 — 2026-08-21
- A replayed market knows what it is. Replayed markets now carry the
exchange’s own record for that ticker —
floor_strike,cap_strike,strike_type,custom_strike,occurrence_datetime, the realyes_sub_title/no_sub_title(“Target Price: $64,968.77” rather than “Yes”) and the market’s own declared price grid. On a threshold market the strike is what the market means, and a bot that read it off a replay used to find nothing there. The record comes from your capture’smeta/log when it has one; otherwise the twin fetches it once per tape, unauthenticated, and caches it into the archive so it never fetches again. - That fetch is yours to refuse, and bounded when you keep it.
RYSTIC_REPLAY_METADATA=offdisables it outright — replay is unaffected either way, since a failed or disabled fetch never blocks playback.RYSTIC_REPLAY_METADATA_HOSTS=<host>pins the host it may dial. The twin only ever speakshttps, never follows a redirect off the host it started on, and logs the host it used on the first fetch. Stated plainly because it matters to a trading desk: with the fetch enabled, replaying a tape tells the exchange which historical window you are studying. - The market is replayed as it was, not as it ended. The fetched record is the market at settlement — finalized, with a result. Status, result, settlement values and every time field stay derived from your tape, so you still get an active market with a book that moves.
- The price grid is per market, not per series. Kalshi migrated
KXGOLD15Mfrom whole-cent to tapered deci-cent midway through 2026-08-20: the 06:45Z window and the 09:45Z window of the same series have different tick sizes. The twin serves each market’s own declared grid, and the per-series table is now only a fallback for a tape whose record is unknown. Readprice_rangesper market rather than memorizing which series is which. - A twin that dies says so at once.
rystic run -d,upandcheckused to wait out the full readiness timeout when a twin refused to boot — against a bad license, say — and then blame the timeout. They now notice the exit and name it:the twin exited before serving: exit status 1. - The shipped scenario packs are complete against the current model. Every
loadable pack is now checked field by field as it loads, and the packs that
were short of the widened market and event records were completed
(
35_self_trade_preventionamong them).
v0.0.19 — 2026-08-20
- Backtest results you can read.
rystic batchnow ends in a per-market P&L table —mtm,net,fees,mark,traded,fills,position,balance— with exact totals, and each finished market’s line carries itsmtmandfees.rystic report <out-dir>re-renders the table from a finished run any time, with-csvfor spreadsheets,-sort <column>, and--json. - Honest mark-to-market. The twin values every open position at the
market’s last traded price (shorts at the dual), updated on every trade —
yours or the tape’s — and zeroed at settlement. Markets that were never
settled are called out loudly: realized columns exclude their outcomes,
the mark columns carry the last-price valuation, and the
-settlementsfile realizes them exactly. - Live progress while tapes play. A heartbeat per slot — events played, percent, time left — and a run-wide estimate on every completion, so a 15-minute tape no longer looks like a hang.
rystic doctorand the replay screen speak the CLI. The tapes hint now says-tapes <dir>instead of naming an internal environment variable, and names where tapes come from.
v0.0.18 — 2026-08-20
- Money folds in exact micro-dollars. Balances, positions and fees accumulate in an exact internal register instead of re-reading their own printed 4-dp values, so a long session’s balance is the exact figure rendered once — sub-cent fee debits no longer drift, and the register never leaks onto the wire.
- A minted market declares the grid the exchange declares. Replayed
markets now carry the complete live record — the declared price grid
(
price_level_structure+price_ranges), the full quote/volume block derived from the tape, and a complete parent event — so a bot that indexes the catalog before pricing no longer dies on a missing field. - An unbuilt fleet image names the build.
rystic batch --runner docker --imagewith an image that was never built now says to build it (with the exact command) instead of surfacing Docker Hub’s “may require ‘docker login’” dead end.
v0.0.17 — 2026-08-19
Same simulator as v0.0.16. First release cut, published and verified end to end by the automated pipeline — every archive, checksum, image digest and download endpoint checked per licensee before publication.
v0.0.16 — 2026-08-19
- Ctrl-C stops a docker fleet. Canceling
rystic batch --runner dockernow kills the whole container process group instead of leaving workers running. - Fleet failures name their cause. A world whose twin never served reports the twin’s last log line in the ledger row; an image whose bundled
rysticpredates-tapesays exactly that instead of “exited 2”. - Sharper docker errors. A missing image is “no such image” (exit 5), never mistaken for an auth failure; a container-name collision names the project holding the name.
v0.0.15 — 2026-08-19
- Fleets from your own image.
rystic batch --runner docker --image <yours> -- <bot>runs one container per tape or world from an image you buildFROM registry.rystic.ai/kalshi-twin:<ver>, with a ledger row per world.-tape <id>names one tape exactly. - Stack files. Declare a hand-picked set of twins once in
rystic.yaml;rystic up/down/ps --projectmanage them as a group. - Export.
rystic export composeemits a digest-pinned compose file (license via.env, never in the file);rystic export antithesisscaffolds the config image and launch steps. - Secrets passed to bots ride
-e NAME— values never appear in docker’s argv.
v0.0.14 — 2026-08-18
- Images pull from
registry.rystic.ai.--runner dockerpulls the published per-licensee image through the registry front door — your license file is the docker credential, andrystic loginruns thedocker loginfor you. - The shipped Dockerfile is retired. The container path is the published image; the HOWTO opens with
rystic login/rystic run.
v0.0.13 — 2026-08-18
- Instances.
rystic run -d --name --port,ps,stop,restart,rm,logs,status, a--jsonenvelope on every command, and a stable exit-code contract (8 = runtime unavailable, 9 = conflict). rystic listis the released catalog — latest, installed and running per product your license covers.- A twin says which build it is.
--version,--health, and product/version/watermark onGET /_rystic/version. rystic gridruns the bundled bots across named scenario worlds; the bots take aRYSTIC_TICKSbudget and exit instead of running forever.- The quickstart’s
check.shresolves your license before booting the twin.
Cut for a subset of licensees and superseded the next day — rystic pull may resolve past it; nothing was lost by skipping it.
v0.0.12 — 2026-08-17
- Shipped scenario packs carry complete market records, held by the same completeness gate as the built-in scenarios.
- Order creation validates required parameters as the outermost gate, so a missing field 400s the way production Kalshi does.
v0.0.11 — 2026-08-17
- Complete market records in scenarios. Every scenario market now carries the full Kalshi field set (
close_time,price_level_structure,price_ranges,yes_sub_title, …), so clients that read the whole record no longer crash mid-stream. A seed-completeness gate keeps future scenarios honest. - Per-customer archives. Release assets are watermarked per licensee;
rystic pullresolves the build your license names.
v0.0.7 — 2026-08-12
- Named scenarios ship with the twin.
rystic run kalshi-twin --scenario thin_book;--list-scenariosandSCENARIOS.mdfor the menu. - Evidence-backed fees and P&L. Taker fees micro-ceil per fill, maker fees only on series that declare them, offsetting fills net on the spot, positions settle at expiry.
- Replay runs on the exchange clock, not capture-arrival time. Every tape starts from a flat portfolio.
- State UI. Effective configuration in the header, an Avg cost tile per bot, a Network panel.
rystic checkaccepts204 No Contentfrom/_rystic/reset.
v0.0.6 — 2026-08-11
First build shipped alongside the rystic CLI. Feature set is v0.0.5’s; existing licenses work unchanged.
v0.0.5 — 2026-08-10
- Replay is 9.4× faster, byte-for-byte identically — a 446k-event tape plays in 41s instead of 384s.
- Member fills print to the public tape, sharing one
trade_idsequence with private fills. - The egress log is bounded at 100k frames (
RYSTIC_EGRESS_RETAINto widen) and reportsdropped. - Network shaping, inert by default —
POST /_rystic/network. - Network panel in the state UI.
v0.0.4 — 2026-08-03
First licensed build: the license gate, LICENSE.txt in every archive, a rebased tape that survives restarts, and one actor identity across HTTP, WS, and replay.
v0.0.2 — first limited release
Trade API v2 (88 routes), the matching engine, the WS feed at /trade-api/ws/v2, captured-day replay, and the state UI at /_rystic/ui.