Skip to content

Payment rails ​

Every service these rails depend on is listed in What we use, and why.

Rails degrade independently. A missing Stripe key does not stop the crypto rails and vice versa. Each route reports 503 rail_unavailable with a clear message rather than throwing an opaque SDK error.

RailAssetFinalityRefund mechanism
StripeCardDaysRefunds API
x402USDC on Base~secondsReturn transfer
MPP / TempoTIP-20 stablecoin~500msReturn transfer

Stripe ​

Stripe is used two ways. Where each sits among the products is on Where the money goes.

Cinemark: pay up front. Stripe Checkout takes the payment, and a refund goes back through the Refunds API.

Gift cards at the register: pay on scan. The shopper saves a card once through Checkout in setup mode. Buying reserves a gift card and charges nothing. Opening the barcode places a manual-capture PaymentIntent for the card's price. If the card worked the hold is captured; if not it is cancelled, so there is nothing to refund. A hold lapses on its own after a few days, so the 15-minute sweep cancels any that are close to lapsing, or that belong to an order that is already over. Each saved card can have $200 reserved or held at once (HOLD_ALLOWANCE_CENTS; 0 turns pay on scan off).

Every call that creates a customer or a hold, or captures, refuses a live key. Cancels and refunds never do, so a key change cannot trap a shopper's money.

Webhook at /api/stripe/webhook, subscribed to checkout.session.completed. It ignores sessions that are not payments, such as a card being saved.

Signature verification is mandatory. An unset STRIPE_WEBHOOK_SECRET rejects all events rather than trusting them.

x402 and MPP ​

Neither has a protocol-level reversal: settlement is a one-way token transfer. A refund is a transfer back from a wallet we control, with a balance precheck so a shortfall produces an operator-actionable message rather than an opaque revert.

MPP/Tempo has no native gas token. Fees come out of a TIP-20 stablecoin balance. So a buyer holding only the stablecoin can transact, and the refund wallet needs enough to cover both the refund and its own fees.

The payer:null bug ​

The MPP receipt has no payer field. We did not drop it: the receipt schema is { method, reference, externalId?, subscriptionId?, status, timestamp } and the Tempo method adds no payer. So receipt.payer was always null, and two separate refund paths were gated on it. Both degraded to "page a human". On testnet with someone watching, that looked fine. On mainnet it would have been a manual refund every time.

The address is recoverable, because the receipt reference is the transaction hash. The recovery prefers the Transfer log crediting our pay-to address over the transaction sender. The order matters: Tempo allows fee sponsorship, so the sender is not guaranteed to be the buyer, even though it was in the observed case.

Recovery returns null rather than throwing, because a failed lookup must not turn a successful payment into an error. The caller decides how loud to be.

The "nothing was charged" lie ​

A post-settlement failure once returned "Nothing was charged." to a buyer whose payment was sitting in the operator wallet. The cause was narrow: only CapacityError took the refund path, and anything else (in the real case, a Prisma constraint violation) fell through to a generic handler.

Now every failure after settlement takes the refund path, and where no payer address is available the response says plainly: "YOU WERE CHARGED." It also tells the buyer the operator has been paged. An honest error is recoverable. A false reassurance is not.

Refund emails are per-rail. Telling an MPP buyer their USDC was returned names a token they never held, and sends them looking for a transfer that does not exist.

Multi-chain escrow ​

CHAINS_ENABLED defaults to empty so a fresh deployment cannot quote a chain it has no escrow on.

There is no default escrow address and no placeholder. An address baked into source that does not exist on the target network fails after a buyer has paid. chainReadiness() names the exact variable each unready chain is waiting on. It is surfaced in /api/health, so a missing address is visible before a buyer funds that chain.

Payout follows the chain the buyer funded. No bridging. A seller preference is recorded and explicitly not acted on: honouring it needs either a bridge or float on every chain, and that is a treasury decision.

Status

Proven on test networks only: Stripe on a test key, and MPP/Tempo with two on-chain settlements on the moderato testnet, paid → fulfilled in about seven seconds.

Built, not proven: x402 on Base, multi-chain escrow (no escrow contract has been deployed), and gift card holds, which are tested against a stand-in for Stripe and have never called Stripe itself.

No mainnet funds have ever moved. The Stripe key in use is a test key.