Skip to main content
A deterministic, in-process digital twin of Stripe’s PaymentIntent, refund and customer API, and of the nine tools Stripe’s MCP server lists for a restricted API key. Agents can create and confirm card payment intents, issue refunds, and poll events without touching Stripe sandbox quota.

By use case

These are the workflows agents actually run against this twin, each naming the MCP tools and REST surfaces involved. Anything named here that is not fully modelled says so in place.

Create and confirm a card PaymentIntent

Create a PaymentIntent with a test card such as pm_card_visa, confirm it, and read the charge it minted. REST: POST /v1/payment_intents, GET /v1/payment_intents/:id, POST /v1/payment_intents/:id/confirm, GET /v1/charges/:id.

Reconcile a refund

Issue a refund against a settled charge, then confirm it against the event log and balance. MCP: stripe_api_write, stripe_api_read. REST: POST /v1/refunds, GET /v1/refunds/:id, GET /v1/charges/:id, GET /v1/events, GET /v1/balance.

Manage a customer and their payment methods

Create a customer record, attach a payment method to it, and look up what is on file before charging again. MCP: stripe_api_write, stripe_api_read. REST: POST /v1/customers, GET /v1/customers/:id, DELETE /v1/customers/:id, GET /v1/customers/:id/payment_methods, POST /v1/payment_methods, POST /v1/payment_methods/:id/attach, POST /v1/payment_methods/:id/detach.

Inspect a subscription and its billing objects

Read products, prices, subscriptions, and invoices. There is no billing engine behind these running real billing-cycle math, and nothing mints an invoice — GET /v1/invoices always returns an empty list. REST: GET /v1/products, GET /v1/prices, GET /v1/subscriptions, GET /v1/subscriptions/:id, GET /v1/invoices, GET /v1/invoices/:id. Every surface named here is shape only — see What you can rely on before you grade on them.

Find and describe an API operation

Search Stripe’s operations by intent and resource, read one operation’s parameters, and call it by operation id through the read and write tools. MCP: stripe_api_search, stripe_api_details, get_stripe_account_info.

Inject a failure and check the retry

Inject a lost-response failure on a refund or PaymentIntent call and confirm the agent retries with the same idempotency key instead of double-charging.

What you can rely on

Each surface below carries two rulings that are deliberately kept apart: heat is how deep it should be, ruled per milestone; state is how deep it is today, measured by the twin’s own tests. Neither moves because the other did. A surface is in exactly one state, and the four below are different answers to “can I write a task against this”: @pome-sh/twin-stripe declares 9 MCP tools and 43 REST surfaces, read from the twin’s own fidelity.inventory.json (last updated 2026-07-12):

Shape only

Read these; do not grade on them. The response shape is right and the behaviour behind it is not asserted, so a criterion that checks a value here can pass for the wrong reason.

Out of scope

Checkout, Connect, webhook delivery loops, and the rest of the Stripe surface beyond what is listed above are not modelled. A Stripe operation the twin does not model answers 400 twin_unsupported_operation naming it, over REST or MCP. A path Stripe does not have answers 501. These surfaces are named rather than left to the catch-all, so the refusal is documented and test-backed:

Known divergence, ruled

14 divergences are on the ledger for Stripe: measured against the real API, found to differ, reviewed, and accepted. Each is registered and reverse-tested, so one that upstream heals becomes a signal rather than a surprise. The numbers are stable identifiers, not positions — a retired divergence leaves its number behind, so a gap in this list is a divergence that closed.

Accepted on the probe ledger

6 differences between a probe’s recording on Stripe and its replay on the twin are reviewed and accepted, each pinned to the one probe and the kinds of difference it shows. A pin that stops matching fails CI. Where it runs. Both doors serve the same twin: pome twin start stripe boots it as a local process, and pome sandbox create --twin stripe starts a hosted sandbox running the same image. The states above are properties of the twin, so they hold on either. Everything above is what the twin declares. What it was measured to do is the probe ledger: every hot surface has probes recorded against a real Stripe test account and replayed on the twin (a success, one common error and, on a write, an idempotent retry), and CI fails a change that leaves a hot surface unproven or short of those probes. A recording older than 90 days counts as unproven, and a weekly job records them again. Its accepted differences are listed above. No count is copied onto this page, because copying one is how it goes stale.

Quickstart

That task boots the Stripe twin, seeds a world from the task, hands your agent a base URL and a token, and scores the run once the agent exits.

Point your agent at it

For interactive development without a full task run, start the standalone twin:
The command prints env-var lines you can paste into your agent’s environment:
A local pome run injects the same three into your agent’s process, pointed at that run’s own sandbox rather than standalone. Hosted sandboxes may instead set POME_STRIPE_API_BASE and POME_STRIPE_API_KEY. It also accepts Stripe-style API keys. Tasks seed a default test key, sk_test_pome_default. Point a Stripe SDK at the REST URL with that key, or send the JWT as Authorization: Bearer <token> — both resolve to the same sandbox.
Replace host and port with the values from your run’s POME_STRIPE_REST_URL. To check whether the twin is alive, use the unauthenticated root health endpoint:

Task seed shape

Stripe task seeds build their world from steps: each step is a Stripe API call the twin runs on the account of the first of api_keys before the agent starts, and pin fixes the ids it mints. clock sets the time they run at, and failure_injection adds its rules. Generate it; do not copy it. The block below is what the twin starts with, printed by the CLI from the twin’s own declared state — so it parses against the twin you are about to seed, today and after the twin changes:
A seed replaces the twin’s starting state. It does not merge into it. Seed your own world and everything below is gone — drop an API key and the key below stops authenticating.
Set twins: ["stripe"] in the task’s ## Config block and name the file <task>.seed.json, beside the task’s .md. That ## Config is the only place the twin gets named: the file above is flat, so it carries no twin id of its own.

Seed file

A task is not the only way in. pome twin start and pome sandbox create take that very same file — no wrapper, no second shape, nothing to convert — and each takes the twin’s name beside it, since the file cannot supply one:
Omit the name and neither door guesses; both stop and say the seed is flat. Add a second twin and the file becomes a per-twin envelope instead, one key each — pome twin new-seed stripe slack writes it:
One twin, one flat file: the twin’s own world with no wrapper, which means it says nothing about which twin it is for. That is why twin start and sandbox create want the name as well, and why a task’s sidecar does not — the ## Config beside it already named the twin. From two twins up the file is a per-twin envelope { <twin>: <seed> }, which does name its twins, though twin start still has to be told which one of them to boot. Replace, not merge, applies wherever the file lands. Build your own world is that job end to end — generating the file, editing it down, both doors, and how to tell whether every field you wrote actually landed.

Example tasks

Ready-made examples you can run or copy to see the twin in action:

Catalog

Run one

Or let the coach pick and run a matching task for you — the pome-suggest-tasks and pome-run-task skills, installed by the graded capstone.