# Kalshi changelog

What changed in each Kalshi simulator release, newest first.

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_time`s 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](https://www.rystic.ai/docs/cli.md). 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`.

---

Source: https://www.rystic.ai/docs/simulators/kalshi/changelog · Markdown: https://www.rystic.ai/docs/simulators/kalshi/changelog.md
