§6.1 (merged 2026-08-08, #3053) makes `extra.paymentFlow` and
`extra.assetTransferMethod` protocol-reserved: "clients and servers MUST
interpret them as defined here rather than as opaque scheme-private fields."
compat/x402.ts contained zero occurrences of either.
Three flows are defined, and two of them settle before the resource runs:
authorization verify → resource → settle → respond (default)
upfront settle → resource → respond (omits /verify)
escrow settle → resource → settle → respond (omits /verify)
s402's pipeline is verify → resource → settle — it IS `authorization`. Lifting
an `upfront` or `escrow` requirement and paying it under that ordering serves
the resource before payment is durably committed.
Latent today: compat accepts only `exact`, and all eleven upstream `exact`
mechanisms declare `authorization`. It goes live the first time one declares
otherwise. SVM `upto` already declares `escrow`, so the protection is the
scheme gate, not a decision.
Inbound `fromX402Requirements` and outbound `toX402V2Requirements` now both
call one gate. `toX402V2Envelope` delegates rather than building its own
literal, so the single outbound fix covers both emit paths — there is a test
that goes red if someone inlines it.
Two corrections to the ticket's premises, both verified before building:
1. `extra` was not "carried as an opaque Record" on the inbound path — the
inbound interface had no `extra` field at all, so it was dropped on the
floor and a gate written to inspect it would have had nothing to read. The
field is added here; that is the precondition for the check, not a
separate feature.
2. The ticket asks to reject an unsupported `assetTransferMethod`. §6.1 says
the opposite is possible: "Allowed assetTransferMethod string values are
mechanism-defined; this protocol reserves the key name, not a global ATM
vocabulary." There is no set to validate against, and enumerating one would
reject conformant peers using any mechanism not hard-coded. ATM is carried
and not adjudicated, with a test asserting an unseen ATM passes. The real
§6.1 obligation is rejecting unsupported ATM/flow COMBINATIONS, which needs
the mechanism's flow-per-ATM table; compat has none.
Absence resolves to `authorization`, and that is safe rather than optimistic:
§6.1 requires the key be present whenever the resolved flow is not
`authorization`, so an absent key proves the flow from a conformant peer.
Unrecognised and unsupported are separate errors on purpose. The operator's
next move differs — an undefined flow means upstream moved and drift detection
missed it; a defined-but-unsupported one is a deliberate s402 limit.
Verification:
cd typescript && pnpm vitest run test/compat-x402-payment-flow.test.ts
19 tests, observed red against main first (10 failed / 9 passed).
Full suite 1121 passed (29 files) · tsc --noEmit clean · build clean.
Spec text verified at x402-foundation/x402 foundation/main @ 167a828e.
Note: a local x402 checkout may sit on a fork branch predating §6.1 — a naive
grep finds nothing there.
Refs: DAN-846
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gap
x402 merged §6.1 Asset Transfer Methods and Payment Flow Models on 2026-08-08 (#3053). It makes two
extrakeys protocol-reserved:compat/x402.tswas last touched three weeks before that landed and contained zero occurrences of either.Three flows are defined, and two settle before the resource executes:
authorization(default)upfront/verify)escrow/verify)s402's pipeline is verify → resource → settle — it is
authorization. Lifting anupfrontorescrowrequirement and paying it under that ordering serves the resource before payment is durably committed. That is a serve-without-finality, not a cosmetic conformance gap.Latent today, with a named trigger. Compat accepts only
exact, and all eleven upstreamexactmechanisms declareauthorization. It goes live the first time one declares otherwise — and SVMuptoalready declaresescrow, so what protects us is the scheme gate, not a decision.What changed
One gate, called from both paths.
toX402V2Envelopedelegates totoX402V2Requirementsrather than building its own literal, so the single outbound fix covers both emit paths — there's a test that goes red if someone inlines it.The supported set is declared, not inherited by silence, and carries an invariant: it may only grow when the pipeline actually gains the corresponding ordering. Adding a flow without implementing its ordering removes the gate rather than widening it.
Unrecognised and unsupported are separate errors on purpose. The operator's next move differs — an undefined flow means upstream moved and drift detection missed it; a defined-but-unsupported one is a deliberate s402 limit. One message for both sends half of readers to the wrong place.
Two corrections to the ticket's premises
Both verified before building, because the fix depended on them.
1.
extrawas not "carried opaquely" inbound — it did not exist. The ticket and the originating report both describe the inbound path as carryingextraas an opaqueRecord<string, unknown>.x402PaymentRequirementshad noextrafield at all; it appears only onx402V2PaymentRequirements, the emit type. So the reserved keys were dropped on the floor, and criterion 1's check would have had nothing to read. Adding the field is the precondition for the gate, not a separate feature.2.
assetTransferMethodcannot be validated, and validating it would be the bug. Criterion 1 asks to reject an unsupported ATM. §6.1 forecloses it:There is no set to check against. Enumerating one would reject conformant counterparties using any mechanism not hard-coded —
eip3009,permit2,sequence,ticketSequence, and whatever ships next. ATM is carried and not adjudicated, with a test asserting an unseen ATM passes. The real §6.1 obligation is rejecting unsupported ATM/flow combinations, which requires the mechanism's declared flow-per-ATM table; compat has none, and inventing a vocabulary would be a bug wearing a check's clothes.Why treating absence as
authorizationis safe rather than optimistic§6.1 requires the key be present whenever the resolved flow is not
authorization:So an absent key proves the flow for a conformant peer — the default is load-bearing upstream, not an assumption we're making locally. Every pre-§6.1 counterparty keeps working unchanged.
Verification
mainbefore the fix (10 failed / 9 passed).tsc --noEmitclean ·pnpm buildclean.foundation/main@167a828e, quoted inline in the test file.paymentFlowthere finds nothing, which reads exactly like "the spec doesn't say this." Readfoundation/main.Deliberately not in this PR
Multi-settle support (§7.2 permits
/settlemore than once).escrowsettles a deposit before the resource and the charge after. That is real work behind a real design question — whether s402 wantsescrowat all — and it must not ride inside a conformance fix. This PR makes the answer "not yet, loudly" instead of "silently, wrongly."Refs: DAN-846
🤖 Generated with Claude Code