Test payment failures
Simulate provider outages, timeouts, and rejections with the test rail before real money is involved.
import { createTollstile, memoryLedger, testRail } from "tollstile";
import { fakeClock } from "tollstile/testing";
const rail = testRail();
const clock = fakeClock();
const toll = createTollstile({ rails: [rail], ledger: memoryLedger({ clock }), clock });
rail.simulate({ settle: "timeout-after-effect" }); // money moved, but the answer was lost
// …call a priced route: the charge ends `unknown`
rail.simulate({});
clock.advance(60_000);
await toll.reconcile({ olderThanMs: 1_000 });
// the charge is `settled`, and rail.effects.settlements is still 1| Simulation | Effect |
|---|---|
verify: "unavailable" | 503, handler not run |
verify: "paid" | The payment moves during verification, like a pushed on-chain transfer: the charge is settled before the handler |
challenge: "unavailable" | The test rail's offer is left out of the 402; with no other rail, 503 payment_unavailable |
settle: "reject" | Charge failed, SETTLEMENT_REJECTED event; the adapter withholds the output and sends a fresh 402 with error code settlement_rejected |
settle: "timeout-before-effect" | unknown; reconciliation retries or releases |
settle: "timeout-after-effect" | unknown; reconciliation records the settlement once |
refund: "timeout-after-effect" | unknown; reconciliation records the refund once |
lookup: "unavailable" | Reconciliation leaves the charge pending |
rail.effects counts settlements, refunds, and releases so tests can assert nothing happened twice. tollstile/testing also exports httpContext() and mcpContext() to drive gates without a framework.
Retries
Send Idempotency-Key on the first paid request and keep it on retries of the same request (on MCP: _meta["tollstile/idempotency-key"]). A completed charge returns 409 already_paid; an in-flight charge returns 409 request_in_progress; an unknown outcome returns 503 payment_outcome_unknown. Do not start a new payment while the outcome is unknown. Released/refunded attempts may run again. See Idempotency for request matching and rail-specific key scopes.