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.
Why two phases exist at all
Section titled “Why two phases exist at all”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:
- the burn transaction has a successful receipt (so it is mined — the facilitator effectively waits about one block),
- the transaction’s
IndexedTransferevent pays at least the quoted amount to the burn address on the right token, and - 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:
- 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),
- generate the Groth16 proof and call the official verifier
(
Verifier.singleTeleporton the default path), - 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).
The gap between the two is a real state
Section titled “The gap between the two is a real state”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/settleanswers 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
failedwith areorg-invalidatedreason 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
acceptedpayment 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.
Rules this site holds itself to
Section titled “Rules this site holds itself to”acceptedandfinalizedare 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.