The Stripe template (apps/stripe-clone, name stripe) is a faithful, API-only
clone of the Stripe REST API: a NestJS backend on Postgres.
Auth is a Stripe-style secret key (sk_test_…) sent as a bearer token — no
signup/login, no per-user session. Every request is scoped to the account that
owns the key. It’s faithful enough at the URL + wire-format level that an
agent built for Stripe can operate it.
Routing — read this first
Real Stripe serves business routes at /v1/* (not /api/v1/*), while the clone
health probe is /api/health. So the backend uses no global prefix: business
controllers bind @Controller('v1/...') and a single health controller binds
@Controller('api'). Both /v1/customers and /api/health resolve faithfully.
What’s inside
| Piece | Notes |
|---|
backend/ | NestJS REST API, Bearer sk_ auth, Postgres via pg. Business at /v1/*, health at /api/health, Swagger at /api/docs. |
supabase/migrations/ | 001_initial_schema.sql + 002_constraints_and_helpers.sql — the schema (17 tables), gen_stripe_id, status checks, run on spin and replayed on reset. |
seeds/ | acct.sql — the bundled deterministic fixture. |
Modes
| Mode | Services | Use it for |
|---|
api (default) | backend | Agents that talk to the REST API. |
This template is API-only — there is no frontend or full mode.
Seeding
asymmetric spin stripe --seed acct # deterministic fixture on create
asymmetric seed stripe-a1b2 --ai # realistic data via the REST API
AI seeding generates customers, products, prices, and invoices (the
template’s declared entities) through real /v1/* calls. See
Seeding.
Auth — the seeded secret key
spin stripe seeds one account and one secret key. The plaintext key (only its
sha256 is stored) is:
sk_test_asymmetric000000000000stripe
Use it as a bearer token against the REST API — no signup/login step:
curl -s http://127.0.0.1:3003/v1/customers \
-H "authorization: Bearer sk_test_asymmetric000000000000stripe"
Real Stripe also accepts the key as the HTTP Basic username, and so does the
clone:
curl -s -u sk_test_asymmetric000000000000stripe: http://127.0.0.1:3003/v1/balance
There is no POST /api/admin/provision route (it returns 404 by design), so the
CLI’s provision step proceeds token-less — the seeded key above is the auth path.
The optional Stripe-Account header is parsed but ignored (single seeded
account).
Two flows worth pointing an agent at
The template is built around two end-to-end money-movement chains (both verified
against a live Postgres):
B=http://127.0.0.1:3003
A="authorization: Bearer sk_test_asymmetric000000000000stripe"
# 1. Subscription that actually goes paid: creates items + the first invoice +
# a PaymentIntent, confirms it to a charge + balance transaction, status=active.
curl -s $B/v1/subscriptions -H "$A" \
-d customer=cus_JennyRosen0001 -d 'items[0][price]=price_ProMonthly01'
# 2. Partial then full refund: amount_refunded increments, a full refund sets
# refunded=true, a negative balance transaction posts, GET /v1/balance reflects
# it, and charge.refunded + refund.created webhooks fire.
curl -s $B/v1/refunds -H "$A" -d charge=ch_SeedPaid000001 -d amount=500
curl -s $B/v1/refunds -H "$A" -d charge=ch_SeedPaid000001
curl -s $B/v1/balance -H "$A"
Money movement is simulated: confirming a PaymentIntent with a seeded test
card (pm_card_visa / pm_card_mastercard) succeeds synchronously. There is no
real PSP, 3DS/SCA, or async bank webhook.
API shape — read this before pointing an agent at it
Unlike the Slack clone (REST-vs-RPC, capability-level match only), Stripe is
itself a REST API, so the clone matches it on the same literal /v1/… paths
with the same wire format — not just the capability level:
- prefixed object ids (
cus_, prod_, price_, pi_, ch_, re_, in_,
sub_, …) — no UUIDs on the wire,
- cursor pagination wrapped in
{object:'list', url, has_more, data} with
limit (default 10, max 100) / starting_after / ending_before,
- form-encoded request bodies (nested
items[0][price] keys), the
Idempotency-Key replay header, a Request-Id: req_… response header, and the
{error:{type,code,message,param}} error envelope,
- per-resource search at
GET /v1/<resource>/search, wrapped in
{object:'search_result', url, has_more, data, next_page},
- per-account rate limiting —
100 req/s for live keys, 25 req/s for test
keys (matching Stripe’s published tiers); over the cap returns HTTP 429 with
a Stripe-Rate-Limited-Reason header and a {error:{code:'rate_limit', …}} body,
- HMAC-signed outgoing webhooks with a
Stripe-Signature: t=…,v1=… header, and a
byte-faithful event envelope (api_version, pending_webhooks,
request, data.previous_attributes).
The template ships an API_PARITY.md with the per-endpoint breakdown, re-audited
against Stripe’s live docs. The implemented core spans customers, products,
prices, payment methods, payment intents (both automatic and manual capture),
charges, refunds, invoices + invoice items, subscriptions + subscription items, a
read-only balance, an events feed, and webhook endpoints — 72 endpoints:
| Status | Count |
|---|
| ✅ exact (path + convention + envelope align) | 72 |
| ❌ missing (sub-actions outside the targeted slice) | 10 |
That’s 88% of the targeted core surface, on exact Stripe paths. Whole Stripe
surfaces outside that core (Connect, Checkout, Terminal, Issuing, tax, disputes,
SetupIntents, …) are unimplemented by design — this is an MVP+Beta subset, not the
full API. A handful of sub-actions in scope are also not yet implemented
(payment_intents/:id/increment_authorization, invoices/:id/send,
subscriptions/:id/resume, customer-scoped payment-method reads). Check
apps/stripe-clone/API_PARITY.md for the full list, including the documented
?expand[] allowlist.
The verify command will fold live fidelity scoring into
the CLI. Until then, API_PARITY.md and the /verify-api workflow are how
fidelity is tracked.
Inspect a running clone
asymmetric query stripe-a1b2 "select stripe_id, email from customers"
asymmetric db stripe-a1b2 # prints a psql shell command
asymmetric logs stripe-a1b2 -f # stream the backend
The schema (customers, charges, invoices, subscriptions, …) comes straight from
001_initial_schema.sql — read it to know what’s queryable.