Skip to Content
Examples and scripts

Guides

Examples and scripts

Rystic-Inc/rystic-scripts  holds the scripts that ship in the simulators’ release archives, one folder per simulator, plus four example stacks. The scripts are MIT-licensed: copy, change and share them. The simulators stay under each archive’s LICENSE.txt.

git clone https://github.com/Rystic-Inc/rystic-scripts
folderwhat’s in it
<simulator>/check.sha smoke check, one per simulator
kalshi/trade-check.sh, api-check.py, bots/, recordings/capture-setup.sh
github-pulls/bots/bot.py, test_bot.py and five broken variants/
environments/four example stacks

What you need

  1. bash and curl. The shell scripts run on the bash 3.2 that macOS ships. None uses jq.
  2. curl 7.75 or later for s3/check.sh, which signs its requests with --aws-sigv4.
  3. Python 3.9 or later. The Kalshi scripts use only the standard library; the GitHub bots need requests (pip install requests).
  4. openssl for api-check.py. Kalshi’s and Clerk’s check.sh use it when it’s there.
  5. An installed simulator, and the API key or license rystic login stored. A script that boots a simulator finds them where the CLI does.

check.sh

One per simulator. It boots its own simulator from the binary in its folder, runs a short round trip on that simulator’s API (table below), then resets. The simulator stays up so you can point a client at it.

Run the shipped copy

An installed simulator lives in ~/.rystic/twins/<product>/<version>/ (under $RYSTIC_HOME if you set it): the binary, check.sh, HOWTO.md, LICENSE.txt and any extra scripts. rystic check prints the path:

rystic check stripe-twin
# -> ✓ stripe-twin 0.0.11 serves and resets cleanly (model …)
# ->   deeper product checks: bash /Users/you/.rystic/twins/stripe-twin/0.0.11/check.sh

bash ~/.rystic/twins/stripe-twin/0.0.11/check.sh localhost:18080
# -> ✓ stripe-twin is good to go on localhost:18080 (kill 4242 to stop; its log is …)
  1. The argument is the address to serve on. Without one it takes port 8080; Clerk, Dropbox, Google Drive, Notion and S3 take a free port and print it.
  2. RYSTIC_CHECK_STOP=1 stops the simulator once the checks pass, for CI:
RYSTIC_CHECK_STOP=1 bash ~/.rystic/twins/stripe-twin/0.0.11/check.sh localhost:18081
# -> ✓ stripe-twin passed its checks on localhost:18081 (stopped)

rystic check is the same idea for any simulator: it serves, reports a model hash and resets.

Run the repo copy

check.sh runs the binary next to it. Put the two in a folder of your own, not in ~/.rystic:

mkdir check
cp rystic-scripts/stripe/check.sh check/
ln -s ~/.rystic/twins/stripe-twin/0.0.11/stripe-twin check/
RYSTIC_CHECK_STOP=1 bash check/check.sh localhost:18081
# -> ✓ stripe-twin passed its checks on localhost:18081 (stopped)

What each one checks

simulatorcheck.sh
Kalshiseeds a market and reads it back; then run trade-check.sh
GitHublists the open-queue world’s pull requests, reads one, refuses an unknown one
Stripecreates a customer, reads it back, 404 on an unknown id
Slackcreates a channel, posts a message, reads both back, refuses an unknown channel
Linearfiles an issue in the sprint-board world, reads it back, refuses an unknown issue
Lobsaves an address, sends a postcard to it, reads both back, refuses an unknown postcard
Notionadds a page and a paragraph to the team wiki, reads them back, trashes the page; refuses a missing page and a request without Notion-Version
Oktacreates a staged user and a group, adds the user, reads both back, 404 on an unknown user
PayPalcreates an order, reads it, patches it, 404 on an unknown capture
Resendcreates, reads and lists an email; creates, reads and deletes a webhook; 404 on an unknown email
S3lists the static-site world’s buckets, puts and reads an object under a key with a slash, deletes it, refuses a missing key
Clerkcreates a user and an organization, merges metadata, refuses a missing user and a request with no secret key; with openssl, signs in through the browser companion
Dropboxcreates a folder, uploads and downloads a file, refuses a duplicate and a missing path with 409, deletes
Google Drivecreates a folder, uploads a file into it, downloads it, 404 on a missing file, deletes

Kalshi

Start a simulator with a market in it:

rystic run kalshi-twin -d --scenario tight_book
# -> kalshi-twin 0.0.40 (model …) → http://localhost:8080  [native default]

In the archive these scripts sit next to the binary; in the repo they’re under kalshi/.

Bots

B=rystic-scripts/kalshi/bots
python3 $B/book-reader.py http://localhost:8080
# -> bid 0.3900 x 25  |  ask 0.4000 x 25  |  mid 0.3950
python3 $B/balance-watcher.py http://localhost:8080
# -> balance 10000.0000  |  positions flat  |  0 resting order(s)  |  0 fill(s)
python3 $B/simple-bot.py http://localhost:8080
# -> buy 1 @ 0.4000: filled 1.00 — balance 9999.6000
python3 $B/random-walker.py http://localhost:8080
# -> the twin coped: every order answered, account and book still read
  1. book-reader.py places nothing. It polls one book twice a second and warns if the book crosses or a price leaves (0, 1).
  2. balance-watcher.py places nothing. Every two seconds it prints balance, positions, resting orders and fills, so you can watch them move beside your own bot.
  3. simple-bot.py buys one YES contract at the best ask every second, immediate-or-cancel.
  4. random-walker.py sends 40 random orders from a fixed seed and passes if every answer is a 2xx or a documented 4xx. Arguments after the URL: ticker, order count, seed.
  5. Each bot takes the URL first, else $RYSTIC_ADDR, else http://localhost:8080. book-reader, simple-bot and random-walker use the first active market unless you pass a ticker second.
  6. The first three run until Ctrl-C. With RYSTIC_TICKS=5 they stop after five ticks and print a verdict. That is how rystic grid kalshi-twin runs all four against six worlds.

Checks

bash rystic-scripts/kalshi/trade-check.sh localhost:8080
# -> ✓ trade check passed: FOK bid 10 @ 0.40 crossed the book — fill, position, book depletion, and WS egress all correct

python3 rystic-scripts/kalshi/api-check.py http://localhost:8080
# -> ✓ account.limits: GET account/limits -> 200 (want 200)
# -> …one line per check, then the total
  1. trade-check.sh resets the simulator, seeds a book, crosses it with a fill-or-kill order, and checks the fill, the position, the depleted book and the WebSocket fill in egress.
  2. api-check.py resets the simulator, seeds its own world, and walks 93 checks across the Trade API, one ✓ or ✗ line each. It exits 1 if any check fails.

Sort your captures into tapes

capture-setup.sh doesn’t record anything. It sorts Kalshi capture files you already have into the folder layout replay and backtesting read:

bash rystic-scripts/kalshi/recordings/capture-setup.sh ~/Downloads/captures
# -> ✓ 2 placed, 0 already in place, 0 skipped — 2 markets / 2 files under /Users/you/data/kalshi-orderbook/events
  1. Every .ndjson.gz under the paths you pass is copied to events/<market_ticker>/. The ticker comes from the file’s first record, so file names don’t matter.
  2. The destination is $KALSHI_CAPTURE_DIR, default ~/data/kalshi-orderbook. That folder is what -tapes takes.
  3. Files already in place are counted, not copied again. A file that isn’t gzip is skipped and named.

GitHub review bots

github-pulls/bots/ holds a Kalshi market maker, not a GitHub client. It is the code under review in the GitHub simulator’s github-pulls_bot_pr_queue world.

  1. bot.py keeps a bid and an ask resting behind the touch on one market, never crosses, and cancels everything at SESSION_SECONDS (default 30) or on SIGTERM. It is configured through the environment: KALSHI_BASE_URL, TICKER, QUOTE_SIZE, MAX_INVENTORY and others.
  2. variants/ holds five copies of bot.py, each broken one way: sides swapped, a syntax error, the wrong orders path, prices in cents, a required order field dropped. Each one is an open pull request in that world, #11 to #15, with a failed bot-smoke check run. Use them to test a grader or a review agent: approving any of them means it missed the bug.
  3. test_bot.py checks that bot.py rests both sides, never crosses or fills on a static book, and exits flat. Then it checks that no variant ever rests both sides.
pip install requests
python3 rystic-scripts/github-pulls/bots/test_bot.py
# -> Ran 2 tests in 33.3s
# -> OK
  1. With no variables, it boots the kalshi-twin rystic pull installed. KALSHI_BASE_URL=http://localhost:8080 uses a simulator that’s already running; KALSHI_TWIN_BIN=./kalshi-twin boots that binary.
  2. It loads the tight_book world before each bot.

Run the bot on its own against the Kalshi simulator from above, reset to tight_book:

curl -X POST localhost:8080/_rystic/scenario \
  -H 'content-type: application/json' -d '{"name":"tight_book"}'
KALSHI_BASE_URL=http://localhost:8080 SESSION_SECONDS=5 python3 rystic-scripts/github-pulls/bots/bot.py
# -> rest bid 2@0.38 …
# -> rest ask 2@0.41 …
# -> flat, exiting

Example stacks

environments/ holds four stack files. Each comes with the docker-compose.yaml that rystic export compose writes from it.

stacksimulators and worldsports
stripe-ciStripe in design_studio8080
kalshi-botKalshi twice, in thin_book and flash_crash8080–8081
coding-agentGitHub in github-pulls_bot_pr_queue, Linear in linear_triage_inbox, Slack in design_studio8080–8082
design-studioStripe, PayPal, Slack, Resend, Okta and Lob, all one company in design_studio8080–8085

The stacks serve from port 8080, so stop the Kalshi simulator from above first:

rystic stop default
cd rystic-scripts
rystic up -f environments/coding-agent/rystic.yaml

up holds the terminal, and Ctrl-C stops every service. Environments walks through a stack file.

Last updated on