# Scripted events

Loads a schedule of messages, seeds and calls that fire on the simulator clock, such as a play-by-play feed or a settlement at the final out.

A schedule lists events that fire when the simulator clock reaches them. So a scenario can carry an outside feed (a score, a weather reading) and an ending (a settlement) alongside the market. Each event has a time and one payload:

| Payload | When the event fires, it |
|---|---|
| `message` | lands on `GET /_feed` |
| `seed` | merges into state, as `POST /_rystic/seed` would |
| `call` | calls one of the simulator's own routes outside `/_rystic/`, such as settling a market when its game ends |

```json
{"id":"end","at":"2026-08-22T01:50:07.240Z","call":{"method":"POST","path":"/_settle","body":{"ticker":"KXMLBGAME-26AUG211840STLPHI-PHI","result":"yes"}}}
```

A `call` on a Trade API route runs as the actor `as` names (`member` by default), through the same guards and handler as your own requests. Its WebSocket frames go out on schedule. `as` and `headers` belong to Trade API routes only. An event's `at` is an RFC 3339 time, or `+<duration>` (`+90s`, `+15m`) from the clock when the schedule loads.

Load a schedule with `POST /_rystic/inject`, or at boot with `--inject <file>` or `RYSTIC_INJECT=<file>` (`rystic run --inject <file>` sets it). A file holds either the `{"events":[…]}` body the route takes or one event per line (JSONL). The `empty_book` scenario holds its clock still at 2026-07-25T14:30:00Z, so a schedule fires when you move the clock:

```bash
export RYSTIC_URL=$(rystic run kalshi-twin -d --port 0 --name events --scenario empty_book --json | jq -r .data.url)
curl -s -X POST "$RYSTIC_URL/_rystic/inject" -d '{"events":[{"id":"td","at":"2026-07-25T14:31:12Z","message":{"channel":"play_by_play","body":{"text":"TD KC, 7-3"}}},{"id":"wx","at":"2026-07-25T14:32:00Z","message":{"channel":"weather","body":{"hpa":1004.2}}}]}'
curl -s "$RYSTIC_URL/_feed?since=0"
# -> {"events":[]}
curl -s -X POST "$RYSTIC_URL/_rystic/clock" -d '{"time":"2026-07-25T14:31:30Z"}'
curl -s "$RYSTIC_URL/_feed?since=0"
# -> {"events":[{"seq":1,"time":"2026-07-25T14:31:12Z","channel":"play_by_play","body":{"text":"TD KC, 7-3"}}]}
curl -s -X POST "$RYSTIC_URL/_rystic/clock" -d '{"advance_seconds":60}'
curl -s "$RYSTIC_URL/_feed?since=1"
# -> {"events":[{"seq":2,"time":"2026-07-25T14:32:00Z","channel":"weather","body":{"hpa":1004.2}}]}
```

### When events fire

- **Each event fires once, when the clock is pinned at or past its `at`.** A replaying tape, `POST /_rystic/clock` or a scenario's `clock` moves the clock, and so does pacing. A jump fires every event it passes, in order. The one-millisecond tick per read doesn't count.
- **`RYSTIC_PACE=<speed>` moves the clock by `speed` seconds per second.** `rystic run --inject` sets `1` unless `--speed` says otherwise, and `--tape` and `--live` never set it. Pacing waits while a `/_replay` tape or a live feed drives the clock. `btc15m_tape` drives the clock itself, so don't pace beside it: set `--speed 0`.
- **A replaying tape fires each event on its own step**, after the tape events before its `at` and before the rest, at any speed. Events past the tape's last event never fire, unless a checkpoint past it carries the clock there. Then those due by the stop fire before the pause. Fractional timestamps keep nanosecond precision, and replacing the schedule during playback reschedules pending events.
- **`at` is the clock's time**, so a `rebase` replay shifts the tape away from it. To write `at` in tape time, load the file with the replay, as [script events on tape time](https://www.rystic.ai/docs/simulators/kalshi/features/tape-replay.md#script-events-on-tape-time) describes.
- **`POST /_rystic/reset` re-arms the whole schedule and empties the feed.** `seq` starts again at `1`, and an offset `at` replays at the same instant.

### Reading the feed and the schedule

**`GET /_feed?since=<seq>`** serves only fired messages, never seeds or what is still pending, and only the ones after `seq`. It sits outside `/_rystic/` and the modeled rate limit, so a UI can poll it.

**`GET /_rystic/inject`** lists every event with these fields:

| Field | What it holds |
|---|---|
| `due` | the instant the event is due |
| `fired_at` | when it fired |
| `as` | a Trade API call's actor |
| `status` | a call's status |
| `error` | why the store refused a seed, or the call's 4xx or 5xx |

An error doesn't stop the schedule: later events still fire. A call must name a route the simulator serves, and anything else refuses the whole schedule. A Trade API call's entry in `GET /_rystic/calls` carries `source: "timeline"` and the event's id as `event`.

**A new `POST /_rystic/inject` replaces the schedule**, and fires at once what the clock has already passed. An `id` is an event's identity. An event the old schedule already fired stays fired in the new one, keeping its `fired_at`, its feed line and what it bound. So re-posting an overlapping rolling window never fires an event twice. To fire an edited event again, give it a new `id`.

---

Source: https://www.rystic.ai/docs/simulators/kalshi/features/scripted-events · Markdown: https://www.rystic.ai/docs/simulators/kalshi/features/scripted-events.md
