Simulators
Stripe Products simulator
stripe-products-twin is a local, stateful simulator of Stripe’s product catalogue. It is a deliberately small surface: five routes over one resource, held to the same standard as the larger twins — measured against api.stripe.com test mode, with every gap on this page written down rather than papered over.
Quickstart
No stripe-products-twin build has been cut yet. Request access for the first one.
curl -fsSL "https://www.rystic.ai/api/install?l=…" | sh # CLI + license
rystic run stripe-products-twin # pulls it, serves :8080
rystic run stripe-products-twin -d --port 9000 --seed scenario.json
rystic run stripe-products-twin --runner docker -d
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
5 routes, 1 resource.
| Resource | Operations |
|---|---|
| Products | create, retrieve, update, delete, list |
| Route | Modelled responses |
|---|---|
GET /v1/products | 200 |
POST /v1/products | 200, 400 |
GET /v1/products/{id} | 200, 404 |
POST /v1/products/{id} | 200, 404 |
DELETE /v1/products/{id} | 200, 404 |
Prices are not modelled. The slice this twin was cut from covers products and prices; only products have interaction vectors today, so /v1/prices is not routed. Payments core (customers, charges, refunds, checkout) is the Stripe simulator; subscriptions are stripe-subscriptions.
Auth
Any Authorization header works — the twin never validates a credential. Use a placeholder sk_test_... so your SDK sends well-formed requests.
Seeding state
| 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 | the ordered effect log — empty for this twin, see Limitations |
POST /_rystic/egress-retain | bound the egress log (0 = unbounded) |
GET /_rystic/registers | list seedable families and fields |
GET /_rystic/ui | the state browser (browser only) |
One register, state_product. Two seed shapes:
-
Scenario file, booted via
RYSTIC_SEED— aseedwrapper plus determinism knobs.{ "seed": { "state_product": { "prod_dev": { "id": "prod_dev", "object": "product", "name": "Pro plan", "active": true } } }, "clock": "2026-01-01T00:00:00Z", "rng_seed": 42 } -
POST /_rystic/seed— the register map only, no wrapper.jq .seed scenario.json | curl -X POST localhost:8080/_rystic/seed -d @-
Success returns 204; rejection names every valid register. unknown register "seed" means you posted the wrapped file.
GET /_rystic/registers returns the register’s key fields and its schema, which is the authoritative list — this page will not track it.
Determinism & reset
curl -X POST localhost:8080/_rystic/reset
jq .seed scenario.json | curl -X POST localhost:8080/_rystic/seed -d @-
# ... run your catalogue flow ...
curl -s localhost:8080/v1/products/prod_dev # assert the exact record
Execution is serialized, ids are monotonic sequences reset with the state, timestamps come from the pinned clock — which runs forward from its pin, so compare time-bearing fields by shape, not byte-equality. State is in-memory: restart = clean slate (plus RYSTIC_SEED).
Limitations
Every item here is recorded against the operation it belongs to in the sealed model, not discovered later.
- No prices, and therefore no price side-effects.
default_price_dataon create does not mint an attached Price and does not setdefault_price./v1/pricesis not routed at all. - No relation expansion.
expand[]is not honoured;default_price,tax_code,package_dimensionsandmarketing_featurescome back as id or null, never inlined. - Delete does not check for attached prices. Real Stripe answers 400 when a product still has prices or SKUs; this twin does not track those references, so the delete succeeds.
- Field-level validation is presence-and-merge only. A
statement_descriptorthat is too long, more than eight images, or an invaliddefault_pricereference are all accepted where Stripe would answer 400. - No
Idempotency-Keyreplay, and no client-supplied id collisions. Ids are server-minted; a create always creates. - No events. This model declares no effects, so
GET /_rystic/egressstays empty and there is no webhook surface. If you need Stripe events, use the Stripe simulator. - Test-mode semantics. Converged against
api.stripe.comtest mode; live-only behavior is out of scope. - One process = one account world. Run multiple ports for isolation.
Release notes
No numbered build yet. Cut builds get an entry here, newest first.