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

Idempotency

Retry a paid request safely. Payer-scoped keys, recorded outcomes, and retries after released or refunded attempts.

Public Beta · early access

Agents retry. A response lost to a timeout must not turn into a second payment.

curl -H "Payment: …" -H "Idempotency-Key: 8f1c…" https://api.example.com/v1/translate

On MCP, send the key in _meta["tollstile/idempotency-key"]. Adapters forward it; there is nothing to configure.

What a retry gets

A retry by the same payer with the same key finds the charge its first attempt created:

First attemptRetry gets
Still running409 request_in_progress, retry later
Settlement outcome unknown503 payment_outcome_unknown, retry later
Settled and fulfillment completed409 already_paid with chargeId, settlement, and result; not charged, not run
Settlement rejected402 settlement_rejected with a fresh quote
Released or refundedA new charge attempt may run after verification and requirements pass
Settled but fulfillment failed and no automatic refund succeeded409 already_paid; contact the merchant
Same key, different request422 idempotency_key_reused
  • Keys are scoped to the payer. A retry that signs a new payment under the same key is still the same request, and the new payment is not charged. One payer cannot occupy another payer's keys.
  • Request matching: commit: "route" is strengthened to "request" for keyed charges: method, resource, path/query and exact body bytes, or MCP tool and canonical arguments. commit: "request" uses that same hash. A custom commit function is also used for idempotency matching; include every input that distinguishes the operation. This is separate from the quote commitment.
  • Keys must contain 1–255 visible ASCII characters (no spaces). Invalid client keys return 400 invalid_request.
  • A rail may supply a protocol payment identifier when the client sends none; the client key takes precedence. MPP charge rails use their challenge ID; x402 can use its payment-identifier extension.
  • Payer identity limits the scope: MPP Stripe uses stripe:<challengeId>, and L402 uses l402:<paymentHash>. A new challenge or invoice changes that scope. KYAPay uses <issuer>#<sub>.
  • The current implementation allows 20 charge attempts per key. After released/refunded attempts exhaust that limit, it returns 422 idempotency_key_reused.
  • Verification and requirements can reject a request before charge lookup. A key does not bypass expired credentials, current access rules, or single-use identity nonces.
  • A rejected settlement remains failed; the same key continues to report settlement_rejected. After confirming that failure, use a new key and valid proof for a new attempt.

Without a key

When neither the client nor the rail supplies a key:

  • A single-use payment presented again while its charge has not been released is refused with 409 proof_already_used. It is not charged twice, but the retry does not get the original result.
  • A reusable credential (L402, KYAPay, credits) presented again is a new request and is charged again. Send a key when retrying reusable credentials.

Returning the original response

Tollstile does not store handler responses. Store the result where you like, and record where with fulfill:

app.post("/v1/images", tollstile(toll.price("$0.04")), async (c) => {
  const image = await generate(await c.req.json());
  const key = await storage.put(image);
  await c.get("payment").fulfill({ resultRef: key });
  return c.json({ image: storage.url(key) });
});

A retry with the same key gets 409 already_paid with "result": "<key>", so the agent can fetch what it already paid for without paying or running the handler again.

See the Internal Contract for the normative rules.

A resultRef must be 1–1024 characters and contain no secrets. It is a reference, not a cached response or proof of delivery. Subscriber grants create no charge and do not deduplicate handler execution. Credits do create charges and use the policy account as the payer scope.

On this page