Skip to Content
SimulatorsKalshi changelog

Simulators

Kalshi changelog

What changed in each Kalshi simulator release, newest first.

v0.0.55 — 2026-11-06

  1. The page has an Errors and limits section. It shows the error body and the errors shaped otherwise, the 429 bodies of the throttling scenarios and the unenforced limits GET /account/limits reports. It lists the routes that page with their limit defaults and caps, the routes that stop at limit with no next page, and the client_order_id rule that keeps a retry from placing an order twice.

  2. 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

  1. The page keeps only the simulator’s own facts on reset, seeding and sign-in. The shared pages cover the general mechanics.

  2. The page states the clock and --inject rules of a tape. A playing tape or live feed re-pins the clock over any time you set, and rystic up tape services default --inject to $RYSTIC_INJECT.

v0.0.53 — 2026-11-05

  1. The call log shows what each call asked for and whether it succeeded. Each GET /_rystic/calls entry now carries args, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), and ok, false for an answer that is not 2xx. A path the simulator does not serve is marked unknown_route: true. ?tail=N returns only the last N calls, and dropped counts the calls the log no longer keeps.

  2. The docs are one page, with a page per feature. The archive ships README.md in place of HOWTO.md and SCENARIOS.md, with the same sections in the same order as every Rystic simulator’s page, and CHANGELOG.md beside 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 under docs/features/, and the scenario table is generated from the scenarios themselves.

  3. The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.

  4. Scenario descriptions use customer terms. The Scenarios table and --list-scenarios describe each scenario in American spelling, with no internal references.

  5. The docs walk a batch backtest to its P&L table. They now cover the tapes -tapes reads, the six values each bot gets (PORT and {{port}} included), a first backtest with its output, what lands under -out and what batch refuses. They also describe api-check.py, and the missing-tapes hint now says capture-setup.sh sorts captures you already have rather than recording them.

  6. 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.

  7. 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.

  8. 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”.

  9. 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.

  10. 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.

  11. The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

  12. 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

  1. 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.

  2. 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_delta for the liquidity it withdraws, carrying your client_order_id on your own connection, so a book folded from the feed matches GET /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.

  3. GET /markets pages like the exchange. It serves 100 markets by default (limit up to 1000), newest created_time first and returns a cursor while 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.

  4. Long sessions with expiring orders stay fast. The simulator now finds the orders whose expiration_time has 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

  1. 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=0 for an unbounded log, or to another frame count to tune it.

v0.0.50 — 2026-11-02

  1. 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.

  2. 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

  1. 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.

  2. A lifecycle subscription is confirmed at once, even under load. A subscribe to market_lifecycle_v2 or multivariate_market_lifecycle is answered with its subscribed confirmation 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

  1. A state dump seeds back as it was, and the next record gets a fresh id. GET /_rystic/inspect posted to POST /_rystic/seed after 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=sequences lists every id counter, and a world document’s new sequences key restores them, so ids the simulator renders from a counter continue where the dump left off.

  2. 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

  1. The docs explain exchange shards and the replay controls. A new Exchange shards section covers per-shard cash and KALSHI_REPLAY_SHARD; replay documents prepare, stream and what seek’s position counts; and the page links Kalshi’s API reference for parameters and bodies.

  2. The twin documents its own control plane in Swagger. /_rystic/swagger now 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 /_settle and /_account/snapshot routes, and the /_replay/* player.

  3. Replay loads a scripted-event file on tape time. POST /_replay/start takes inject, a file whose at values are tape time, fired as replay reaches each instant, with seed timestamps shifted onto the rebased clock; rystic run --tape --inject sends it, inject files may be JSONL, and GET /_replay/status reports the schedule under inject.

  4. 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 two state_live_data seeds at the same moment give both milestones the same last_updated_ts instead of the second landing a millisecond later. Requests after them still stamp later than any of them.

v0.0.46 — 2026-10-30

  1. 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

  1. 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 at GET /events/{event_ticker} instead of 404ing under a ticker-derived name.

v0.0.44 — 2026-10-26

  1. Markets serve settlement_bounds_type. Every market the twin mints, replays or seeds from a scenario pack now carries settlement_bounds_type, as every live market read does: default (no settlement bounds), or a replayed market’s own value and settlement_floor_dollars when its record names a floor. A client that indexes the field no longer meets its absence.

  2. Re-posting a schedule never fires an event twice. A POST /_rystic/inject that re-sends an id the running schedule already fired keeps that event fired with its original fired_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 new id.

  3. A live_data update moves its milestone’s last_updated_ts. A state_live_data seed that changes a race’s record, by /_rystic/seed or a scheduled inject, stamps that milestone’s last_updated_ts with the twin clock, so a bot polling GET /milestones?min_updated_ts= sees the update land. A seed that changes the milestone’s own last_updated_ts keeps that value.

  4. A replayed event can be mutually exclusive. A replayed market’s minted event took mutually_exclusive: false always; an archive whose meta/ carries the event’s own record ("kind":"event", keyed by event ticker) now sets it from that record, so GET /events/{t} serves true for D/R winner pairs and seat buckets.

  5. 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

  1. Milestone lists page and filter like live. GET /milestones now requires limit (0 or less serves 100, the most is 500), returns a cursor that walks the rest of the list, sorts by start_date then id, and applies minimum_start_date and min_updated_ts. A missing or malformed limit, date or timestamp, or an unreadable cursor, is the 400 live sends.

  2. /_replay/watch is 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.

  3. The state UI follows your OS dark setting. With the system set to dark, /_rystic/ui renders on a dark ground; the Markets card and the rest of the shell follow it. No setting to change.

  4. The state UI stops re-requesting on every feed update. The header fetched /_rystic/version and /_rystic/registers each 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

  1. WebSocket fills preserve trade direction. Fill messages use side: "yes" with action: "buy" for bids and action: "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.

  2. Indexed list query parameters. tickers, ids, milestone_ids and percentiles sent as tickers[0]=…&tickers[1]=…, as some SDKs encode arrays, read the same as the repeated parameter.

  3. 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.

  4. Fill messages carry your client_order_id. WebSocket fill messages now include the client_order_id your order was placed with, as live Kalshi’s do, so a bot can match a fill to its order before it knows the exchange order_id. Orders placed without one still get fills without the field.

  5. Market lists come back in the exchange’s order. GET /markets lists 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

  1. 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.

  2. A paced clock keeps wall time, and rystic run --speed 0 stops it. Under RYSTIC_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 run hands the twin only the pace it resolved, so --speed 0, a world without events, --tape and --live keep the clock manual even when your shell exports RYSTIC_PACE.

  3. Your own exchange key from files. KALSHI_KEY_ID_FILE reads the key ID from a file, and with no key variables set the CLI reads /run/secrets/kalshi_key_id and /run/secrets/kalshi_private_key, so a shared compose.yaml carries your key as two Compose secrets instead of per-person environment. A path in KALSHI_KEY_ID is refused and pointed at KALSHI_KEY_ID_FILE, and a key file the container cannot read names the uid and the fix.

  4. A live setting without live mode is refused. rystic run with a --live-* flag, a live_* stack key or a RYSTIC_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=0 or live: false still turns live off on purpose. A misspelled RYSTIC_LIVE_* variable is refused with the setting it meant, and any variable a typo away from a Rystic setting gets a warning.

  5. Your Rystic API key from a file. RYSTIC_TOKEN_FILE reads the key from a file, and with neither it nor RYSTIC_TOKEN set the CLI reads /run/secrets/rystic_token before config.json and the key rystic login stored, so a shared compose.yaml carries 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

  1. Scheduled calls reach the Trade API, and a world can carry its own timeline. A call event in POST /_rystic/inject or --inject may name a Trade API route and act as the actor its as names, through the same handlers and WebSocket frames as your orders, and an event’s at may be +<duration>. A --seed file, or a complete world document posted to POST /_rystic/seed, may carry its own credentials and events, 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, --inject and RYSTIC_INJECT now need state_set instead of scenarios.

  2. A timeline call can hand what it minted to later events. A call event may add bind, {"<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.

  3. A clock jump fires each timeline event at its own time. When one POST /_rystic/clock or paced tick passes several events, each fires with the clock at its at, 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.

  4. 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.

  5. A timeline call can carry raw bytes, XML or a JSON array. A call event may send body_text or body_base64 instead of body; 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 in body_text. A body may 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.

  6. GET /account/limits shows the advanced API usage level once you upgrade. After POST /account/api_usage_level/upgrade, the limits read answers usage_tier: "advanced", Kalshi’s advanced read and write buckets, and the advanced grant in grants, as Kalshi does. A client can now confirm its upgrade through the public API.

  7. A triggered order group refuses new orders until it is reset. After PUT /portfolio/order_groups/{id}/trigger, an order naming that group in order_group_id is refused and does not rest, as Kalshi documents; PUT /portfolio/order_groups/{id}/reset admits 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

  1. 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 image registry.rystic.ai/kalshi-twin-ui:<ver> beside the twin’s, pulled with the same docker login, and rystic ui runs 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 the verified_ui entitlement is refused with feature_not_in_plan.

  2. The scripts in this archive are MIT-licensed. check.sh and 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.txt no longer covers them; it still governs the twin itself.

v0.0.38 — 2026-10-11

  1. Kalshi is on Pro and Enterprise. A Free API key is refused product_not_in_plan for rystic pull kalshi-twin and a start. On Pro and Enterprise every world, control route and the verified Kalshi UI are open, and without --scenario the twin boots empty.

v0.0.37 — 2026-10-09

  1. The live feeder separates the exchange’s delay from its own. The feeder’s [status] line now reports upstream_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.

  2. 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/feed batch no longer holds order requests until it finishes: they run between chunks of at most 1,000 frames.

  3. 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-events or RYSTIC_LIVE_EVENTS_FILE is now named in the feeder log with the flag that carries it, instead of being reported as an event not yet open. Setting --live-carry beside named markets or events logs that the carry is ignored.

  4. A subaccount transfer is refused when its amount is not positive or exceeds the source balance. POST /portfolio/subaccounts/transfer answers 400 transfer_amount_must_be_positive for a negative amount and 400 insufficient_balance for more than from_subaccount holds, as Kalshi does; neither moves a balance. A bot that relied on the twin accepting those transfers now sees Kalshi’s refusal.

  5. 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.

  6. 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 a fill line for each of your fills with its order_id, trade_id and the recorded time it happened.

  7. 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.

  8. An open state UI no longer slows replay. /_rystic/events sends what queued in one write at most every 5 ms instead of waking for every event, so with /_rystic/ui open a busy replay applies about 17% more events per second. Each event keeps its own frame and ID, and resuming with Last-Event-ID or ?cursor= works as before.

  9. 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.

  10. 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.

  11. 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

  1. Query parsing errors match the demo on sixteen GET routes. Declared query fields return the measured flat 400 message 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.

  2. 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.

  3. 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-trader account; set RYSTIC_ACTOR to use another account.

  4. 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 a session.json whose replay_start body plays them with every offset at 0. The set reads as one continuous tape without the twin. examples/kalshi-sidefeed builds 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.

  5. 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.

  6. 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

  1. Replay stops at an exact tape time. POST /_replay/start takes checkpoints and POST /_replay/play takes until_ms, both in absolute tape milliseconds (the axis of status.start_ms). Playback applies every event through the stop, pauses there, and status.at_checkpoint_ms reports the stop, so the same actions at the same stops give the same fills and balances after a reset.

  2. A windowed capture archive replays its whole window, quiet markets included. An archive cut to a [from, to) window carries a manifest.json that declares the window, an opening book per market and a SHA-256 per file, and holds no frame at or after to. 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.

  3. 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/trades and 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.

  4. Replay can use a zero-fee series seed. Before starting playback, seed the market’s series in state_series with its ticker, fee_type: "quadratic" and fee_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.

  5. Replay steps by a fixed amount of tape or to the next scheduled event. POST /_replay/play takes advance_ms to play that many tape milliseconds from the head and pause, or until: "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, and status.next_event_ms shows the next stop, so a harness can drive a session one step at a time with identical results on every run.

  6. 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 member account. It reaches only the Trade API routes it uses; the existing state UI remains separate.

  7. A market can settle on schedule. A scripted-event schedule now takes a call event that runs one of the twin’s own routes, such as POST /_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’s manifest.json records each market’s outcome, read from its market records, while its meta/ stays free of it, so nothing the agent reads shows the winner early.

  8. 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.

  9. The control plane can be locked away from an agent, and every action logged to disk. With RYSTIC_ADMIN_TOKEN set, /_rystic/*, /_replay/* and /_settle need that value in X-Rystic-Admin-Token (or the rystic_admin_token cookie). GET /_rystic/version stays open as the readiness probe, and so does GET /_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.

  10. 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.

  11. A grader can read a whole account, the action log and the run’s configuration over HTTP. GET /_account/snapshot returns 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, and GET /_rystic/config names 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.

  12. Replay can play games back to back in any order. POST /_replay/start takes a sequence of games, each a market or a list of markets, with gap_ms between 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_DIR can hold one cut archive per game. GET /_replay/status and GET /_rystic/config report each game’s recorded window, session window and offset, so a session time maps back to its recorded time.

  13. The Enterprise image runs a replayed session with the network off. The HOWTO’s new “Running offline” section shows the docker create --network none recipe for a cut library: license, RYSTIC_REPLAY_METADATA=off, the admin token, the action log and a single cash pool. It also names GET /_rystic/version as 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

  1. check.sh fails fast on a port that never answers. Its port check and boot wait give each /_rystic/version probe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging.

  2. Order IDs from the matching engine come from their own range. An order the engine places gets an order_id such as a7cade2e-8c1a-43f4-8000-000000000001 rather than 00000001-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.

  3. market_unopened seeds its market as initialized. The market reports status: "initialized", Kalshi’s status for a market before its open time, where it reported "unopened", which is a GET /markets filter value and never a market’s status. Orders are still refused with 409 market_closed, and GET /markets?status=unopened still lists the market.

  4. Scripted events fire on the sim clock. Load a schedule of timed messages and seeds with POST /_rystic/inject, or --inject <file> / RYSTIC_INJECT at boot. Each event fires once when the clock reaches its at, between the same two tape events at any replay speed. Fired messages are readable at GET /_feed?since=<seq>, and GET /_rystic/inject shows what fired and when.

v0.0.33 — 2026-09-30

  1. The batched cancel is measured against live on the order it places. The test for DELETE /portfolio/events/orders/batched now 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.

  2. 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

  1. 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 — the btc15m_tape world’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.

  2. 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); its expected_expiration_time, latest_expiration_time, expiration_time and occurrence_datetime follow the market’s own geometry on the replayed clock (5 minutes and 7 days past close on a 15-minute market) instead of all reading close_time. After POST /_settle the 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.

  3. 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} and DELETE /portfolio/orders/batched now answer 410 deprecated_v1_order_endpoint with 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 with RYSTIC_KALSHI_LEGACY_ORDERS=on. Four sealed probes hold the refusal.

  4. The portfolio reads publish what live publishes. GET /portfolio/orders lists newest first by the full created_time instant, including fractional seconds, with order ID breaking ties so cursors resume deterministically; an order canceled through DELETE /portfolio/events/orders/batched keeps an RFC 3339 last_update_time instead of a unix integer; every resolved trade from a replayed tape reaches GET /markets/trades with 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 internal owner field.

  5. Error envelopes match the exchange. A path the simulator does not route answers Go’s plain-text 404 page not found like live; GET /markets?status=<anything outside unopened|open|paused|closed|settled> is a 400 bad_request with details invalid status filter instead of an empty page; GET /markets?limit=1001 answers the parameter binder’s {"msg":"Parameter validation failed for GetMarkets: …"}; a routed 404 carries {"error":{"code","message"}} and no service key; and the rate_limited world’s 429 body is the documented {"error":"too many requests"}.

  6. 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 same sid, 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 6 Already subscribed.

v0.0.31 — 2026-09-26

  1. The login offer starts Kalshi in btc15m_tape, and HOWTO.md and the smoke scripts start from an API key. rystic login offers Kalshi booted into btc15m_tape, the world every plan can run, instead of tight_book. HOWTO.md leads with rystic 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 on RYSTIC_TOKEN with the Enterprise license second, sends /_settle with Content-Type: application/json, lists the lease check-in among outbound calls, and says the DELETE /portfolio/events/orders kill switch answers 404, as Kalshi’s demo environment does. check.sh runs on your API key as well as a license and, on Free, waits for btc15m_tape’s market and reads it back instead of seeding; trade-check.sh says it needs Pro or Enterprise when the plan refuses its seed.

v0.0.30 — 2026-09-25

  1. Self-serve on the metered edition replaces the free edition. A Rystic account’s token runs the metered build: rystic login rys_… once, then rystic run or docker 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.

  2. Live mode carries markets by event. --live-events E1,E2, RYSTIC_LIVE_EVENTS, or a shared --live-events-file / RYSTIC_LIVE_EVENTS_FILE (stack keys live_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

  1. Live market selections survive rollover. The feeder reapplies --live-markets and RYSTIC_LIVE_MARKETS_FILE on 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.
  2. WebSocket lifecycle and price-feed channels work. multivariate_market_lifecycle, pyth_value, cfbenchmarks_value, communications and order_group_updates now accept subscriptions. Markets minted by a tape or seed emit lifecycle creation events, and /_settle emits 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

  1. Orders on btc15m_tape now fill. In v0.0.27 every order placed on the recorded KXBTC15M market answered 404 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/balance shows a balance_breakdown of $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

  1. A real recorded session as a named world: btc15m_tape. ./kalshi-twin --scenario btc15m_tape boots 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 /_replay call. 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 served active on 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.md lists it under Recorded) and the one world the free edition serves.
  2. /_rystic/version reports more. The control-plane contract moves from 7 to 9. edition is always present: empty on a licensed build and free on the free edition, so a caller can tell them apart without probing. --version prints the same value as an edition: line. initial_clock is the epoch a reset returns the clock to. api_base_paths, service_hosts, preserve_service_urls and credential_required report settings other twins declare. Kalshi declares none, so they read empty, false and 0, present so one client reads every twin the same way.
  3. The EULA in the archive gains a Free Edition section. LICENSE.txt now 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

  1. Drop a WebSocket connection on demand. GET /_rystic/ws lists every open socket with its subscriptions; POST /_rystic/ws-drop cuts them by tickers (any socket whose subscription names one), by connections (IDs from the listing), or all. The default is a bare close with no close frame, what a crashed server or a severed path looks like; graceful: true sends 1001 going away first. 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.
  2. One tape as many markets. /_replay/prepare and /_replay/start take copies: N, playing one tape as N distinct markets on one clock, named <tape>-1 … <tape>-N, each on its own stream; {"tape", "as"} members in markets pick 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, and status shows which tape each member plays. Each copy is folded separately, so memory scales with copies × tape size. rystic run kalshi-twin --tape <id> --copies N does the same from the CLI, and the copies survive a restart.
  3. A load rig for the reconnect path. examples/kalshi-loadtest in 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.00 means the twin kept up) and reconnect latency. Its README records what one twin sustained; run it on your own hardware before trusting a number.
  4. Every live setting from the environment. --live reads RYSTIC_LIVE=1 and every --live-x-y flag reads RYSTIC_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_B64 carries your own key inline beside KALSHI_PRIVATE_KEY_FILE (the ID and one key form); a stack file’s live: true service takes live_env, live_series, live_max_markets, live_shards, live_carry and live_markets; and rystic run --live --runner docker runs the feeder on the host beside the container, ended first by stop.
  5. 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.
  6. A market always has its event. Seeding a state_market_catalog record (or replaying over one you seeded first, the strike-metadata workflow) now mints the state_event it names when none exists, so GET /events/{event_ticker} and GET /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’s exchange_index instead of a fixed 0, and no longer claims a collateral_return_type the exchange never serves. An event you seed yourself is left as you seeded it.

v0.0.25 — 2026-09-12

  1. 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/pause now keeps that paced position, play (and a speed change mid-flight) resumes from it instead of rewinding to the last applied event, and GET /_replay/status reports 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 uninterrupted play is unchanged.

v0.0.24 — 2026-09-06

  1. The twin can be fed a live stream, not only a recorded tape. rystic run kalshi-twin --live starts 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-series and --live-carry, and spread the carried markets over more sockets with --live-shards. Keys come from KALSHI_KEY_ID and KALSHI_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/feed and 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.

  2. 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 climbing lag means nothing is feeding the twin.

  3. 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_time expired 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_time and ts included. What changes for a bot: nothing expires while the twin is idle — advance the clock past the deadline with POST /_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 carry created_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

  1. The boot banner prints a URL you can paste. 0.0.21 and 0.0.22 announced http://[::]:8080 on all four boot lines — the wildcard bind the listener reports, not a host — while the CLI’s own summary line said localhost:8080 for the same port. The banner now rewrites the unspecified address in all three forms ("", ::, 0.0.0.0) to localhost; an explicit host survives untouched. --health still answers 127.0.0.1, because a distroless image has no resolver.
  2. Two order-entry rejections now come in live’s order and live’s words. A reused client_order_id answers 409 order_already_exists before the balance check, and a fill_or_kill the book cannot cover answers 409 fill_or_kill_insufficient_resting_volume before it too; the twin used to answer 400 insufficient_balance to both, and the duplicate’s envelope (duplicate_client_order_id) was one live has never sent. good_till_canceled and post_only keep answering insufficient_balance pre-match, as live does. One walk of the book per order; nothing is written on a rejection.
  3. 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 on historical/markets/{ticker} and on candlesticks at all three intervals, empty fills, orders and trades. --scenario has_history now boots a world holding both halves — KXDEMO-26-A open and tradeable, KXDEMO-25-SETTLED in the archive behind a 2026-07-20 cutoff — each 404ing on the other’s half, which is the property historical/cutoff exists to express.
  4. Four reads answer the question asked. A candlestick’s end_period_ts is 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_history is one series per event, with the percentile taken across that event’s markets, not one duplicate-addressed item per market. total_cost_shares_fp counts 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

  1. Every world in a batch gets its own arguments. The bot contract was four environment variables; the argv after -- was exec’d byte-identical in every slot, so rystic 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-run prints the resolved command per world, and {{market}} is refused on grid and batch -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.
  2. 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.
  3. Fills and orders carry the side/action fields live still serves. The WebSocket fills channel serves action_side; REST serves book_side and outcome_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.
  4. 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.
  5. 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.
  6. Smaller surface corrections. Block-trade accept requires the caller to be a named party. /exchange/status omits exchange_estimated_resume_time rather than sending it null. /events/{ticker}/milestones is served. expiration_time is 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.
  7. 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

  1. 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_index on the order record), the cash on that shard is what admits it, and the three portfolio/intra_exchange_instance_transfers routes move cash between shards. Cancels route too, and the default is shard 0: a DELETE .../events/orders/{id} with no ?exchange_index cannot 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”.
  2. Post-only meets your own quotes the way live does. A post_only order under self_trade_prevention_type: maker whose 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 under taker_at_cross — still answers the exact 400 invalid 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.
  3. 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.
  4. Position money at live’s scale. GET /portfolio/positions renders 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.
  5. 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.
  6. 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.
  7. A fresh install has somewhere to go. The bundled random-walker and 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-scenarios names them, so the quickstart works thirty seconds after the install, tape or no tape.

v0.0.20 — 2026-08-21

  1. 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 real yes_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’s meta/ 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.
  2. That fetch is yours to refuse, and bounded when you keep it. RYSTIC_REPLAY_METADATA=off disables 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 speaks https, 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.
  3. 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.
  4. The price grid is per market, not per series. Kalshi migrated KXGOLD15M from 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. Read price_ranges per market rather than memorizing which series is which.
  5. A twin that dies says so at once. rystic run -d, up and check used 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.
  6. 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_prevention among them).

v0.0.19 — 2026-08-20

  1. Backtest results you can read. rystic batch now 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 its mtm and fees. rystic report <out-dir> re-renders the table from a finished run any time, with -csv for spreadsheets, -sort <column>, and --json.
  2. 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 -settlements file realizes them exactly.
  3. 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.
  4. rystic doctor and 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

  1. 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.
  2. 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.
  3. An unbuilt fleet image names the build. rystic batch --runner docker --image with 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

  1. Ctrl-C stops a docker fleet. Canceling rystic batch --runner docker now kills the whole container process group instead of leaving workers running.
  2. 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 rystic predates -tape says exactly that instead of “exited 2”.
  3. 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

  1. Fleets from your own image. rystic batch --runner docker --image <yours> -- <bot> runs one container per tape or world from an image you build FROM registry.rystic.ai/kalshi-twin:<ver>, with a ledger row per world. -tape <id> names one tape exactly.
  2. Stack files. Declare a hand-picked set of twins once in rystic.yaml; rystic up / down / ps --project manage them as a group.
  3. Export. rystic export compose emits a digest-pinned compose file (license via .env, never in the file); rystic export antithesis scaffolds the config image and launch steps.
  4. Secrets passed to bots ride -e NAME — values never appear in docker’s argv.

v0.0.14 — 2026-08-18

  1. Images pull from registry.rystic.ai. --runner docker pulls the published per-licensee image through the registry front door — your license file is the docker credential, and rystic login runs the docker login for you.
  2. 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

  1. Instances. rystic run -d --name --port, ps, stop, restart, rm, logs, status, a --json envelope on every command, and a stable exit-code contract (8 = runtime unavailable, 9 = conflict).
  2. rystic list is the released catalog — latest, installed and running per product your license covers.
  3. A twin says which build it is. --version, --health, and product/version/watermark on GET /_rystic/version.
  4. rystic grid runs the bundled bots across named scenario worlds; the bots take a RYSTIC_TICKS budget and exit instead of running forever.
  5. The quickstart’s check.sh resolves 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

  1. Shipped scenario packs carry complete market records, held by the same completeness gate as the built-in scenarios.
  2. 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

  1. 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.
  2. Per-customer archives. Release assets are watermarked per licensee; rystic pull resolves the build your license names.

v0.0.7 — 2026-08-12

  1. Named scenarios ship with the twin. rystic run kalshi-twin --scenario thin_book; --list-scenarios and SCENARIOS.md for the menu.
  2. 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.
  3. Replay runs on the exchange clock, not capture-arrival time. Every tape starts from a flat portfolio.
  4. State UI. Effective configuration in the header, an Avg cost tile per bot, a Network panel.
  5. rystic check accepts 204 No Content from /_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

  1. Replay is 9.4× faster, byte-for-byte identically — a 446k-event tape plays in 41s instead of 384s.
  2. Member fills print to the public tape, sharing one trade_id sequence with private fills.
  3. The egress log is bounded at 100k frames (RYSTIC_EGRESS_RETAIN to widen) and reports dropped.
  4. Network shaping, inert by default — POST /_rystic/network.
  5. 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.

Last updated on