Simulators
Stripe simulator
stripe-twin is a local, stateful twin of the Stripe API. Create a customer
and it persists; refund a charge and its status updates. Real lifecycle,
conflict, and error paths — not stubs.
Quickstart
Download stripe-twin into an empty directory first (see
Installation), then paste this block. It
starts the twin, seeds a customer, reads it back, and prints ✓ or the failing
step:
cat > /tmp/twin-check.sh <<'EOF'
#!/bin/bash
[ -f ./stripe-twin ] || { echo "✗ ./stripe-twin not found in $(pwd) — download it first (see rystic.ai/docs/installation)"; exit 1; }
chmod +x ./stripe-twin
./stripe-twin >/tmp/stripe-twin.log 2>&1 &
TWIN_PID=$!
for i in $(seq 1 50); do curl -sf localhost:8080/_rystic/version >/dev/null && break; sleep 0.2; done
fail() { echo "✗ $1 — see /tmp/stripe-twin.log"; kill $TWIN_PID 2>/dev/null; exit 1; }
curl -sf localhost:8080/_rystic/version | grep -q model_hash || fail "twin not responding on :8080"
curl -sf -X POST localhost:8080/_rystic/seed -d '{
"state_customer": { "cus_check": { "id": "cus_check", "object": "customer",
"email": "check@example.com" } } }' || fail "seed rejected"
curl -sf localhost:8080/v1/customers/cus_check \
-H "Authorization: Bearer sk_test_check" | grep -q '"id":"cus_check"' || fail "read-back failed"
curl -sf -X POST localhost:8080/_rystic/reset || fail "reset failed"
echo "✓ stripe-twin is good to go on :8080 (kill $TWIN_PID to stop)"
EOF
bash /tmp/twin-check.sh
Run options:
./stripe-twin # serves :8080
RYSTIC_ADDR=:9000 RYSTIC_SEED=scenario.json ./stripe-twin
Point the SDK at it — base URL only, code unchanged:
stripe.api_key = "sk_test_twin"
stripe.api_base = "http://localhost:8080"
const stripe = require("stripe")("sk_test_twin", {
host: "localhost", port: 8080, protocol: "http",
});
Coverage
49 routes, 11 resources:
| Resource | Operations |
|---|---|
| Customers | create, retrieve, update, delete, list |
| PaymentIntents | create, retrieve, update, confirm, cancel, list |
| Charges | create, retrieve, update, list |
| Refunds | create, retrieve, update, list |
| PaymentMethods | create, retrieve, update, attach, detach |
| SetupIntents | create, retrieve, update, confirm, cancel, list |
| SetupAttempts | list |
| Checkout Sessions | create, retrieve, update, expire, list, line items |
| Payment Links | create, retrieve, update, list, line items |
| WebhookEndpoints | create, retrieve, update, delete, list |
| Events | retrieve, list |
Not included: Products, Prices, Subscriptions, Invoices — those are the
stripe-products and stripe-subscriptions twins.
Auth
Any Authorization header works — no real account, nothing leaves your
machine. Use a placeholder sk_test_... so your SDK sends well-formed
requests.
Seeding state
The /_rystic/ control plane:
| Endpoint | What it does |
|---|---|
GET /_rystic/version | health check + baked model hash |
POST /_rystic/reset | wipe all state |
POST /_rystic/seed | load a state snapshot |
POST /_rystic/clock | set or advance the clock |
POST /_rystic/seed-rng | fix the RNG |
GET /_rystic/inspect | dump the entire state |
GET /_rystic/egress | webhook events your requests triggered, in order |
GET /_rystic/registers | list seedable families and fields |
One register per resource: state_customer, state_charge,
state_payment_intent, ... — GET /_rystic/registers lists them all.
Two seed shapes:
-
Scenario file (boot via
RYSTIC_SEED) — aseedwrapper plus optional determinism knobs:{ "seed": { "state_customer": { "cus_dev": { "id": "cus_dev", "object": "customer", "email": "dev@example.com" } } }, "clock": "2026-07-23T00:00:00Z", "rng_seed": 42 } -
POST /_rystic/seed— the register map only, no wrapper:jq .seed scenario.json | curl -X POST .../_rystic/seed -d @-. Anunknown register "seed"error means you posted the wrapped file.
Success returns 204; rejection returns {"errors":[...]} naming every valid
register.
Determinism & reset
curl -X POST localhost:8080/_rystic/reset
jq .seed scenario.json | curl -X POST localhost:8080/_rystic/seed -d @-
# ... run your payment flow ...
curl -s localhost:8080/v1/charges/ch_1 # assert exact records
curl -s localhost:8080/_rystic/egress # assert exact events
Execution is serialized; clock and RNG are controllable; ids come from the
seeded RNG. Identical runs are byte-identical — if they aren't, the
nondeterminism is in your flow. State is in-memory: restart = clean slate
(plus RYSTIC_SEED if set).
Limitations
- No webhook delivery. Endpoint CRUD is modeled; the events your requests
trigger render to
GET /_rystic/egress(andGET /v1/events) instead of being delivered. - Frozen world. No async payment transitions or scheduled expiry — drive
time via
/_rystic/clock. - Payments core only. No money movement, payouts, or background events; billing lives in the sibling twins.
- Test-mode semantics. Converged against real
api.stripe.comtest mode; live-mode-only behavior is out of scope. - One process = one account world — run multiple ports for isolation.