Originally created by: SergeevDmitry
A facilitator can accept a settlement and then lose the response (timeout, connection reset, proxy 502) before we ever see a transaction hash. "No hash" does not mean "no transfer".
Today the pipeline treats a settlement as uncertain only when a hash is present. settle() throws PAYMENT_PROVIDER_UNAVAILABLE without one, uncertainSettlementTxHash() returns nothing for that case, and the pipeline records the attempt as failed and releases the AP2 hold. Payment replay protection still holds, but the released mandate can be presented again with a different payment authorization, which is exactly what the hold prevents. On the one rail we ship today, it also breaks a rule [#6] already states: provider outages must stay distinguishable from rejected payments.
This is a deliberate decision, not an oversight: pipeline-authorization.test.ts (github.com) asserts "releases the reservation when settlement throws without moving funds". The premise that a throw without a hash means no funds moved is what this issue disputes, so that test flips. The docs/security.md matrix already calls a facilitator timeout uncertain without qualifying it by hash, so the documentation describes the behaviour this issue asks for.
Version: 1.4.0, commit a05a08f.
Steps to reproduce (planned): point x402 at a stub facilitator that answers /verify normally; make its /settle perform the transfer and then close the socket without a body; invoke a paid resource with AP2 enabled and a valid mandate; inspect the payment attempt and the AP2 replay store.
Expected: attempt unresolved, AP2 hold retained, backend not executed, buyer told "outcome unknown, do not pay again".
Actual (from source): attempt failed, hold released, retryable provider error returned.
Done when a settlement whose result never arrives is recorded as unresolved, not failed, and the purchase authority behind it cannot be reused until someone proves the outcome.
Does the facilitator SDK expose enough to classify a thrown error as pre- or post-transfer, or must every transport error after /settle was sent count as uncertain?
Originally posted by: Revinand
No, the SDK cannot separate them; its own binding comment says it throws a bare Error for a 401 and a
FacilitatorResponseErrorfor a bad body. So a verdict is only ever a returned value, every throw is uncertain, and the gateway over-retains.failedis now written by nothing; it stays in the union because existing databases hold rows under the old reading.Ticket changed by: SergeevDmitry