Skip to Content
Agent quick start

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

  1. 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=..., the l= value is the license) or a file named rystic-license.json. If they say they have none, use the free edition; it covers Slack and GitHub only.
  2. 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.
  3. Kick the tires. For each covered simulator: rystic pull, rystic doctor, run it with a world, make the request in section 7, read GET /_rystic/egress. Then run one item from that simulator’s Cookbook section end to end.
  4. 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).
  5. Report back with the login output, rystic ps, one request and response per simulator, and anything that failed, answered 404, 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

CheckRequirement
OSmacOS or Linux, 64-bit. Windows zip on request.
EditionAn install link or license file from Rystic, or the free edition (Slack and GitHub only, no license).
DockerOptional. Needed only for --runner docker, Compose, CI service containers, or Harbor.
AccountNone. Nothing to sign up for.

Which edition you have decides what works:

Free editionLicensed
SimulatorsSlack, GitHubevery simulator the license names
Worldsone fixed world per simulatorthe full scenario pack, seeds, tapes
Control planeversion, routes, reset, network only; everything else answers 404 naming the paid unlockall of it
Docker imagesnoregistry.rystic.ai, the license is the credential

Licensing has the full comparison.

4. Install and authenticate

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.

SimulatorModelsRunDefault worldFull page
stripe-twincharges, refunds, payment intents, checkout, customers, webhook egressrystic run stripe-twinempty; --scenario busy_account for a month of trafficStripe
stripe-products-twinStripe productsrystic run stripe-products-twinemptyStripe › Products
kalshi-twinTrade API v2, WebSocket feed, real matching engine, tape replayrystic run kalshi-twin --scenario tight_booknone; a world is requiredKalshi
github-pulls-twinpull requests, reviews, merges, checks, git over HTTP, gh GraphQL subsetrystic run github-pulls-twinempty; free edition ships github-pulls_open_queueGitHub
slack-twinSlack Web API, seeded workspaces, Slack MCP tool subsetrystic run slack-twin --scenario refund_desknone; a world is required. Without one the workspace is empty: auth.test answers invalid_auth, lists come back empty. Free edition ships design_studioSlack

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:

  1. Base URL. Replace the vendor host with http://localhost:8080. For Slack, https://slack.com/api/ becomes http://localhost:8080/api/; the twin also answers without the /api/ prefix. For Kalshi the path stays: https://api.elections.kalshi.com/trade-api/v2 becomes http://localhost:8080/trade-api/v2.
  2. Auth. Any Authorization header is accepted. Use a synthetic token so the change is visible in diffs: sk_test_twin, ghp_twin, xoxb-local.
  3. 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.

RouteDoesExample
GET /_rystic/versionhealth check, model hash, edition, licensee, expirycurl -s localhost:8080/_rystic/version
GET /_rystic/scenarioslist the named worlds compiled into this buildcurl -s localhost:8080/_rystic/scenarios
POST /_rystic/scenarioreset, then apply a named world; answers 204curl -X POST localhost:8080/_rystic/scenario -H 'content-type: application/json' -d '{"name":"busy_account"}'
POST /_rystic/seedload a register map (the seed key of a scenario file)jq .seed scenario.json | curl -X POST localhost:8080/_rystic/seed -d @-
GET /_rystic/registersevery seedable family and its required fieldscurl -s localhost:8080/_rystic/registers
POST /_rystic/clockset or advance the clock; nothing moves on its owncurl -X POST localhost:8080/_rystic/clock -d '{"time":"2026-01-01T00:00:00Z"}'
GET /_rystic/egressthe webhooks or WS frames your requests triggered, in ordercurl -s 'localhost:8080/_rystic/egress?tail=5'
GET /_rystic/inspectdump the entire statecurl -s localhost:8080/_rystic/inspect > state.json
POST /_rystic/networklatency and fault shaping per endpoint, off by defaultcurl -X POST localhost:8080/_rystic/network -d '{"enabled":true}'
POST /_rystic/resetwipe all state, restart egress seq at 1curl -X POST localhost:8080/_rystic/reset

Two things agents get wrong:

  1. Webhooks are not delivered. They render into GET /_rystic/egress. Assert there, not on a receiver. On Stripe, GET /v1/events is routed but always empty.
  2. A scenario file has a wrapper. Boot-time --seed file.json takes the whole file; POST /_rystic/seed takes only its .seed map. 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

  1. 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.
  2. Start the simulator detached on a fixed port. In test setup, POST /_rystic/scenario with the world the test needs, so tests are order-independent.
  3. Replace any assertion on a webhook receiver with a read of GET /_rystic/egress.
  4. Run the suite. For each failure, check the simulator page’s Limitations before touching the test; the twin may not model that path.
  5. rystic stop dev in 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 sawIt meansDo this
curl: … error: 403 from the link (22, or 56 on macOS)the l= value was cut shortpaste the full link, or use the license-file route
command not found: rysticthe binary’s folder is not on PATHopen a new terminal; the installer printed the folder
exit 3, no license file found, signature does not verify, license_expireda license problemLicense errors
exit 9port 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 unavailableDocker not running, only with --runner dockerstart Docker, or drop the flag
404 from /_rystic/seed, /scenario, /egress, /inspect with a body naming a paid unlockfree edition; the control plane is compiled outuse the fixed world, or get a license
Kalshi {"markets":[]}, or no markets in the twin's world (exit 1) from a sample botKalshi 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/seedjq .seed scenario.json | curl -X POST … -d @-
422 Validation Failed on POST /pulls after seeding a repositoryhead and base branches not seededseed state_repository, state_user, state_branch, state_commit together
Slack invalid_auth from auth.test, conversations.list empty, channel_not_found on a real channelSlack started without a world; the licensed twin boots empty--scenario refund_desk, or POST /_rystic/scenario
Slack ok: false with HTTP 200a Slack API error, not a transport errorread the body, not the status
WARNING — incomplete seed in the loga seeded record is missing required fieldsGET /_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.

Simulators

  1. Stripe: charges, refunds, checkout, customers; Products is a second binary.
  2. Kalshi: matching engine, WebSocket feed, replay, live mode, multiple bots.
  3. GitHub: reviews, merges, permission rules, git over HTTP, gh support.
  4. Slack: Web API, seeded workspaces, MCP tool subset.

Run it somewhere else

  1. Docker: image defaults, Compose, environment variables, many simulators as many containers.
  2. Docker builder: pick simulators and copy a ready rystic.yaml, CLI lines and Compose file.
  3. Integration tests in CI: GitHub Actions service container, one secret, your test runner.
  4. Slack MCP in Harbor: an RL agent using Slack MCP inside a Harbor sandbox, with reset between episodes.
  5. Slack Harbor reference run: the packaged held-out task on the reference harness.

Data and evaluation

  1. Kalshi backtesting: replay captured markets and run a bot against every tape with rystic batch.
  2. Fidelity: how each simulator is measured against the real API.

Reference

  1. The rystic CLI: every command, the stack file, files and environment, exit codes, what leaves the machine.
  2. CLI flag reference: every flag, generated from command help.
  3. Licensing: editions, activation, where the license lives, errors, free edition.
  4. Installation: the human walkthrough of steps 4 to 7 above.

11. Machine-readable surfaces

SurfaceURLUse when
This page as Markdownhttps://www.rystic.ai/docs/agent-quick-start.mdfirst read; append .md to any docs URL for the raw page
Agent skillhttps://www.rystic.ai/.well-known/agent-skills/rystic/skill.mdyour agent loads skills; body is the doc index plus when to reach for a simulator
Skills indexhttps://www.rystic.ai/.well-known/agent-skills/index.jsondiscovery, with a content digest to detect a changed skill
Docs MCPhttps://www.rystic.ai/mcpyour agent speaks MCP over HTTP; tools search_rystic_docs and fetch_rystic_doc
MCP server cardhttps://www.rystic.ai/.well-known/mcp/server-card.jsontool schemas without connecting
llms.txthttps://www.rystic.ai/llms.txtone line per page, the compact index
llms-full.txthttps://www.rystic.ai/llms-full.txtevery 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
Last updated on