Skip to content

feat(x402): optional beforePayment hook to screen the recipient before paying - #1454

Open
hypeprinter007-stack wants to merge 1 commit into
coinbase:mainfrom
hypeprinter007-stack:x402-before-payment-hook
Open

feat(x402): optional beforePayment hook to screen the recipient before paying#1454
hypeprinter007-stack wants to merge 1 commit into
coinbase:mainfrom
hypeprinter007-stack:x402-before-payment-hook

Conversation

@hypeprinter007-stack

Copy link
Copy Markdown

What & why

Implements the neutral pre-payment hook proposed in #1402.

Today the x402 action provider gates where the agent can pay (registeredServices) and how much (maxPaymentUsdc), but nothing checks who the recipient is. A compromised or mistaken agent can pay a sanctioned / drainer / phishing address that passes both existing gates. This adds the missing seat: an optional recipient check at the pre-payment chokepoint.

The change

A new optional beforePayment config hook:

const config: X402Config = {
  registeredServices: ["https://api.example.com"],
  beforePayment: async ({ payTo, url, network, asset, amount, method }) => {
    if (payTo && (await isSanctioned(payTo))) return { abort: true, reason: "recipient flagged" };
  },
};
  • Runs in retry_http_request_with_x402 after amount + network validation and immediately before any signing/settlement.
  • Receives the payment context including the selected option's payTo.
  • Returning { abort: true, reason } refuses the payment — no signature, no settlement; the action returns a structured error.
  • Provider-neutral by design: no screening backend is imported. Any implementation plugs in (sanctions / reputation / allowlist). The README shows anchor-x402-safe-pay's allow/review/block verdict as one example.

This matches the design discussion on #1402 (three-state-friendly: the hook returns a decision + reason and is agnostic to how the verdict was reached).

Scope & compatibility

  • Backward compatible — no behavior change unless beforePayment is set.
  • Covers the recommended two-step flow (make_http_requestretry_http_request_with_x402), which exposes payTo before paying. The one-shot make_http_request_with_x402 auto-settles a 402 inside wrapFetchWithPayment and does not expose the recipient pre-settlement, so it is intentionally not gated (documented in the README). Gating it would need a larger refactor of that path.

Tests

Two cases added mirroring the existing retryWithX402 tests:

  • beforePayment returns { abort: true } → hook sees the payTo/context, wrapFetchWithPayment is never called, action returns the abort error.
  • beforePayment allows → payment proceeds normally.

Notes for reviewers

  • Files: schemas.ts (config field + X402BeforePaymentContext / X402BeforePaymentDecision types), x402ActionProvider.ts (resolve + call site), x402ActionProvider.test.ts (2 tests), README.md (docs + example).
  • I wasn't able to run the full TypeScript monorepo suite in my environment; the added tests mirror the existing retry tests exactly and the types are consistent by construction — please let CI validate.
  • Happy to add Python parity in the same PR (or a follow-up) if you'd like it here — kept this TS-first to get a read on the shape before duplicating.

Closes #1402 (or partially addresses, if you'd prefer to keep it open for the Python side).

…e paying

Implements the neutral pre-payment hook proposed in coinbase#1402. Today the x402 action
provider gates *where* the agent pays (registeredServices) and *how much*
(maxPaymentUsdc), but nothing checks *who* the recipient is — a compromised or
mistaken agent can pay a sanctioned/drainer address.

Adds an optional `beforePayment` config hook that runs in retry_http_request_with_x402
after amount + network validation and immediately before the payment is signed/
settled, receiving the payment context (incl. the selected option's `payTo`).
Returning { abort: true, reason } refuses the payment — no signature, no settlement.

Deliberately provider-neutral: no screening backend is imported. Any implementation
plugs in (sanctions / reputation / allowlist); the README shows anchor-x402-safe-pay's
allow/review/block verdict as one example.

Scope: the hook covers the recommended two-step flow (make_http_request ->
retry_http_request_with_x402), which exposes payTo before paying. The one-shot
make_http_request_with_x402 auto-settles a 402 inside wrapFetchWithPayment and does
not expose the recipient pre-settlement, so it is intentionally not gated (noted in
the README). Fully backward compatible — no behavior change unless beforePayment is set.

Tests: added two cases mirroring the existing retry tests — abort (no payment signed)
and allow (payment proceeds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@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

@github-actions github-actions Bot added documentation Improvements or additions to documentation action provider New action provider typescript labels Aug 18, 2026
hypeprinter007-stack added a commit to hypeprinter007-stack/anchor-x402-safe-pay that referenced this pull request Aug 22, 2026
…payment hook

Adds an optional, stateless per-send amount cap that composes with the recipient
verdict at the pre-payment hook (JS composeCapWithScreen / Py compose_cap_with_screen).

Why the hook, not guardedSend: with x402 the amount is payee-set in the 402
challenge, so a cap checked when the agent plans a call can be bypassed by a
challenge-time bump (TOCTOU). guardedSend wraps an opaque thunk that pays
internally and never sees the amount; the hook seat carries the real payTo +
amount at send time (matches coinbase/agentkit#1454's beforePayment context), so
the bound is evaluated against the amount that will actually be paid.

Fail-closed: flagged recipient OR over-cap amount OR screen failure all abort;
verdict wins; maxAmount is inclusive (paying exactly the cap is allowed).
Stateless by design — cumulative/rate budgets stay in the wallet/agent layer.
Structured reasons: flagged_recipient | exceeds_cap.

Test path contributed by @Sergio87Felix (#2); implemented to that contract with
the boundary corrected to inclusive-max. +7 JS, +7 Python regression cases; README
+ llms.txt document the helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action provider New action provider documentation Improvements or additions to documentation typescript

Development

Successfully merging this pull request may close these issues.

Proposal: a neutral pre-payment recipient hook in the x402 action provider (inspect payTo before paying)

2 participants