Skip to content

fix(DAN-846): honour x402 §6.1 payment-flow keys on both compat paths - #4

Open
Danny-Devs wants to merge 1 commit into
mainfrom
et3rnald/dan-846-x402-payment-flow
Open

fix(DAN-846): honour x402 §6.1 payment-flow keys on both compat paths#4
Danny-Devs wants to merge 1 commit into
mainfrom
et3rnald/dan-846-x402-payment-flow

Conversation

@Danny-Devs

Copy link
Copy Markdown
Contributor

The gap

x402 merged §6.1 Asset Transfer Methods and Payment Flow Models on 2026-08-08 (#3053). It makes two extra keys protocol-reserved:

extra.assetTransferMethod and extra.paymentFlow are protocol-reserved keys in PaymentRequirements.extra: clients and servers MUST interpret them as defined here rather than as opaque scheme-private fields.

compat/x402.ts was last touched three weeks before that landed and contained zero occurrences of either.

Three flows are defined, and two settle before the resource executes:

Flow Ordering
authorization (default) verify → resource → settle → respond
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. That is a serve-without-finality, not a cosmetic conformance gap.

Latent today, with a named trigger. Compat accepts only exact, and all eleven upstream exact mechanisms declare authorization. It goes live the first time one declares otherwise — and SVM upto already declares escrow, so what protects us is the scheme gate, not a decision.

What changed

One gate, called from both paths. toX402V2Envelope delegates to toX402V2Requirements rather 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. extra was not "carried opaquely" inbound — it did not exist. The ticket and the originating report both describe the inbound path as carrying extra as an opaque Record<string, unknown>. x402PaymentRequirements had no extra field at all; it appears only on x402V2PaymentRequirements, 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. assetTransferMethod cannot be validated, and validating it would be the bug. Criterion 1 asks to reject an unsupported ATM. §6.1 forecloses it:

Allowed assetTransferMethod string values are mechanism-defined; this protocol reserves the key name, not a global ATM vocabulary.

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 authorization is safe rather than optimistic

§6.1 requires the key be present whenever the resolved flow is not authorization:

When the resolved payment flow is not authorization, PaymentRequired accepts[].extra.paymentFlow MUST be present… authorization MAY be omitted or explicit.

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

cd typescript && pnpm vitest run test/compat-x402-payment-flow.test.ts
  • 19 tests, observed red against main before the fix (10 failed / 9 passed).
  • Full suite 1121 passed across 29 files · tsc --noEmit clean · pnpm build clean.
  • Spec text verified at x402-foundation/x402 foundation/main @ 167a828e, quoted inline in the test file.

⚠️ A local x402 checkout may sit on a fork branch that predates §6.1 — mine did, and a naive grep for paymentFlow there finds nothing, which reads exactly like "the spec doesn't say this." Read foundation/main.

Deliberately not in this PR

Multi-settle support (§7.2 permits /settle more than once). escrow settles a deposit before the resource and the charge after. That is real work behind a real design question — whether s402 wants escrow at 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

§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>
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
s402-docs Ready Ready Preview Aug 16, 2026 12:37am

@Danny-Devs

Copy link
Copy Markdown
Contributor Author

⚠️ CI red here is inherited from main, not caused by this PR — DAN-860

Both open PRs fail identically: test_conformance.py unlock decode, S402Error: unlock payload requires encryptionId (string), got NoneType. Neither PR touches unlock or encryptionId, and they have nothing to do with each other.

main @ 7d50fa9 on a clean tree, with none of this branch present:

cd python && uv run --extra dev pytest -v  →  2 failed, 152 passed

main has been red since 2026-07-21 (feat(unlock): single-transaction pay-to-decrypt scheme). Nothing has merged since, so nothing caught it.

The underlying defect is larger than a failing test: the unlock scheme's Python and TypeScript field sets are disjointencryptionId/encryptedContentId/encryptionServiceId versus packageId/keyServers/threshold/contentDigest, with an empty intersection. A design change reached the conformance vectors and the TS runtime validator but not the Python implementation, docs/specification.md, or TS's own types.ts. Diagnosis and the decision it needs: DAN-860.

Deliberately not fixed inside this PR — a red-main repair riding in a feature branch is unreviewable, and it would mean neither PR's green could be trusted as evidence about itself.

This PR's own suites are green: pnpm typecheck clean, pnpm vitest run full pass, pnpm build clean, and its new tests were observed red against main before the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant