Run reconciliation
Resolve charges left mid-lifecycle by crashes or unknown provider outcomes.
When a provider times out or a process dies between steps, a charge can be left reserved, settling, unknown, or refund_pending. toll.reconcile() resolves them by asking the provider — never by guessing.
const report = await toll.reconcile({ olderThanMs: 15 * 60_000 });
// { examined: 3, resolved: 2, pending: 1, charges: [...], errors: [...] }charges lists every charge examined with its state before and after; errors lists what went wrong, also delivered to onEvent.
From the command line
Export your instance from a module and run it from cron, CI, or by hand:
import { toll } from "./src/toll.js";
export default toll;npx tollstile reconcile --older-than 15mReconciled 3 charges last updated more than 15m ago
chg_5701… GET /report $0.05 unknown/completed → settled/completed
chg_9a2c… GET /report $0.05 reserved/running → released/failed
chg_c41e… POST /summarize $0.50 unknown/completed · unknown/completed
2 resolved · 1 pending · 1 error
! PROVIDER_UNAVAILABLE chg_c41e…: Provider did not answer within 10000ms …| Option | Meaning |
|---|---|
--config <file> | Default export: the instance, { toll }, or a function returning either. Default tollstile.config.mjs |
--older-than <age> | 90s, 15m, 2h, 1d. Default 15m — longer than your slowest handler |
--json | The report as JSON |
--fail-on-pending | Exit 2 while charges stay unresolved, for alerting |
Exit codes: 0 done, 1 errors reported, 2 pending with --fail-on-pending. It needs a database ledger: a memory ledger in a separate process has nothing to reconcile.
Running several workers at once is safe: a charge another worker moves first is left to it.
Schedule it
setInterval(() => void toll.reconcile(), 60_000);export default {
fetch: app.fetch,
scheduled: (_event, _env, ctx) => ctx.waitUntil(toll.reconcile()),
};What it does
| Charge | Action |
|---|---|
| reserved, handler not completed | Release — the service may not exist |
| reserved, handler completed | Settle |
| settling or unknown | Look up at the provider; record what happened, or retry, or release |
| settled, handler not completed | Refund — money moved before the service was confirmed |
| refund pending or unknown | Look up; record the refund or retry it |
olderThanMs must exceed your slowest handler, so reconciliation never acts on a request still running. Charges it cannot resolve stay pending and emit error events.
Retries
While a keyed charge is unknown, retries return 503 payment_outcome_unknown. After reconciliation records settled/completed, a retry returns 409 already_paid with the stored result reference; reconciliation does not rerun the handler or replay its response. Released/refunded attempts may run again. See Idempotency.