Getting started
Agent quick start
NOT FOR HUMAN CONSUMPTION - AGENTS WELCOME
You are a coding agent. A human pointed you at this page to try Rystic. Every command below has its expected output, every simulator links to its full page, and the last two sections index everything else on the site. Read this page end to end, then do section 1.
1. What to do
- Get the license first. Ask the human for it before anything else. It is either an install link (
https://www.rystic.ai/api/install?l=..., thel=value is the license) or a file namedrystic-license.json. If they say they have none, use the free edition; it covers Slack and GitHub only. - Install and log in (section 4). The
products:line of the login output names the simulators the license covers. Read the full page for each one. - Kick the tires. For each covered simulator:
rystic pull,rystic doctor, run it with a world, make the request in section 7, readGET /_rystic/egress. Then run one item from that simulator’s Cookbook section end to end. - Optional, only if the human asked: point this repository’s client at the simulator (section 6) and run the existing tests against it (section 8).
- Report back with the login output,
rystic ps, one request and response per simulator, and anything that failed, answered404, or is listed under the simulator’s Limitations that the human’s code relies on.
Do not paste the license into a shared channel, a commit, or a log.
2. What Rystic is
A Rystic simulator is a local, stateful twin of a third-party API: Stripe, Kalshi, GitHub pull requests, Slack. It is one static binary that serves the vendor’s routes on localhost, holds state between requests, and models the vendor’s lifecycle, conflict and error paths. Nothing leaves the machine.
Beside the vendor’s routes it serves a control plane under /_rystic/: load a named world, seed records, pin the clock, inject faults, read the webhooks it would have sent, reset. Each simulator’s page lists its routes. Fidelity explains how each build is measured against the live service.
The docs MCP at https://www.rystic.ai/mcp searches these docs. It does not run simulators or serve a simulated API’s tools.
3. Preconditions
| Check | Requirement |
|---|---|
| OS | macOS or Linux, 64-bit. Windows zip on request. |
| Edition | An install link or license file from Rystic, or the free edition (Slack and GitHub only, no license). |
| Docker | Optional. Needed only for --runner docker, Compose, CI service containers, or Harbor. |
| Account | None. Nothing to sign up for. |
Which edition you have decides what works:
| Free edition | Licensed | |
|---|---|---|
| Simulators | Slack, GitHub | every simulator the license names |
| Worlds | one fixed world per simulator | the full scenario pack, seeds, tapes |
| Control plane | version, routes, reset, network only; everything else answers 404 naming the paid unlock | all of it |
| Docker images | no | registry.rystic.ai, the license is the credential |
Licensing has the full comparison.
4. Install and authenticate
Licensed, with an install link
Paste the whole line. The long l= value is the license.
curl -fsSL "https://www.rystic.ai/api/install?l=eyJwYXlsb2FkIjoiZXlKc2FXTmxi..." | sh
Licensed, with a license file
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login ./rystic-license.json
Either route ends like this:
installed rystic 0.0.20 → /usr/local/bin/rystic # or ~/.local/bin; RYSTIC_BIN overrides
license: /Users/you/.rystic/rystic-license.json
licensee: Acme
products: [stripe-twin, kalshi-twin] # or (all)
watermark: acme-2026
issued: 2026-08-19
expires: 2027-08-19 (valid)
signature: verified
activated. next: rystic pull # installs the twin your license covers
docker login registry.rystic.ai: skipped — no container runtime answered; when you have one: docker login registry.rystic.ai -u license --password-stdin < <license-file>
The last two lines are fine as printed: name the simulator on pull when the license covers more than one, and Docker is only needed for --runner docker. The license is verified on the machine and parked in ~/.rystic. If Docker is running, login also logs it in to registry.rystic.ai. Then pull and check the simulator you picked in step 5:
rystic pull <simulator>
rystic doctor <simulator>
rystic pull also upgrades an older install, and ends by suggesting rystic check <simulator>: it boots the installed build once and confirms it serves and resets. doctor checks the installed build without booting it, and not whether a newer one exists.
Every ✗ is followed by a → line naming the command that fixes it. ! is a warning. Before the first pull, ✗ install no installed version is normal; its → line says rystic pull <simulator>.
Free edition, no license
One download per simulator, no CLI. Every release replaces the build behind the link.
curl -fsSL -o slack-twin.tar.gz "https://www.rystic.ai/api/free-release?twin=slack-twin&os=$(uname -s | tr A-Z a-z)&arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" # or twin=github-pulls-twin
tar xzf slack-twin.tar.gz
./slack-twin # port taken? ./slack-twin --addr :9000
# expect: rystic: slack-twin <ver> listening on [::]:8080 (model sha256:…) — free edition, world design_studio
--addr :<port> (or RYSTIC_ADDR) moves it off 8080. There is no --help; an unknown flag is ignored and the binary starts. The banner’s API base → line is right as printed: the twin serves every method at both /<method> and /api/<method>, so http://localhost:8080 and http://localhost:8080/api/ both work.
The free edition reports anonymous usage; what it sends lists every field. The licensed twin never phones home.
5. Pick a simulator
One process serves one world. Run several simulators on different ports for several services.
| Simulator | Models | Run | Default world | Full page |
|---|---|---|---|---|
stripe-twin | charges, refunds, payment intents, checkout, customers, webhook egress | rystic run stripe-twin | empty; --scenario busy_account for a month of traffic | Stripe |
stripe-products-twin | Stripe products | rystic run stripe-products-twin | empty | Stripe › Products |
kalshi-twin | Trade API v2, WebSocket feed, real matching engine, tape replay | rystic run kalshi-twin --scenario tight_book | none; a world is required | Kalshi |
github-pulls-twin | pull requests, reviews, merges, checks, git over HTTP, gh GraphQL subset | rystic run github-pulls-twin | empty; free edition ships github-pulls_open_queue | GitHub |
slack-twin | Slack Web API, seeded workspaces, Slack MCP tool subset | rystic run slack-twin --scenario refund_desk | none; a world is required. Without one the workspace is empty: auth.test answers invalid_auth, lists come back empty. Free edition ships design_studio | Slack |
Each full page has the same core sections: Quickstart, Coverage, Auth, Seeding state, Determinism & reset, Limitations. Simulators that send webhooks or frames add Egress log; most add a Cookbook. Read Limitations before writing tests; it names what the twin does not model.
Common run forms:
rystic run stripe-twin # foreground, :8080, Ctrl-C stops
rystic run stripe-twin -d --name dev --port 8080 # detached, named
rystic run stripe-twin -d --port 0 # detached on a free port, printed; use when 8080 is taken
rystic run stripe-twin --scenario busy_account # boot into a named world
rystic run stripe-twin --runner docker -d # as a container
Expected identity line on start:
stripe-twin 0.0.5 (model 95c5dbcd, licensed to Acme until 2027-08-19) → http://localhost:8080 [native default]
Several at once: put them in a rystic.yaml and rystic up -d. The Docker builder writes that file; the CLI page explains it.
6. Point the client at it
Three rules hold for every simulator:
- Base URL. Replace the vendor host with
http://localhost:8080. For Slack,https://slack.com/api/becomeshttp://localhost:8080/api/; the twin also answers without the/api/prefix. For Kalshi the path stays:https://api.elections.kalshi.com/trade-api/v2becomeshttp://localhost:8080/trade-api/v2. - Auth. Any
Authorizationheader is accepted. Use a synthetic token so the change is visible in diffs:sk_test_twin,ghp_twin,xoxb-local. - Code unchanged. Only the client’s constructor or an environment variable changes. The full page for each simulator shows the one-line SDK change in Python and JavaScript.
stripe.api_key = "sk_test_twin"
stripe.api_base = "http://localhost:8080"
const octokit = new Octokit({ auth: "ghp_twin", baseUrl: "http://localhost:8080" });
client = WebClient(token="xoxb-local", base_url=os.environ.get("SLACK_API_BASE", WebClient.BASE_URL)) # http://localhost:8080/api/
The control plane
Same routes on every licensed simulator; only the world names and register families differ.
| Route | Does | Example |
|---|---|---|
GET /_rystic/version | health check, model hash, edition, licensee, expiry | curl -s localhost:8080/_rystic/version |
GET /_rystic/scenarios | list the named worlds compiled into this build | curl -s localhost:8080/_rystic/scenarios |
POST /_rystic/scenario | reset, then apply a named world; answers 204 | curl -X POST localhost:8080/_rystic/scenario -H 'content-type: application/json' -d '{"name":"busy_account"}' |
POST /_rystic/seed | load a register map (the seed key of a scenario file) | jq .seed scenario.json | curl -X POST localhost:8080/_rystic/seed -d @- |
GET /_rystic/registers | every seedable family and its required fields | curl -s localhost:8080/_rystic/registers |
POST /_rystic/clock | set or advance the clock; nothing moves on its own | curl -X POST localhost:8080/_rystic/clock -d '{"time":"2026-01-01T00:00:00Z"}' |
GET /_rystic/egress | the webhooks or WS frames your requests triggered, in order | curl -s 'localhost:8080/_rystic/egress?tail=5' |
GET /_rystic/inspect | dump the entire state | curl -s localhost:8080/_rystic/inspect > state.json |
POST /_rystic/network | latency and fault shaping per endpoint, off by default | curl -X POST localhost:8080/_rystic/network -d '{"enabled":true}' |
POST /_rystic/reset | wipe all state, restart egress seq at 1 | curl -X POST localhost:8080/_rystic/reset |
Two things agents get wrong:
- Webhooks are not delivered. They render into
GET /_rystic/egress. Assert there, not on a receiver. On Stripe,GET /v1/eventsis routed but always empty. - A scenario file has a wrapper. Boot-time
--seed file.jsontakes the whole file;POST /_rystic/seedtakes only its.seedmap.unknown register "seed"means you posted the wrapped file.
7. Verify
Two checks are the same on every simulator, then one vendor-shaped request per simulator below. All three answer as shown: done. Otherwise go to When it fails.
rystic ps
# NAME PRODUCT VERSION RUNNER URL MODEL LICENSEE UPTIME STATUS
# dev stripe-twin … native http://localhost:8080 95c5dbcd Acme 12s running
curl -s localhost:8080/_rystic/version
# {"product":"stripe-twin","version":"…","model_hash":"sha256:…","edition":"", …}
Free edition: rystic ps does not apply (no CLI), edition reads "free", and the request must be one the fixed world supports.
Stripe
curl -s localhost:8080/v1/charges -u sk_test_twin: -d amount=2000 -d currency=usd -d source=tok_visa
# {"id":"ch_000000000000000000000001","amount":2000,…}
curl -s 'localhost:8080/_rystic/egress?tail=1'
# … "charge.succeeded" …
Products is its own binary: curl -s localhost:8080/v1/products -u sk_test_twin: -d name=Widget answers {"id":"prod_…","name":"Widget",…}.
Kalshi
Boots empty; a world is required.
rystic run kalshi-twin -d --name dev --port 8080 --scenario tight_book
curl -s 'localhost:8080/trade-api/v2/markets?limit=1'
# {"cursor":"","markets":[{"ticker":"…","status":"active",…}]}
{"markets":[]} means the world was skipped; the sample bots say no markets in the twin's world. random-walker.py on the Kalshi page runs 40 orders through it; trade-check.sh places one fill-or-kill order on a book of its own and resets the world.
GitHub
Boots empty; load a world, or seed one (Seeding state).
curl -X POST localhost:8080/_rystic/scenario -H 'content-type: application/json' -d '{"name":"github-pulls_open_queue"}'
curl -s localhost:8080/repos/tb-owner/tb-repo/pulls -H 'Authorization: Bearer ghp_twin'
# [{"number":44,…}]
curl -s 'localhost:8080/_rystic/egress?tail=1'
Free edition already sits in github-pulls_open_queue; skip the scenario call.
Slack
Boots empty; a world is required. Without one auth.test answers invalid_auth, conversations.list is [] and posts answer channel_not_found.
rystic run slack-twin -d --name dev --port 8080 --scenario refund_desk
curl -s 'localhost:8080/api/conversations.history?channel=CRYSOPS0001&limit=1' -H 'Authorization: Bearer xoxb-local'
# {"ok":true,"messages":[…]}
Slack sends no webhooks, so its egress log stays empty after a post; that is not a failure. Free edition already sits in design_studio: pick a channel from conversations.list instead of CRYSOPS0001.
8. Worked workflows
Swap the vendor for the twin in an existing test suite
- Find where the client is constructed. Add a base-URL override read from an environment variable (
STRIPE_API_BASE,GITHUB_API_URL,SLACK_API_BASE) that defaults to the vendor. - Start the simulator detached on a fixed port. In test setup,
POST /_rystic/scenariowith the world the test needs, so tests are order-independent. - Replace any assertion on a webhook receiver with a read of
GET /_rystic/egress. - Run the suite. For each failure, check the simulator page’s Limitations before touching the test; the twin may not model that path.
rystic stop devin teardown, or leave it running for the next session.
Run the twin in GitHub Actions
Licensed only. One secret, one service container, no CLI in CI:
gh secret set RYSTIC_LICENSE --body "$(base64 < rystic-license.json | tr -d '\n')"
services:
stripe-twin:
image: registry.rystic.ai/stripe-twin:0.0.5
credentials: { username: license, password: ${{ secrets.RYSTIC_LICENSE }} }
env: { RYSTIC_LICENSE: ${{ secrets.RYSTIC_LICENSE }} }
ports: ["8080:8080"]
The job starts after the image’s HEALTHCHECK passes. Tags are never latest. Integration tests in CI has the whole workflow and the Go test helpers.
Seed a world, inject a fault, assert, reset
curl -X POST localhost:8080/_rystic/scenario -H 'content-type: application/json' -d '{"name":"busy_account"}'
curl -X POST localhost:8080/_rystic/network -d '{"enabled":true}' # see the simulator page for the per-endpoint profile
# run the client; assert it retries or backs off as designed
curl -s localhost:8080/_rystic/egress | jq '.events[] | {seq, effect}'
curl -X POST localhost:8080/_rystic/reset
Each simulator’s Cookbook section has more of these with real ids.
9. When it fails
| You saw | It means | Do this |
|---|---|---|
curl: … error: 403 from the link (22, or 56 on macOS) | the l= value was cut short | paste the full link, or use the license-file route |
command not found: rystic | the binary’s folder is not on PATH | open a new terminal; the installer printed the folder |
exit 3, no license file found, signature does not verify, license_expired | a license problem | License errors |
| exit 9 | port in use | --port 0. rystic ps lists only this Rystic home’s instances, so a port held by anything else needs lsof -i :8080 |
| exit 8, runtime unavailable | Docker not running, only with --runner docker | start Docker, or drop the flag |
404 from /_rystic/seed, /scenario, /egress, /inspect with a body naming a paid unlock | free edition; the control plane is compiled out | use the fixed world, or get a license |
Kalshi {"markets":[]}, or no markets in the twin's world (exit 1) from a sample bot | Kalshi started without a world; the twin itself boots fine and empty | --scenario tight_book, or POST /_rystic/scenario |
unknown register "seed" | posted the wrapped scenario file to /_rystic/seed | jq .seed scenario.json | curl -X POST … -d @- |
422 Validation Failed on POST /pulls after seeding a repository | head and base branches not seeded | seed state_repository, state_user, state_branch, state_commit together |
Slack invalid_auth from auth.test, conversations.list empty, channel_not_found on a real channel | Slack started without a world; the licensed twin boots empty | --scenario refund_desk, or POST /_rystic/scenario |
Slack ok: false with HTTP 200 | a Slack API error, not a transport error | read the body, not the status |
WARNING — incomplete seed in the log | a seeded record is missing required fields | GET /_rystic/registers lists them; RYSTIC_SEED_GATE=reject makes it a refusal |
Useful commands: rystic logs -f <name>, rystic status, rystic doctor <product>. Exit codes lists every code. rystic doctor output names the broken piece without revealing the license.
10. Read next
Simulators
- Stripe: charges, refunds, checkout, customers; Products is a second binary.
- Kalshi: matching engine, WebSocket feed, replay, live mode, multiple bots.
- GitHub: reviews, merges, permission rules, git over HTTP,
ghsupport. - Slack: Web API, seeded workspaces, MCP tool subset.
Run it somewhere else
- Docker: image defaults, Compose, environment variables, many simulators as many containers.
- Docker builder: pick simulators and copy a ready
rystic.yaml, CLI lines and Compose file. - Integration tests in CI: GitHub Actions service container, one secret, your test runner.
- Slack MCP in Harbor: an RL agent using Slack MCP inside a Harbor sandbox, with reset between episodes.
- Slack Harbor reference run: the packaged held-out task on the reference harness.
Data and evaluation
- Kalshi backtesting: replay captured markets and run a bot against every tape with
rystic batch. - Fidelity: how each simulator is measured against the real API.
Reference
- The rystic CLI: every command, the stack file, files and environment, exit codes, what leaves the machine.
- CLI flag reference: every flag, generated from command help.
- Licensing: editions, activation, where the license lives, errors, free edition.
- Installation: the human walkthrough of steps 4 to 7 above.
11. Machine-readable surfaces
| Surface | URL | Use when |
|---|---|---|
| This page as Markdown | https://www.rystic.ai/docs/agent-quick-start.md | first read; append .md to any docs URL for the raw page |
| Agent skill | https://www.rystic.ai/.well-known/agent-skills/rystic/skill.md | your agent loads skills; body is the doc index plus when to reach for a simulator |
| Skills index | https://www.rystic.ai/.well-known/agent-skills/index.json | discovery, with a content digest to detect a changed skill |
| Docs MCP | https://www.rystic.ai/mcp | your agent speaks MCP over HTTP; tools search_rystic_docs and fetch_rystic_doc |
| MCP server card | https://www.rystic.ai/.well-known/mcp/server-card.json | tool schemas without connecting |
llms.txt | https://www.rystic.ai/llms.txt | one line per page, the compact index |
llms-full.txt | https://www.rystic.ai/llms-full.txt | every page as one file, for a single context load |
Claude Code takes the MCP endpoint in one line:
claude mcp add --transport http rystic https://www.rystic.ai/mcp