npm packages are not installable yet: [email protected] can be published after 2026-09-16 10:30 UTC. Details
Tollstile

Run reconciliation

Resolve charges left mid-lifecycle by crashes or unknown provider outcomes.

Public Beta · early access

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:

tollstile.config.mjs
import { toll } from "./src/toll.js";
export default toll;
npx tollstile reconcile --older-than 15m
Reconciled 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 …
OptionMeaning
--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
--jsonThe report as JSON
--fail-on-pendingExit 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

Node
setInterval(() => void toll.reconcile(), 60_000);
Cloudflare Workers
export default {
  fetch: app.fetch,
  scheduled: (_event, _env, ctx) => ctx.waitUntil(toll.reconcile()),
};

What it does

ChargeAction
reserved, handler not completedRelease — the service may not exist
reserved, handler completedSettle
settling or unknownLook up at the provider; record what happened, or retry, or release
settled, handler not completedRefund — money moved before the service was confirmed
refund pending or unknownLook 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.

On this page