rystic

Simulators

Stripe simulator

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:

ResourceOperations
Customerscreate, retrieve, update, delete, list
PaymentIntentscreate, retrieve, update, confirm, cancel, list
Chargescreate, retrieve, update, list
Refundscreate, retrieve, update, list
PaymentMethodscreate, retrieve, update, attach, detach
SetupIntentscreate, retrieve, update, confirm, cancel, list
SetupAttemptslist
Checkout Sessionscreate, retrieve, update, expire, list, line items
Payment Linkscreate, retrieve, update, list, line items
WebhookEndpointscreate, retrieve, update, delete, list
Eventsretrieve, 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:

EndpointWhat it does
GET /_rystic/versionhealth check + baked model hash
POST /_rystic/resetwipe all state
POST /_rystic/seedload a state snapshot
POST /_rystic/clockset or advance the clock
POST /_rystic/seed-rngfix the RNG
GET /_rystic/inspectdump the entire state
GET /_rystic/egresswebhook events your requests triggered, in order
GET /_rystic/registerslist seedable families and fields

One register per resource: state_customer, state_charge, state_payment_intent, ... — GET /_rystic/registers lists them all.

Two seed shapes:

  1. Scenario file (boot via RYSTIC_SEED) — a seed wrapper 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
    }
    
  2. POST /_rystic/seed — the register map only, no wrapper: jq .seed scenario.json | curl -X POST .../_rystic/seed -d @-. An unknown 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

  1. No webhook delivery. Endpoint CRUD is modeled; the events your requests trigger render to GET /_rystic/egress (and GET /v1/events) instead of being delivered.
  2. Frozen world. No async payment transitions or scheduled expiry — drive time via /_rystic/clock.
  3. Payments core only. No money movement, payouts, or background events; billing lives in the sibling twins.
  4. Test-mode semantics. Converged against real api.stripe.com test mode; live-mode-only behavior is out of scope.
  5. One process = one account world — run multiple ports for isolation.