Skip to content

feat(x402): generic beforePayment pre-sign hook (vendor-neutral)#205

Open
twzrd-sol wants to merge 1 commit into
BlockRunAI:mainfrom
twzrd-sol:feat/generic-before-payment-hook
Open

feat(x402): generic beforePayment pre-sign hook (vendor-neutral)#205
twzrd-sol wants to merge 1 commit into
BlockRunAI:mainfrom
twzrd-sol:feat/generic-before-payment-hook

Conversation

@twzrd-sol

@twzrd-sol twzrd-sol commented Jul 13, 2026

Copy link
Copy Markdown

What

Adds a generic beforePayment lifecycle hook to startProxy.

Fires after ClawRouter selects a payment requirement, before the transaction is built and signed — the last deterministic checkpoint where a payment can be refused without a wallet signature.

const handle = await startProxy({
  wallet: "0x...",
  beforePayment: async ({ selectedRequirements, resourceUrl }) => {
    if (!ALLOWED.has(selectedRequirements.payTo)) {
      return { abort: true, reason: "payTo not on allowlist" };
    }
    // return void → proceed to signing
  },
});

Design

  • No vendor dependency. The hook is a plain callback — the caller wires whatever policy they own (allowlist, spend cap, trust preflight, compliance check). No new packages, no new env vars.
  • Official x402 lifecycle. Uses x402.onBeforePaymentCreation, the same hook BlockRun's own builder-code stamp and chain logging already use.
  • Return { abort: true, reason } to refuse — wallet never signs, payment never reaches the merchant.
  • Return void / { abort: false } to proceed unchanged.
  • Async-safe — the hook may be async (e.g. to call an external policy service).
  • Fail-closed — an unhandled throw inside the hook aborts the payment.

Relationship to the withdrawn #204

#204 asked ClawRouter to add a branded peerDep (twzrd-x402-gate) and TWZRD-named env vars. This PR replaces that with a generic, no-dependency seam. The same pre-sign outcome, but the policy is the caller's responsibility — not a specific package.

Changes (+65 lines, 1 file)

  • src/proxy.ts: ProxyOptions.beforePayment type + onBeforePaymentCreation registration in startProxy

No test file yet — if the shape is right, I'll add one.

🤖 Generated with Hermes Agent

Summary by CodeRabbit

  • New Features
    • Added an optional pre-payment approval hook for proxy integrations.
    • Payment requests can now be reviewed and approved or refused before transactions are created and signed.
    • Refused payments return a configurable reason or a default refusal message.

Adds an optional `beforePayment` callback to ProxyOptions. Fires after
requirement selection, before transaction build + signing — the last
deterministic checkpoint at which a payment can be refused.

- No vendor dependency. Wire any policy you own: allowlist, spend cap,
  trust preflight, compliance check.
- Return `{ abort: true, reason }` to refuse — wallet never signs.
- Return `void` or `{ abort: false }` to proceed.
- Async-safe (can call external policy services).
- Fail-closed: unhandled throw aborts the payment.

Uses the same official x402 lifecycle hook (`onBeforePaymentCreation`)
that BlockRun's own builder-code stamp and chain logging already use.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an optional beforePayment hook to ProxyOptions, exports its context and result contracts, and registers it during proxy startup to allow payment creation to be refused before transaction signing.

Changes

Pre-payment policy

Layer / File(s) Summary
Hook contract and signing guard
src/proxy.ts
ProxyOptions accepts an optional beforePayment hook with exported context and result types. startProxy registers the hook with x402, maps selected requirements into the context, and throws when the hook returns an abort decision.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant startProxy
  participant x402
  participant beforePayment
  participant paymentSigning
  startProxy->>x402: register beforePayment hook
  x402->>beforePayment: provide selected requirements and resource URL
  beforePayment-->>x402: approval or abort decision
  x402->>paymentSigning: build and sign payment when approved
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new vendor-neutral beforePayment pre-sign hook added to startProxy.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/proxy.ts (2)

2048-2073: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Capture options.beforePayment in a local variable to avoid non-null assertion.

The options.beforePayment! assertion inside the closure is safe at runtime (guaranteed by the enclosing if), but TypeScript can't narrow through async closures. Capturing the value in a local const eliminates the assertion and is more resilient to future refactoring.

♻️ Proposed refactor
   if (options.beforePayment) {
+    const hook = options.beforePayment;
     x402.onBeforePaymentCreation(async (ctx) => {
-      const decision = await options.beforePayment!({
+      const decision = await hook({
         selectedRequirements: {
           scheme: ctx.selectedRequirements.scheme,
           network: ctx.selectedRequirements.network,
           amount: ctx.selectedRequirements.amount,
           asset: ctx.selectedRequirements.asset,
           payTo: ctx.selectedRequirements.payTo,
           maxTimeoutSeconds: ctx.selectedRequirements.maxTimeoutSeconds,
           extra: ctx.selectedRequirements.extra,
         },
         resourceUrl: ctx.resourceUrl,
       });
       if (decision && decision.abort === true) {
         throw new Error(
           `[ClawRouter] payment aborted by beforePayment hook: ${decision.reason ?? "refused"}`,
         );
       }
     });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.ts` around lines 2048 - 2073, Capture options.beforePayment in a
local constant within the enclosing conditional, then invoke that local callback
inside the async onBeforePaymentCreation closure instead of using the non-null
assertion. Preserve the existing callback arguments and abort behavior.

2052-2071: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a timeout guard around the hook execution.

The PR summary explicitly states the hook may call an external policy service asynchronously. If that service hangs, await options.beforePayment!(...) blocks indefinitely, stalling payment creation and the entire request. While the overall requestTimeoutMs (default 180s) may eventually cover this, a dedicated timeout on the hook call would fail fast and provide a clearer error. Consider wrapping the hook invocation with AbortSignal.timeout() or a Promise.race against a configurable deadline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.ts` around lines 2052 - 2071, Guard the asynchronous
options.beforePayment hook invocation in the x402 onBeforePaymentCreation
handler with a dedicated, preferably configurable timeout. Ensure a hanging
external policy service rejects promptly with a clear timeout error, while
preserving the existing decision.abort handling for hooks that complete
normally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/proxy.ts`:
- Around line 2048-2073: Capture options.beforePayment in a local constant
within the enclosing conditional, then invoke that local callback inside the
async onBeforePaymentCreation closure instead of using the non-null assertion.
Preserve the existing callback arguments and abort behavior.
- Around line 2052-2071: Guard the asynchronous options.beforePayment hook
invocation in the x402 onBeforePaymentCreation handler with a dedicated,
preferably configurable timeout. Ensure a hanging external policy service
rejects promptly with a clear timeout error, while preserving the existing
decision.abort handling for hooks that complete normally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 792c5101-a13c-4383-b55a-0ff425c18e4c

📥 Commits

Reviewing files that changed from the base of the PR and between 36d26b3 and 8850d52.

📒 Files selected for processing (1)
  • src/proxy.ts

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