Seed format reference
A seed is one JSON file describing a world's starting universe. Loading is quiet — no events, no webhooks — so the event log records only what happens after the world exists. The seed's canonical-JSON SHA-256 is its seed_hash; every world records which hash it came from, and all deterministic IDs derive from it.
Validate any seed with:
pnpm exec worlds seed validate my-seed.json
Top level
{
"name": "my-seed", // kebab-case, required
"version": 1, // integer ≥ 1, required
"epoch": 1767225600, // unix seconds — the logical clock starts here, required
"clock_mode": "manual", // "manual" (default) or "auto"
"auto_tick_seconds": 1, // auto mode: clock advance per twin request (default 1)
"data": {
"customers": [...],
"products": [...],
"prices": [...],
"invoices": [...],
"subscriptions": [...],
"tickets": [...]
}
}
clock_mode: "manual" means logical time moves only via worlds clock advance / the admin API. "auto" additionally ticks auto_tick_seconds per twin API request.
All object arrays are optional (default []). Every seeded object carries an explicit id with the right Stripe prefix (cus_, prod_, price_, in_, ch_, sub_) — that's what makes referential integrity checkable and identical seeds materialize identical worlds. created_offset fields are seconds before the epoch (seeded objects are history).
data.customers[]
| field | type | notes |
|---|---|---|
id |
cus_... |
required, unique |
name |
string | required |
email |
string | null | valid email or null |
description, phone |
string | null | optional |
balance |
integer cents | default 0 |
metadata |
object of strings | default {} |
created_offset |
seconds before epoch | default 0 |
data.products[] and data.prices[]
Products: id (prod_), name, optional description, active (default true), metadata.
Prices: id (price_), product (must exist), unit_amount (integer cents), currency (3 letters, default usd), optional nickname, active, metadata, and recurring:
"recurring": { "interval": "month", "interval_count": 1 } // or null for one-time
Only prices with recurring can back subscriptions.
data.invoices[]
{
"id": "in_seed0001",
"customer": "cus_seed0001", // must exist
"status": "paid", // draft | open | paid | void | uncollectible
"currency": "usd",
"created_offset": 86400,
"lines": [ // at least one
{ "description": "Pro Plan", "amount": 4000, "quantity": 1, "price": "price_seed_pro_m" }
],
"charges": [ // only valid on status: "paid"
{ "id": "ch_seed0001", "amount": 4000 } // optional: "amount_refunded"
]
}
lines[].amountis the line total in cents;priceis optional and must exist if given.- Non-draft invoices get a deterministic number (
<customer prefix>-<sequence>) and the matchingstatus_transitionstimestamps. chargesexist only on paid invoices.amountdefaults to the invoice total. More than one charge on a single invoice models a double charge — that's howsaas-billing-smallplants Dana Doublepay's duplicate $40 charge.charges[].amount_refunded— cents already refunded at the epoch (optional, no default). The charge starts with that much refunded, andrefunded: truewhen it equals the amount, so a re-refund is refused exactly as after a live refund. It may not exceed the charge amount. The refund objects themselves are history and are not seeded.
data.subscriptions[]
{
"id": "sub_seed0001",
"customer": "cus_seed0001", // must exist
"price": "price_seed_pro_m", // must exist and be recurring
"quantity": 1,
"current_period_start": 1766361600, // absolute unix seconds; default: created time
"current_period_end": 1769040000, // default: start + one price interval
"cancel_at_period_end": false,
"created_offset": 5184000
}
When the logical clock advances past current_period_end, the world generates the renewal invoice, charges it (auto-pay), emits the events — all timestamped at the boundary — and rolls the period. Set periods straddling the epoch to make renewals reachable with a small clock advance.
data.tickets[]
The demo-grade help desk (/api/v2/tickets…, Zendesk-shaped; scope.md). Tickets carry no explicit id: the loader assigns "1", "2", … in file order, and tickets created at runtime continue the same counter. Linkage to Stripe is textual — requesters mention invoice numbers and emails in the body, as real customers do — so there are no cross-references to validate.
| field | type | notes |
|---|---|---|
subject |
string | required |
body |
string | required |
status |
new | open | pending | hold | solved | closed |
default open |
priority |
low | normal | high | urgent |
default normal |
requester |
{ name, email } |
required; both non-empty, email a valid address |
tags |
string[] | default [] |
created_offset |
integer ≥ 0 | seconds before the epoch; a larger offset is an older ticket, so a queue read oldest-first follows descending offsets |
The tasks file (tasks.md) is compiled into this shape: one ticket per task, thirty minutes apart, oldest first, and a rubric keyed by position.
Referential integrity
worlds seed validate (and world creation) reject seeds with: duplicate ids, unknown product/price/customer references, charges on non-paid invoices, subscriptions on non-recurring prices, or inverted periods. Errors name the exact JSON path.
Importing a seed from a real Stripe account
worlds seed import --from-stripe --key rk_live_... --name acme-prod-shape
Reads the account's shape through ordinary read-only list endpoints (a restricted read-only key is enough) and emits a normal seed file:
- Kept verbatim: product and price ids (your agent's code references them, so remapping would break the code under test — which means a merchant-chosen product or price id, which Stripe accepts at creation, stays verbatim even when it is free text; the manifest lists such ids under
verbatim), amounts, currencies, invoice states and line structure, each charge's refunded total (amount_refunded, so a refunded charge stays refunded in the twin), subscription periods/cadences — including multi-charge invoices, so a real double charge in your account becomes a planted test case. - Replaced: customer names/emails become deterministic fakes derived from a hash of the original id; product names, price nicknames and invoice line descriptions become hash-derived placeholders (
Product Ab3x,Plan Ab3x; a line takes its price's nickname — kept or placeholder, the price's would-be placeholder when it has none — orLine item Nwhen it has no price), because free text can name a client or a person.--keep-nameskeeps product names and price nicknames verbatim,--keep-descriptionskeeps line descriptions verbatim; identities are never kept. - Dropped: customer descriptions, phones, and metadata (
--keep-metadataopts back in for test-mode accounts); metadata on products, prices, invoices and subscriptions is never read. - Remapped:
cus_/in_/ch_/sub_ids are hashed so the seed can't be joined back to the live account by id. - Deterministic: same account state + same flags → identical bytes. The epoch derives from the data (newest object, rounded up to UTC midnight), never from the wall clock. The output is a content-hashed fixture you check into your repo — not a live dependency.
This is pseudonymization, not anonymity: amounts, cadence and timing survive and can identify an account on their own. --manifest <file> writes a machine-readable record of the import beside the seed — how many of each field were replaced, dropped or kept, the merchant-chosen product and price ids copied verbatim (count and up to three examples), the allowlist of seed fields the importer writes (SEED_FIELDS — one list per kind of object; a test walks every produced seed against it), up to three samples of any free text a --keep-* flag let through, how many refunded charges were folded into amount_refunded (the refund objects themselves are not seedable), and the ids of any disputed charges, which the twin imports as ordinary charges because it does not model disputes.
Flags: --base-url (defaults to https://api.stripe.com; any Worlds world works too — that's how the importer is tested), --limit <n> objects per resource (default 100), --out <file>, --manifest <file>, --keep-names, --keep-descriptions, --keep-metadata, --epoch <unix>. Objects that can't be represented (e.g. non-active subscriptions, invoices whose customer is unreachable) are skipped and listed, never half-written; the output is validated with the same rules as seed validate before the file is written.
Exporting a world back to a seed (forking)
The inverse direction: snapshot a live world's state into a new seed.
worlds world export w_abc123def456 --name my-fork
# or: GET /admin/worlds/:id/export-seed?name=my-fork
Run an agent, export the result, and the exported file is a new starting universe — useful for "start the next test where the last one ended" and for turning an interesting hand-built state into a shared fixture. The export always passes seed validate, and export ∘ load is idempotent: re-exporting a round-tripped world yields an identical data block.
A seed describes settled history, so only what the seed format can express survives. The response's lossy report counts everything dropped rather than letting it vanish silently:
- refund objects (each charge's refunded total survives as
amount_refunded, so the fork refuses the same re-refunds), payment intents, and charges not attached to an invoice - pending invoice items (swept ones survive as invoice lines), deleted customers, canceled subscriptions
- the event log — that's run history, not state
The epoch becomes the world's current logical clock and every object's created_offset is preserved relative to it.
Library seeds
| seed | contents |
|---|---|
saas-billing-small |
50 customers, 200 invoices in mixed states, 20 subscriptions, and Dana Doublepay (cus_seed_dana, dana.doublepay@example.com) with two identical $40 charges (ch_seed_dana_1, ch_seed_dana_2) on invoice in_seed_dana |
saas-billing-smoke |
5 customers, 3 invoices (draft/open/paid), 1 subscription — for fast tests |
acme-prod |
Acme Cloud: 505 customers, 1,404 invoices, 186 subscriptions, and an 8-ticket support inbox (data.tickets) with three planted, ticket-shaped issues — the pnpm showcase seed and the key-gated live merge gate |
acme-prod-shift |
the same book of business (byte for byte) plus 22 more cast customers and a 30-ticket inbox — two deliberate doppelgänger pairs, same-customer follow-ups, an authority budget across four outage claims, a prompt-injection ticket. Answer key: rubrics/acme-prod-shift.json; no world-visible field carries one. Generated by scripts/acme-shift.ts. The pnpm shift seed, and the published benchmark: it does not grow |
acme-prod-library |
the starter library: the shift's world and its thirty tickets verbatim (tickets 1-30, their keys unchanged), then the library's cases on more cast customers of the same company — the amount the records set against the amount the ticket quotes, a duplicate beside a legitimate open invoice, a void asked for where a refund is right, a refund already issued, a subsidiary's account, a claimed approval, a chargeback threat, a credit over the cap, a cancellation already scheduled, a plan that does not exist, two more doppelgänger pairs, a second embedded instruction. Every case keyed in rubrics/acme-prod-library.json (the sizes are in that file and on packs/README.md); the seeded history the grader reads — a refunded charge, a credit on a balance, a scheduled cancellation — lives here. Generated by scripts/acme-library.ts on the shift's build. The source of the starter packs, and the no-key path of worlds init |
All five are generated deterministically by scripts/gen-seeds.ts (pnpm seeds:gen — regenerating produces byte-identical files, and pnpm check:generated fails the build when a committed seed and its generator disagree), and so are the two rubrics beside the shift and library seeds and the starter packs under packs/ (scripts/gen-packs.ts, a tasks file per authority — tasks.md). Which builder writes which file: seeds/README.md.