Skip to content

fix(x402): bind signed payment to the approved payment option - #1404

Open
SashaMIT wants to merge 1 commit into
coinbase:mainfrom
SashaMIT:fix/x402-bind-signed-payment-to-approved-option
Open

fix(x402): bind signed payment to the approved payment option#1404
SashaMIT wants to merge 1 commit into
coinbase:mainfrom
SashaMIT:fix/x402-bind-signed-payment-to-approved-option

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown

Summary

retry_http_request_with_x402 asks the user (or agent policy) to approve a specific payment option, validates that option against maxPaymentUsdc — and then signs whatever the server returns on the retry's 402, because wrapFetchWithPayment receives a plain x402Client with the default accepts[0] selector. The approved option is never enforced on the payload that actually gets signed.

A registered service can therefore present a cheap option at confirmation time and demand a larger amount — or a different recipient — when the retry happens. The maxPaymentUsdc check gives false assurance because it runs against the approved option's amount, not the signed one.

Fix

Pass a payment-requirements selector into the x402Client constructor (the hook the x402 SDK provides for exactly this):

  • Retry path (retry_http_request_with_x402): createApprovedPaymentSelector accepts only requirements matching the approved option on network/asset/payTo at an amount not exceeding the approved amount; anything else throws before any signature is produced.
  • Direct path (http_request_with_x402, which by design skips user confirmation): createCappedPaymentSelector refuses requirements above the configured maxPaymentUsdc, so the provider's own spending limit also binds what actually gets signed there.

Test plan

  • New utils.selector.test.ts: matching requirement selected; inflated amount refused; swapped recipient refused; wrong network refused; v2 price field handled; cap selector accepts-at/refuses-above the limit
  • Full x402 suite: 28/28 pass; tsc --noEmit clean; eslint/prettier clean

Made with Cursor

retry_http_request_with_x402 validates the user-approved payment option
against the spending limit, but then calls wrapFetchWithPayment with a
plain x402Client, which signs whatever payment requirements the server
returns on the retry's 402. A registered service could present a cheap
option at confirmation time and demand a larger amount (or different
recipient) at retry time.

Pass a payment-requirements selector into the x402Client:

- retry path: selector accepts only requirements matching the approved
  option on network/asset/payTo at an amount not exceeding the approved
  amount; anything else throws before signing.
- direct (no-confirmation) path: selector caps the signed amount at the
  configured maxPaymentUsdc, matching the limit the confirmation path
  already enforces.

Made-with: Cursor
@SashaMIT
SashaMIT requested a review from murrlincoln as a code owner August 6, 2026 15:53
@cb-heimdall

Copy link
Copy Markdown

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants