# Observing a simulator

Read a simulator's state, the requests it served, a live event feed and what it would have sent out — from the control plane, rystic tail, or the state UI in a browser.

Every simulator records what it holds, what it was asked and what it would have sent. The examples use the Stripe simulator with one request made; every simulator serves the same routes.

```bash
rystic pull stripe-twin
PORT=$(rystic run stripe-twin -d --name observe --port 0 --scenario design_studio --json | jq -r .data.port)
curl -s localhost:$PORT/v1/customers -u sk_test_twin: -d email=ada@example.com | jq -r .id
# -> cus_lllllllllllllc
```

| Want | Read |
|---|---|
| The whole state | `GET /_rystic/inspect` |
| Every request your code made | `GET /_rystic/calls` |
| A live feed of requests, effects and control calls | `GET /_rystic/events`, `rystic tail` |
| What it would have sent out | `GET /_rystic/egress` |
| All of it in a browser | `/_rystic/ui` |

## State

`GET /_rystic/inspect` returns every register that holds a record, keyed by register, then by record key. An empty simulator answers `{}`.

```bash
curl -s localhost:$PORT/_rystic/inspect | jq -c 'map_values(length)'
# -> {"state_charge":269,"state_customer":31,"state_event":1,"state_payment_intent":276,…}
```

1. The dump is a register map: post it to `/_rystic/seed` after a reset and the state comes back the same ([Seeding](https://www.rystic.ai/docs/concepts/worlds.md#seeding)).
2. `?view=identities` folds the alias keys a world files a user under, such as `member` on Slack and GitHub, into the user they name. Read with it; seed from the default dump, which keeps the aliases.
3. `GET /_rystic/registers` describes each register ([Registers](https://www.rystic.ai/docs/concepts/worlds.md#registers)).

## Requests

`GET /_rystic/calls` lists every API request since the last reset, oldest first. Control-plane calls are not in it.

```bash
curl -s localhost:$PORT/_rystic/calls | jq -c '.calls[0]'
# -> {"actor":"member","at":"2026-03-01T00:00:00.001Z","duration_ms":0,"method":"POST","path":"/v1/customers","principal":"","seq":1,"status":200,"template":"/v1/customers"}
```

1. `actor` is the identity the request acted as, `template` the route it matched, and `at` the simulator's clock.
2. `total` counts every call since the last reset; a reset empties the list and restarts `seq` at 1.
3. It keeps as many calls as the egress log keeps events ([below](#retention)).

## Live feed

`GET /_rystic/events` is a Server-Sent Events stream. Each event is an `id:` line and a `data:` line, of `kind` `request`, `effect` or `control`.

```bash
curl -sN localhost:$PORT/_rystic/events
# -> id: 1
# -> data: {"seq":1,"kind":"control","at":"2026-01-01T00:00:00Z","data":{"detail":null,"op":"reset"}}
# -> id: 2
# -> data: {"seq":2,"kind":"control","at":"2026-03-01T00:00:00Z","data":{"detail":{"frames":0,"name":"design_studio"},"op":"scenario"}}
# -> …
```

1. On connect it sends up to the last 1,000 events since the last reset, then streams live.
2. `seq` keeps counting across resets. Reconnect with `Last-Event-ID: <seq>` or `?cursor=<seq>` to get only what came after.

`rystic tail` prints the same feed, one line per event, stamped with the simulator's time of day:

```bash
rystic tail --addr http://localhost:$PORT
# -> 00:00:00.000 CTL reset
# -> 00:00:00.000 CTL scenario frames=0 name=design_studio
# -> 00:00:00.001 REQ POST /v1/customers -> 200 (0ms)
```

1. The default shows writes and control calls. `--events all` adds reads (`REQ GET …`) and effects (`FX  customer.created …`). [CLI flag reference → tail](https://www.rystic.ai/docs/cli-reference.md#tail) lists the scopes.
2. A detached simulator writes the same lines to its log: `rystic logs -f observe`.

## Egress

`GET /_rystic/egress` holds what the simulator would have sent out, in order: Stripe's webhooks, Resend's emails, Kalshi's WebSocket frames. [Webhooks](https://www.rystic.ai/docs/webhooks.md) covers them.

```bash
curl -s localhost:$PORT/_rystic/egress | jq -c '{total, dropped, types: [.events[].type]}'
# -> {"total":1,"dropped":0,"types":["customer.created"]}
```

1. Each event carries `seq`, `effect`, `type` and `payload`. `?tail=N` returns the last N.
2. `total` counts everything emitted since the last reset; `events` may hold fewer.
3. A reset empties the log and restarts `seq` at 1.

### Retention

1. The log keeps 100,000 events by default. Beyond that the oldest drop, and `dropped` counts them: check it before trusting the log as complete.
2. `RYSTIC_EGRESS_RETAIN` (`rystic run --egress-retain`) sets the bound at boot; `POST /_rystic/egress-retain` with `{"retain":N}` sets it at runtime. `0` removes it.
3. `GET /_rystic/version` shows the bound as `egress_retain`. A reset keeps it.

## The state UI

Open `http://localhost:$PORT/_rystic/ui` in a browser. Its cards:

1. **Activity**: the live feed.
2. **Store**: the state, register by register.
3. **Effects**: the egress log.
4. **Network**: the network profile ([Network profiles](https://www.rystic.ai/docs/control-plane.md#network-profiles-and-fault-injection)).
5. **Reset**, in the header, wipes the simulator as `POST /_rystic/reset` does.

A simulator with its own view of the vendor's app, such as Stripe, Lob and PayPal, adds its cards above these; the simulator's page describes them.

Done with the example: `rystic stop observe`.

---

Source: https://www.rystic.ai/docs/concepts/observing · Markdown: https://www.rystic.ai/docs/concepts/observing.md
