Skip to content

Accepted vs finalized

A SubEtha payment settles in two phases with two names, and every page on this site keeps them distinct. accepted is what the HTTP request path can observe; finalized happens minutes later and never on the request path. Neither implies the other has happened, and no API response, log line, or doc sentence may use one to mean the other.

x402 is synchronous: the provider must confirm payment before serving the resource. But zERC20’s construction earns its on-chain properties from the time gap between the burn and the proven mint — the mint must not happen in the same breath as the burn. SubEtha resolves that tension by splitting settlement: serve on the burn (accepted), mint later (finalized).

accepted — synchronous, on the request path

Section titled “accepted — synchronous, on the request path”

A payment is accepted when the burn transfer to the offer’s one-time payTo address is confirmed on-chain. Under the default acceptOnReceipt behavior that means, concretely:

  1. the burn transaction has a successful receipt (so it is mined — the facilitator effectively waits about one block),
  2. the transaction’s IndexedTransfer event pays at least the quoted amount to the burn address on the right token, and
  3. the burn address holds the funds.

The facilitator does not wait for the zERC20 indexer to ingest the transfer before accepting (acceptOnReceipt defaults to true; set it false for the conservative wait-for-ingest behavior). That wait is not needed for freeloader resistance — a successful receipt already means the funds are burned and the payer cannot reclaim them — so it is paid later, by finalization, which cannot proceed without it anyway.

On the x402 wire, /settle success is exactly accepted. The SettleResponse carries extra: { phase: "accepted" }, and its transaction field is the burn transaction hash — the on-chain event a payer can audit — never the mint.

Serving is fail-closed: the middleware runs verify → handler (response buffered) → settle, and if settle does not succeed the buffered response is discarded and the client receives a 402. Nothing is served on a failed settle.

finalized — asynchronous, never on the request path

Section titled “finalized — asynchronous, never on the request path”

A payment is finalized when the proof-gated mint to the real recipient (the treasury, or the FeeSplit contract) lands on-chain. This is driven by the facilitator’s finalize loop, off the request path:

  1. wait for the indexer to ingest the burn and for the transfer root to be proved on-chain (on the local stack, roughly one to two minutes),
  2. generate the Groth16 proof and call the official verifier (Verifier.singleTeleport on the default path),
  3. the mint lands — the payment transitions to finalized.

No client request ever waits for this. The provider has typically served the resource minutes before the mint exists. A payer that wants evidence of its own payment audits the burn transaction from the SettleResponse; the mint is deliberately not attributable to that burn by an on-chain observer (see Privacy: what SubEtha hides — and what stays public).

Between accepted and finalized the facilitator’s journal carries the payment as pending. Things that can happen in that gap, and what they mean:

  • pending_burn (gasless path only). If /settle answers while the facilitator’s broadcast burn is still unmined, the response is { success: false, errorReason: "pending_burn" }. Re-sending the same payload is idempotent and safe — the burn is never double-executed — but building a fresh payload for it risks paying twice.
  • Reorg invalidation. A receipt is not finality. Before minting, the finalize pass re-verifies the chain evidence; if the burn transaction has vanished and the burn address readably holds less than the amount, the entry transitions to failed with a reorg-invalidated reason instead of finalizing. On the local development stack reorgs do not occur in practice; the machinery is the safety floor for anything beyond it.
  • Facilitator restart. The journal restores pending entries on start and the finalize loop resumes them. An accepted payment does not need the payer or provider to do anything for finalization to complete.

None of these affect what was served: accepted already happened on its own evidence, and the resource decision is never revisited.

  • accepted and finalized are never interchanged, abbreviated to a shared “settled”, or presented as one state with a delay.
  • “Payment succeeded” on the request path always means accepted, nothing stronger.
  • The mint is never described as part of serving the resource.

The normative field-by-field wire format for both phases is the wire protocol reference; the conceptual walk-through of a whole payment is x402 v2 on SubEtha.