Simulators
Containers, Compose and offline runs
The simulator ships as a multi-arch container image beside the native binary, so a bot and its simulator can start together in Compose or CI. An Enterprise image replays a whole session with networking off. The commands below pull images and mount host paths, so they are shown as listings rather than examples the docs check runs. Docker covers the shared parts.
One container
Your API key is the registry credential, so there is nothing else to sign up for. Through the CLI:
rystic login rys_…
rystic stop default
rystic run kalshi-twin --runner docker -d
rystic ps
rystic login stores the key and logs Docker in to registry.rystic.ai. rystic run --runner docker pulls registry.rystic.ai/kalshi-twin:<ver>, runs it and prints the identity line, and rystic ps shows it next to any native instance. With Docker alone (CI, Compose, a teammate who never installs the CLI):
printf %s "$RYSTIC_TOKEN" | docker login registry.rystic.ai -u token --password-stdin
docker pull registry.rystic.ai/kalshi-twin:<ver>
docker run --rm -e RYSTIC_TOKEN -p 8080:8080 registry.rystic.ai/kalshi-twin:<ver>
- The image carries the simulator (its 47 scenarios compiled in), the smoke and example scripts and
rystic. The archive’sscenarios/folder ships in the archive only. - It runs as uid 10001 with a built-in
HEALTHCHECK(kalshi-twin --health). It holds no credential: the key, or an Enterprise license, arrives at run time as an environment value, never in a layer, a build secret or a bind mount. - On Pro the simulator checks in with
lease.rystic.aiwhile it runs, so the container needs outbound HTTPS there. --tapeson the CLI (or-v ~/data/kalshi-orderbook:/tapes:roby hand) mounts a tape library read-only for replay.- Pin by digest for anything that must be reproducible:
docker pullprints it, ordocker image inspect --format '{{.RepoDigests}}' registry.rystic.ai/kalshi-twin:<ver>.
Compose and CI
One file, one secret and no Dockerfile:
services:
kalshi-twin:
image: registry.rystic.ai/kalshi-twin@sha256:<digest> # or :<ver>
environment:
RYSTIC_TOKEN: ${RYSTIC_TOKEN} # your API key, from .env or the CI secret store
volumes: ["rystic-leases:/var/lib/rystic/leases"] # the image's RYSTIC_LEASE_CACHE
init: true
ports: ["8080:8080"]
bot:
image: your-bot:local
environment:
KALSHI_BASE_URL: http://kalshi-twin:8080/trade-api/v2
depends_on:
kalshi-twin: { condition: service_healthy } # the image's own HEALTHCHECK
volumes:
rystic-leases: {}
docker compose up starts the bot only once the simulator serves. The rystic-leases volume keeps the simulator’s last lease across restarts, so a container that ran recently can start once while the lease server is unreachable. The pull needs the docker login above once per machine (in CI, run it in the job with your API key from the secret store). Building your own image on top is one line, FROM registry.rystic.ai/kalshi-twin:<ver>, and the key still comes in at run time.
Enterprise uses its license file instead of a key, everywhere above. Log in with docker login registry.rystic.ai -u license --password-stdin < ./rystic-license.json. Then set RYSTIC_LICENSE: ${RYSTIC_LICENSE} (the license’s base64, or its JSON) in place of RYSTIC_TOKEN, with no lease volume, since an Enterprise simulator never checks in with Rystic.
Under Antithesis the same Compose file is the environment. Once pulled, the simulator is deterministic and needs no network, provided every tape ships its meta/ or RYSTIC_REPLAY_METADATA=off is set (running offline below). So it’s a fixture and not a dependency, which is why Antithesis needs Enterprise.
You do not have to write that file. rystic export compose writes it from a stack file (below), with each image pinned by digest through the registry so the run is reproducible, and a .env.example that names the credential it needs and never a value. rystic export antithesis -o dir --tenant t --gcp-project p adds the FROM scratch config Dockerfile and a README with the tenant push commands. The simulator never emits setup_complete (your workload does), and copying the image into your own registry is something your agreement must permit: confirm with Rystic first.
A hand-picked set: rystic.yaml
Ten simulators are not ten run lines. A stack file names them once, with every key a run flag, and up starts them, ps --project lists them and down stops them:
version: 1
project: demo
services:
maker:
product: kalshi-twin
port: 0
scenario: thin_book
taker:
product: kalshi-twin
runner: docker
port: 0
tapes: ~/data/kalshi-orderbook
tape: KXBTC15M-26JUL222300-00
rystic up -d
rystic ps --project demo
rystic down
rystic up -d prints two identity lines, [native maker] and [docker taker]; without -d, rystic up runs in the foreground and Ctrl-C stops it. A stack file never carries a credential, because rystic login did that once for the machine, and it is what rystic export compose and rystic export antithesis read.
Running offline
An Enterprise image runs a whole replayed session with networking off. Pass the license and the settings below, and copy your cut library in rather than bind-mounting it. The image runs as uid 10001, so make the library world-readable before the copy, because docker cp keeps the modes it finds.
chmod -R a+rX ./data
docker create --name twin --network none \
-e RYSTIC_LICENSE="$RYSTIC_LICENSE" \
-e RYSTIC_REPLAY_METADATA=off \
-e RYSTIC_ADMIN_TOKEN="$ADMIN_TOKEN" \
-e RYSTIC_ACTION_LOG=/work/actions.jsonl \
-e KALSHI_REPLAY_SHARD=0 \
-e KALSHI_CAPTURE_DIR=/data/lib \
-e RYSTIC_INJECT=/data/inject.json \
registry.rystic.ai/kalshi-twin:<ver>
docker cp ./data twin:/data
docker start twin
| Setting | What it does offline |
|---|---|
RYSTIC_LICENSE | Verified against the key stamped in the binary. No call home. |
RYSTIC_REPLAY_METADATA=off | Market records come from each cut’s meta/; nothing is fetched. |
RYSTIC_ADMIN_TOKEN | Gates the control routes. The harness sends it and the agent never sees it. |
RYSTIC_ACTION_LOG | One JSON line per state change, served at /_rystic/actions. |
KALSHI_REPLAY_SHARD=0 | One cash pool across every replayed game. |
KALSHI_CAPTURE_DIR | The session library: one cut archive per game. |
RYSTIC_INJECT | The session’s feed lines and settle calls, loaded at boot. |
The readiness probe is GET /_rystic/version. It needs no admin token and carries no tape or account state, and there’s no /_rystic/health. The image’s HEALTHCHECK calls the same route. Under --network none your harness reaches the simulator from the container’s own network namespace, through docker exec or a sidecar started with --network container:twin. Once the simulator is ready, the harness seeds the account and starts the session. Replay never starts on its own:
twin() { docker run --rm --network container:twin curlimages/curl -sf -H "X-Rystic-Admin-Token: $ADMIN_TOKEN" "$@"; }
twin localhost:8080/_rystic/seed -d '{"state_balance":{"member":{"balance":1000000,"balance_dollars":"10000.0000","portfolio_value":0,"updated_ts":0}}}'
twin localhost:8080/_replay/start -d '{"sequence":["<game-1>","<game-2>"],"gap_ms":60000,"checkpoints":[<first-stop-ms>]}'
twin localhost:8080/_replay/play -d '{"until":"next_event"}'
The sidecar can be any curl image, pulled before you go offline. Step the session with play after each agent move. This path is checked against the release Dockerfile on every nightly Docker run.