Skip to Content
SimulatorsStripe Products

Simulators

Stripe Products simulator

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.

ResourceOperations
Productscreate, retrieve, update, delete, list
RouteModelled responses
GET /v1/products200
POST /v1/products200, 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
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/egressthe ordered effect log — empty for this twin, see Limitations
POST /_rystic/egress-retainbound the egress log (0 = unbounded)
GET /_rystic/registerslist seedable families and fields
GET /_rystic/uithe state browser (browser only)

One register, state_product. Two seed shapes:

  1. Scenario file, booted via RYSTIC_SEED — a seed wrapper 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
    }
    
  2. 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.

  1. No prices, and therefore no price side-effects. default_price_data on create does not mint an attached Price and does not set default_price. /v1/prices is not routed at all.
  2. No relation expansion. expand[] is not honoured; default_price, tax_code, package_dimensions and marketing_features come back as id or null, never inlined.
  3. 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.
  4. Field-level validation is presence-and-merge only. A statement_descriptor that is too long, more than eight images, or an invalid default_price reference are all accepted where Stripe would answer 400.
  5. No Idempotency-Key replay, and no client-supplied id collisions. Ids are server-minted; a create always creates.
  6. No events. This model declares no effects, so GET /_rystic/egress stays empty and there is no webhook surface. If you need Stripe events, use the Stripe simulator.
  7. Test-mode semantics. Converged against api.stripe.com test mode; live-only behavior is out of scope.
  8. One process = one account world. Run multiple ports for isolation.
Release notes

No numbered build yet. Cut builds get an entry here, newest first.

Last updated on