# Sample bots, the grid and the bot contract

Four sample bots, two smoke tests and an API check, `rystic grid`, which runs your bot against every scenario, and the contract a bot keeps under the grid and `rystic batch`.

The release archive ships four standalone bots, two smoke tests, an API check and a tape helper. The CLI runs a bot of yours across a grid of scenarios, one fresh simulator per cell, or across every tape in a library ([Batch backtests](https://www.rystic.ai/docs/simulators/kalshi/features/batch-backtests.md)). The commands below read files from the archive and the CLI install, so they're shown as listings rather than examples the docs check runs.

### Smoke tests

```sh
cd ~/.rystic/twins/kalshi-twin/*/
bash check.sh
bash trade-check.sh
python3 api-check.py http://localhost:8080
```

`check.sh` boots its own simulator on your API key (or Enterprise license), seeds a market and reads it back. It generates a throwaway `twin_key.pem` beside the binary when it can, which needs `openssl` and a writable directory.

`trade-check.sh` seeds a book, crosses it fill-or-kill and asserts fill, position, depletion and WebSocket egress. Pass `host:port` as `$1` if the simulator isn't on `:8080`. It calls `POST /_rystic/reset` and seeds its own book. So the running scenario, balances, orders and egress are gone afterward: post your scenario again.

`api-check.py` also resets the simulator, seeds its own state and walks the Trade API: catalog reads, the order lifecycle, RFQs and quotes, order groups, subaccounts and API keys. It prints one `✓` or `✗` line per check, exits 1 if any fails and needs `openssl`. The tape helper, `capture-setup.sh`, is under [Tapes](https://www.rystic.ai/docs/simulators/kalshi/features/batch-backtests.md#tapes). The container image carries the smoke tests and the bots, but not `api-check.py`, `capture-setup.sh` or the archive's `scenarios/` folder.

### Example bots

Four standalone scripts ship in the archive, standard library only, with nothing to install. Each takes the base URL as its first argument, defaulting to `$RYSTIC_ADDR`, then `http://localhost:8080`. The ones that take a market take its ticker second, defaulting to the simulator's first active market. `balance-watcher` watches the account, not a market:

```sh
python3 simple-bot.py
python3 book-reader.py
python3 balance-watcher.py
python3 random-walker.py
```

- **`simple-bot.py`** buys 1 YES contract at the best ask every second, immediate-or-cancel: the canary.
- **`book-reader.py`** places nothing. It polls the book twice a second and prints the top of book and the mid. It warns if a poll returns a status other than 200, a price leaves (0, 1) or the book crosses.
- **`balance-watcher.py`** places nothing. Every two seconds it prints balance, positions, resting orders and fills on one line, so you can watch them move beside your own bot.
- **`random-walker.py`** places 40 random orders. It exits 0 if every response was a 2xx or a 4xx the API documents, never a 5xx or a hang. Its form is `random-walker.py [base-url] [ticker] [orders] [seed]`, and it replays exactly under a pinned seed, so a failure reruns identically.

`simple-bot`, `book-reader` and `balance-watcher` run until Ctrl-C unless `RYSTIC_TICKS` caps the run (`rystic grid` sets it, and you can too). So start one next to your own bot. On `btc15m_tape` the walker prints a split such as `40 orders -> 201×25, 400×8, 409×7`: accepted, malformed and refused. A simulator that took all 40 wouldn't be modeling an exchange. On a moving tape the counts vary from run to run, and on a static scenario such as `tight_book` they repeat exactly.

### The grid

`rystic grid` runs bots by scenario matrix: every bundled bot, plus your own if you pass one, each against the simulator's declared scenarios, with one fresh simulator per cell.

```sh
rystic grid
rystic grid -worlds all
rystic grid -- python3 my-bot.py
```

| Command | Runs |
|---|---|
| `rystic grid` | the bundled bots across the declared scenarios: `tight_book`, `thin_book`, `empty_book`, `chop`, `trend_up` and `flash_crash` |
| `rystic grid -worlds all` | the bundled bots across every scenario the simulator carries |
| `rystic grid -- python3 my-bot.py` | the bundled bots, plus your bot as another row |

The grid prints each cell, exits non-zero if any cell failed or never ran and writes per-cell logs to `grid-<timestamp>/<bot>/logs/`.

| A cell shows | When it |
|---|---|
| a check mark | passed |
| `✗exit` | exited with a failure |
| `!` | hit a slot error |
| `·` | never ran |

Your bot keeps [the bot contract](#the-bot-contract): read `RYSTIC_ADDR`, and `RYSTIC_WORLD` if you care which scenario you're in, and exit 0 for a pass. The grid also hands every cell `RYSTIC_TICKS` (`-ticks`, default 15). The bundled bots read it as their tick budget, and yours can too.

### The bot contract

The grid and `rystic batch` pass each run's bot the values below, both as environment variables and as placeholders they substitute into the bot's own argv. A scenario run, such as every grid cell, has no market, so it gets all but `RYSTIC_MARKET`:

| Variable | Placeholder | What it is |
|---|---|---|
| `RYSTIC_ADDR` | `{{addr}}` | this run's simulator as `host:port`, with no scheme: point your Kalshi base URL at `http://` plus this |
| `RYSTIC_MARKET` | `{{market}}` | the market ticker the tape folder is named for; unset on a scenario run |
| `RYSTIC_WORLD` | `{{world}}` | this run's id |
| `RYSTIC_WORLD_KIND` | `{{world_kind}}` | `tape` or `scenario`, for a bot that handles both |
| `RYSTIC_RESULT` | `{{result}}` | a file path: JSON your bot writes there lands in its ledger row |
| `PORT` | `{{port}}` | a loopback port no other run's bot holds, for anything your bot serves |

Exit 0 is a pass. A non-zero exit is recorded in the ledger, not fatal, and the batch carries on. A bot still running when its tape ends gets SIGTERM, then SIGKILL five seconds later.

[Batch backtests](https://www.rystic.ai/docs/simulators/kalshi/features/batch-backtests.md#run-your-bot-on-every-tape) shows the placeholders on a fleet of tapes. The CLI checks them before anything boots:

| You pass | What happens |
|---|---|
| an unrecognized placeholder | refused before anything boots, so a typo can't become a run that looks like it worked |
| `{{market}}` on `rystic grid` or `batch -scenarios` | refused, because a scenario has no market and your bot can't tell `--market ""` from a real one: use `{{world}}` there |

---

Source: https://www.rystic.ai/docs/simulators/kalshi/features/bots-and-batches · Markdown: https://www.rystic.ai/docs/simulators/kalshi/features/bots-and-batches.md
