Skip to content

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

Open

Description

@hypeprinter007-stack

The x402 action provider gates payments on two axes today — host (isUrlAllowed) and amount (validatePaymentLimit) — but nothing lets an operator inspect the payment recipient wallet (payTo) before wrapFetchWithPayment signs in retryWithX402.

The gap. "Is the address my agent is about to send USDC to sanctioned / a known drainer / phishing?" is a distinct axis from host-allowlist and amount-cap, and arguably the one an autonomous payer most wants closed before signing. The selected requirements already carry payTo (it is on PaymentRequirements), so the data is in hand — there is just no decision point exposed to config.

Precedent. The official @x402 core client already exposes onBeforePaymentCreation(context) returning { abort, reason }, with context.selectedRequirements.payTo available. AgentKit wraps @x402/fetch but does not surface an equivalent pre-pay decision point to x402ActionProvider config.

Proposal. A neutral, optional config hook — e.g.:

beforePayment?: (req: { payTo: string; network: string; scheme: string; amountUsdc: number; url: string })
  => { allow: boolean; reason?: string } | Promise<{ allow: boolean; reason?: string }>;

evaluated in retryWithX402 right after validatePaymentLimit and before wrapFetchWithPayment — same shape as the existing isUrlAllowed / validatePaymentLimit gates. It just hands the operator the selected requirements and lets them allow/deny. No vendor coupling — the hook is provider-agnostic.

Use case / one implementation. Recipient sanctions + address-reputation screening before an autonomous send. anchor-x402-safe-pay (npm/PyPI) is one drop-in that returns an allow/review/block verdict for a payTo, but the hook itself is generic.

Happy to send the PR (config field + hook eval in retryWithX402 + a test, mirroring validatePaymentLimit) if you are open to the addition — proposing first since it is a new public config surface.

Activity

  1. OceanAlt666 commented on Aug 17, 2026

    @OceanAlt666

    +1 — a neutral pre-payment payTo hook is the natural seat for compliance/AML screening on the buyer side, and the x402 client already ships the abortable onBeforePaymentCreation precedent, so this fits cleanly with existing patterns.

    One design note: let the hook express three outcomes, not two. A compliance screen that can't evaluate (upstream down, unknown address) should be able to return UNCERTAIN and let the integrator choose fail-open vs fail-closed, instead of being forced into a false allow. And a hard deny should require strong, verifiable evidence (sanctions / mixer / issuer-freeze), not a soft score crossing a threshold — otherwise the hook and a plain fail-closed default become indistinguishable.

  2. hypeprinter007-stack commented on Aug 17, 2026

    @hypeprinter007-stack
    Author

    Strong +1 on the three-state contract — from practice, not theory. A recipient screen that can't evaluate (provider down, unknown address, malformed response) has to be able to say UNCERTAIN rather than get coerced into allow — that's precisely how a "fail-closed" guard silently fails open. I shipped a fix to my own screen wrapper for exactly that bug last week, so I'll back this one from experience.

    One refinement on the states: "couldn't evaluate" and "evaluated, but elevated risk" are both non-allow but they're different signals, and the integrator should choose the policy for each. In practice that's two knobs: the verdict (allow / review / block) and the on-error behavior (fail-open vs fail-closed on UNCERTAIN). Collapsing them loses the case a lot of teams actually want — hold on review, but let me decide what happens when the screen itself is down.

    Two things I'd anchor the design on:

    1. Keep the hook provider-neutral. Its value is that it's a seat, not a vendor. If the callback carries { decision, evidence } and nothing provider-specific, any backend drops in — Ocean's adapters, my anchor-x402 /v1/screen (already returns allow/review/block + per-signal evidence: OFAC + address-reputation), or a team's in-house list. Multiple independent implementations already returning this shape is a good sign it's the right abstraction.
    2. A hard block should require verifiable evidence (sanctions / mixer / issuer-freeze), with softer scores mapping to review/uncertain — otherwise the hook collapses into a plain fail-closed default and adds nothing over the existing onBeforePaymentCreation abort.
  3. hypeprinter007-stack commented on Aug 18, 2026

    @hypeprinter007-stack
    Author

    Went ahead and pre-built this so there's a concrete diff to react to rather than an open question: #1454.

    It adds an optional, provider-neutral beforePayment hook to retry_http_request_with_x402 — runs after amount + network validation and immediately before signing/settlement, receives the payment context (incl. payTo), and { abort: true, reason } refuses the payment without paying. No screening backend is imported; any implementation plugs in. Matches the three-state-friendly shape discussed above (decision + reason, agnostic to how the verdict was reached).

    Backward compatible (no behavior change unless the hook is set), +2 tests mirroring the existing retry tests. Scoped to the two-step flow, which exposes payTo pre-payment; the one-shot make_http_request_with_x402 auto-settles inside wrapFetchWithPayment and can't expose the recipient before paying, so it's intentionally not gated (noted in the PR). Happy to add Python parity there if you want it in the same PR.

    Would appreciate a read on whether this is the shape you'd merge.

  4. twzrd-sol commented on Sep 12, 2026

    @twzrd-sol

    Named implementation note for the optional beforePayment seat this issue proposes (after amount/network validation, before wrapFetchWithPayment signs).

    If a Solana x402 client already exposes beforePayment / onBeforePaymentCreation, the drop-in wash-refuse is:

    npm i twzrd-x402-gate@0.9.5 x402-solana@3.0.0
    import { createX402Client } from \"x402-solana\";
    import { createTwzrdBeforePaymentHook } from \"twzrd-x402-gate\";
    
    const client = createX402Client({
      wallet,
      network: \"solana\",
      beforePayment: createTwzrdBeforePaymentHook({ refuseWashFlagged: true }),
    });

    That hook is after requirement select, before sign. { abort: true, reason } on wash; signer_invocation_count stays 0. No vendor lock in AgentKit — same shape as the generic hook in #1454 / this issue. Live pin is npm view twzrd-x402-gate version (0.9.5), not a frozen comment.

    Not asking Coinbase to vendor this. One implementation of the recipient axis you already named.

  5. hypeprinter007-stack commented on Sep 12, 2026

    @hypeprinter007-stack
    Author

    Exactly the intent — the value is a provider-neutral seat, not any one vendor's screen.

    #1454 wires this shape into retryWithX402 (after amount/network validation, before wrapFetchWithPayment signs), returning { abort, reason } with the signer never invoked on refuse — and shows anchor-x402-safe-pay's screenAllows (sanctions + address-reputation axis) as one reference implementation. twzrd-x402-gate covering the wash axis is another. That's the point: multiple independent implementations registering on a single neutral hook is precisely the argument for landing the seat in AgentKit rather than any provider re-implementing amount/host gates from scratch.

  6. OceanAlt666 commented on Sep 15, 2026

    @OceanAlt666

    Another implementation of this pattern, for reference. It targets the same stage as #1454 — after amount and network validation, immediately before the signer runs — and is provider-neutral: it does not require AgentKit to depend on any particular screening provider.

    One additional failure mode may be worth considering alongside the recipient axis: the gap between the payment requirements the seller published and the ones the agent ends up paying against.

    If the seller signs its payment requirements (for example, Ed25519 over a canonical representation of the resource plus the accepts[] entries), and the seller's verification key is already authenticated or bound to that origin, the hook can verify that signature offline before any recipient screening happens.

    A payTo substituted between the seller and the agent would then fail signature verification rather than simply being screened as a different recipient. That distinction matters because a newly introduced address may have little or no adverse history yet, so recipient-reputation screening alone may not detect the substitution.

    A few implementation details may also be useful input:

    • Three outcomes (allow / review / decline) instead of a boolean. A boolean cannot distinguish "screened and clean" from "could not screen", and an autonomous payer needs to treat those differently.
    • Machine-readable reason codes on every path, including failure paths, so the caller can branch on why rather than parse prose.
    • In this implementation, undecodable or unavailable states never resolve to allow.

    The implementation:

    npm i @oceanalt/x402 @oceanalt/core
    import { OceanAltClient } from "@oceanalt/core";
    import { wrapFetch } from "@oceanalt/x402";
    
    const client = new OceanAltClient();
    const fetchGuarded = wrapFetch(fetch, {
      client,
      pay: (pr) => yourPayer.headersFor(pr),
    });

    The verdict envelope these packages emit — the three outcomes and reason-code set — is documented at https://oceanalt.com/en/rap. That is our own framework, not an x402 or AgentKit standard; the link is included only to make the field semantics explicit.

  7. hypeprinter007-stack commented on Sep 15, 2026

    @hypeprinter007-stack
    Author

    This is a good addition — the requirements-integrity axis is genuinely distinct from recipient reputation, and you're right that reputation alone can't catch a payTo substituted in transit (a fresh address has no history to screen against). Signature-verifying the seller's published requirements before screening closes that gap.

    The nice part is both checks ride the same insertion point #1454 proposes: after amount/network validation, before the signer runs. The hook is provider-neutral by design — it can carry a signature check, a screening verdict, or both, without AgentKit depending on any one framework. That's the layer I'd like to see land in core; the verdict logic (yours, ours, anyone's) stays pluggable above it.

    +1 on the three-outcome envelope and machine-readable reason codes, with undecodable/unavailable never resolving to allow — that's the same contract our /v1/screen returns (allow/review/block, fail-closed), and an autonomous payer really does need to distinguish "screened clean" from "couldn't screen."

  8. OceanAlt666 commented on Sep 16, 2026

    @OceanAlt666

    Agreed — the seat in core with the verdict logic pluggable above it is the right boundary, and both axes riding the same insertion point is the cleanest framing of it.

    Two implementation notes from building against this pattern, offered as input to #1454 rather than as requirements.

    1. Verify the requirements from the source they actually travel in.

    In x402 v2, PaymentRequired is carried base64-encoded in the PAYMENT-REQUIRED response header. The response body is not the canonical source of the payment requirements and may be empty.

    Our first implementation signed a canonical form derived from the body. It passed its own tests and broke on the first live run against a real endpoint, because we were authenticating a representation that carried nothing.

    So the signed material should be derived from the PaymentRequired object actually received through that header. Whether an implementation signs the wire representation or a defined canonical subset of the decoded object can stay implementation-specific: a signer over raw bytes needs access to the raw header, while one signing a canonical field set does not.

    Core does not need to prescribe either — only to leave the wire form reachable for implementations that need it.

    2. One runtime boundary that may be worth making explicit.

    A provider can expose richer outcomes such as allow / review / decline, but an adapter ultimately has to map those into the hook contract. If that mapping returns an object AgentKit does not recognize, the current execution path only checks decision.abort; an unrecognized non-empty shape therefore falls through and payment proceeds.

    We ran into this from the implementation side: our screening interface emits a three-outcome verdict, so the translation into the hook's execution contract is ours to get right — which is also why the boundary is visible to us.

    The invariant worth protecting at the execution boundary seems narrow: an unrecognized return shape should not silently become permission to sign.

    The reason to guard it there, rather than relying on every adapter to get the mapping right, is that the failure is silent and fails in the direction of paying. The two tests currently in #1454 cover { abort: true } and the normal allow path, but not an unrecognized non-empty return shape.

    Concretely this could stay small: if the hook returns a non-empty value that does not contain a recognized abort decision, return a structured hook-contract error and do not invoke the signer. A test for that case alongside the two already in the PR would make the invariant explicit.

    There is a similar information-loss issue with reasons: the current hook exposes a human-readable reason?: string, while providers may already have machine-readable reason codes. Preserving an optional code across the adapter boundary would let autonomous callers branch without parsing prose.

    None of this asks core to interpret a provider's verdict logic — that stays pluggable above the hook, as you described. Happy for our implementation to sit behind that interface rather than be part of it.

  9. hypeprinter007-stack commented on Sep 16, 2026

    @hypeprinter007-stack
    Author

    Both land, and both improve #1454 — thanks for bringing implementation scars rather than opinions.

    1. Sign from the header, not the body. Confirmed from the seller side: our v2 402 carries the full PaymentRequired base64 in the PAYMENT-REQUIRED response header, and the body is a separate, often-empty thing — an integrity check derived from the body authenticates nothing, exactly as you hit. So the hook context needs to expose the decoded PaymentRequired object (and the raw wire header, for a bytes-signer) as the canonical source. Agreed core shouldn't prescribe raw-bytes vs canonical-subset — only leave the wire form reachable. I'll make the context surface that, not just the parsed body.

    2. An unrecognized return shape must not become permission to sign. Strong +1, and I've hit this exact failure mode: a buyer-side screening wrapper of ours had a fail-open bug where an unknown/missing verdict wasn't in the block set, so it resolved to allow — the fix was to validate the decision is a recognized shape before treating "no abort" as permission, and fail closed on anything else. Same invariant here: if the hook returns a non-empty value without a recognized abort decision, that's a hook-contract error → don't invoke the signer. I'll add that guard plus the third test (unrecognized non-empty return ⇒ no sign) alongside the existing {abort:true} and allow cases.

    On reasons: agreed that reason?: string drops machine-readable codes. I'll widen the contract to carry an optional code through the boundary so autonomous callers can branch without parsing prose — provider verdict logic still stays pluggable above the hook.

    Appreciate you keeping your impl behind the interface; that's the right boundary.

  10. hypeprinter007-stack commented on Sep 16, 2026

    @hypeprinter007-stack
    Author

    Pushed to #1454 (92af548). All three landed:

    • Fail-closed: a non-empty beforePayment return without a boolean abort is now a hook-contract error — the signer is not invoked. Unrecognized shapes can't fall through to paying anymore.
    • code passthrough: the decision carries an optional machine-readable code alongside reason, surfaced in the action result.
    • Requirements reachable: make_http_request now returns the decoded paymentRequirements and the raw paymentRequirementsHeader (v2's authenticated source), and retry_http_request_with_x402 threads both into the hook context — so an integrity check can verify the requirements, not just the recipient.

    Tests cover unrecognized-shape ⇒ no sign and the code passthrough (25 pass); README documents the contract. Verdict logic stays pluggable above the hook, as discussed — appreciate the review.

  11. TheAliphant commented on Sep 26, 2026

    @TheAliphant

    MACH Base is live x402 infrastructure for Base-chain reads with no signup or API key. We are trying to verify the cleanest caller-pays integration path for your agent/runtime. A live compatibility call is GET https://api.mach.gallery/api/base/block-number ($0.005 USDC on Base); discovery: https://api.mach.gallery/.well-known/x402 ; OpenAPI: https://api.mach.gallery/openapi.json . If this fits your stack, one real paid call is enough to validate it. If not, a short note on the blocking integration/policy is useful. MACH conversation=99bd1fe67a59cac772fc7011.

  12. nwilliams005 commented on Oct 31, 2026

    @nwilliams005

    We shipped this as a standalone preflight (ok | caution | avoid plus payTo history) and opened a provider PR that hooks the same spot before wrapFetchWithPayment signs: #1613. Happy to align on a neutral beforePayment shape. Disclosure: x402check is my tool.

  13. hypeprinter007-stack commented on Nov 1, 2026

    @hypeprinter007-stack
    Author

    Thanks for the note. Happy to align, since a shared shape is the point of #1402.

    The neutral piece is #1454: an optional beforePayment on X402Config in the existing x402 action provider. It runs after amount/network validation and just before signing, receives the selected option's payTo (plus the payment requirements), and returning { abort: true, reason } refuses the payment. It has no vendor dependency, so any screener can plug in, including x402check.

    Given that, the cleanest alignment might be for the beforePayment helper in #1613 to return the X402BeforePaymentDecision shape from #1454. Then x402check works as a drop-in hook with no second surface. Could you also drop "Closes #1402 #1454" from #1613? A vendor action provider is a useful addition, but it doesn't implement the neutral hook itself, and maintainers may want to review the two separately.

    Disclosure: I run anchor-x402, which also offers recipient screening.

  14. added a commit that references this issue on Nov 1, 2026
  15. nwilliams005 commented on Nov 2, 2026

    @nwilliams005

    Thanks — aligned.

    #1613's createX402checkBeforePayment / create_x402check_before_payment now returns the exact X402BeforePaymentDecision shape from #1454 ({ abort, reason?, code? }), so it drops into beforePayment as a provider-neutral hook. The Closes #1402 #1454 line is removed (now Related: #1402, #1454); happy for maintainers to review the two PRs separately.

    Standalone preflight_check / verify_payto actions stay for callers who want an explicit tool. Types are defined locally until #1454 merges (no unmerged imports).

    Disclosure: I maintain x402check (endpoint + history preflight: live 402 price/network/payTo vs that URL's history, including payTo-change detection).

  16. hypeprinter007-stack commented on Nov 2, 2026

    @hypeprinter007-stack
    Author

    Thanks, that's exactly the alignment I was hoping for. With x402check returning the X402BeforePaymentDecision shape, #1454 now has two independent screeners ready to plug into the same beforePayment hook: x402check, and anchor-x402-safe-pay as in its README example.

  17. OceanAlt666 commented on Nov 4, 2026

    @OceanAlt666

    One more implementation for the proposed beforePayment hook in #1454: @oceanalt/agentkit-hook (v0.1.0) returns its X402BeforePaymentDecision shape, undefined to allow or { abort: true, reason, code } to abort. In the default block mode, decline and review abort the payment, and if client.decide() rejects, the hook treats that as review and aborts as well.

    Disclosure: we maintain OceanAlt. Package: https://www.npmjs.com/package/@oceanalt/agentkit-hook

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions