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.
A payment becomes finalized only after it was accepted and the asynchronous
proof-gated mint lands. Therefore finalized implies that the accepted phase
already occurred, but accepted does not imply that finalization has happened.
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 has a successful receipt 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). Receipt acceptance is the
request-path criterion: a successful receipt is the evidence accepted is
built on, while chain finality and reorg risk are checked separately before
finalization (see below). The ingest wait is likewise 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; no direct
on-chain record links the mint to that burn — the strength and limits of that
property are covered in
Privacy: what SubEtha hides — and what stays public.
Failure before acceptance
Section titled “Failure before acceptance”Not every settle attempt reaches accepted:
pending_burn(gasless path only). If/settleanswers while the facilitator’s broadcast burn is still unmined, the response is{ success: false, errorReason: "pending_burn" }— a settle failure beforeaccepted, so nothing is served. 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.
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:
- 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 verifiably 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 How SubEtha uses x402 v2.