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
| folder | what’s in it |
|---|---|
<simulator>/check.sh | a 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
bashandcurl. The shell scripts run on thebash3.2 that macOS ships. None usesjq.curl7.75 or later fors3/check.sh, which signs its requests with--aws-sigv4.- Python 3.9 or later. The Kalshi scripts use only the standard library; the GitHub bots need
requests(pip install requests). opensslforapi-check.py. Kalshi’s and Clerk’scheck.shuse it when it’s there.- An installed simulator, and the API key or license
rystic loginstored. 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 …)
- 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.
RYSTIC_CHECK_STOP=1stops 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
| simulator | check.sh |
|---|---|
| Kalshi | seeds a market and reads it back; then run trade-check.sh |
| GitHub | lists the open-queue world’s pull requests, reads one, refuses an unknown one |
| Stripe | creates a customer, reads it back, 404 on an unknown id |
| Slack | creates a channel, posts a message, reads both back, refuses an unknown channel |
| Linear | files an issue in the sprint-board world, reads it back, refuses an unknown issue |
| Lob | saves an address, sends a postcard to it, reads both back, refuses an unknown postcard |
| Notion | adds 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 |
| Okta | creates a staged user and a group, adds the user, reads both back, 404 on an unknown user |
| PayPal | creates an order, reads it, patches it, 404 on an unknown capture |
| Resend | creates, reads and lists an email; creates, reads and deletes a webhook; 404 on an unknown email |
| S3 | lists the static-site world’s buckets, puts and reads an object under a key with a slash, deletes it, refuses a missing key |
| Clerk | creates 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 |
| Dropbox | creates a folder, uploads and downloads a file, refuses a duplicate and a missing path with 409, deletes |
| Google Drive | creates 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
book-reader.pyplaces nothing. It polls one book twice a second and warns if the book crosses or a price leaves (0, 1).balance-watcher.pyplaces nothing. Every two seconds it prints balance, positions, resting orders and fills, so you can watch them move beside your own bot.simple-bot.pybuys one YES contract at the best ask every second, immediate-or-cancel.random-walker.pysends 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.- Each bot takes the URL first, else
$RYSTIC_ADDR, elsehttp://localhost:8080.book-reader,simple-botandrandom-walkeruse the first active market unless you pass a ticker second. - The first three run until Ctrl-C. With
RYSTIC_TICKS=5they stop after five ticks and print a verdict. That is howrystic grid kalshi-twinruns 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
trade-check.shresets 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.api-check.pyresets 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
- Every
.ndjson.gzunder the paths you pass is copied toevents/<market_ticker>/. The ticker comes from the file’s first record, so file names don’t matter. - The destination is
$KALSHI_CAPTURE_DIR, default~/data/kalshi-orderbook. That folder is what-tapestakes. - 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.
bot.pykeeps a bid and an ask resting behind the touch on one market, never crosses, and cancels everything atSESSION_SECONDS(default 30) or on SIGTERM. It is configured through the environment:KALSHI_BASE_URL,TICKER,QUOTE_SIZE,MAX_INVENTORYand others.variants/holds five copies ofbot.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 failedbot-smokecheck run. Use them to test a grader or a review agent: approving any of them means it missed the bug.test_bot.pychecks thatbot.pyrests 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
- With no variables, it boots the kalshi-twin
rystic pullinstalled.KALSHI_BASE_URL=http://localhost:8080uses a simulator that’s already running;KALSHI_TWIN_BIN=./kalshi-twinboots that binary. - It loads the
tight_bookworld 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.
| stack | simulators and worlds | ports |
|---|---|---|
stripe-ci | Stripe in design_studio | 8080 |
kalshi-bot | Kalshi twice, in thin_book and flash_crash | 8080–8081 |
coding-agent | GitHub in github-pulls_bot_pr_queue, Linear in linear_triage_inbox, Slack in design_studio | 8080–8082 |
design-studio | Stripe, PayPal, Slack, Resend, Okta and Lob, all one company in design_studio | 8080–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.