From 28b1d771058da91a81eb7c7eecb121a431b8c045 Mon Sep 17 00:00:00 2001 From: Fsocietyhhh <1211904451@qq.com> Date: Sun, 5 Jul 2026 14:47:41 -0700 Subject: [PATCH] feat(x402): stamp BlockRun builder-code service code on payments Tag every x402 payment XClawRouter signs with the ERC-8021 Schema 2 service code s: ["blockrun"] so BlockRun-originated traffic is attributed on-chain. An onAfterPaymentCreation hook stamps builder-code.info.s onto the payload, preserving any app code (a) the server echoed back. Mirrors @x402/extensions' BuilderCodeClientExtension but fires unconditionally (independent of the server advertising the extension) and needs no @x402 upgrade. The EIP-712 signature covers the authorization, not the extensions, so stamping after creation is safe. Encoding into settlement calldata is done by the CDP facilitator. Docs: https://docs.cdp.coinbase.com/x402/core-concepts/builder-codes --- src/builder-code.test.ts | 29 +++++++++++++++++++++++++++++ src/builder-code.ts | 32 ++++++++++++++++++++++++++++++++ src/proxy.ts | 14 ++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/builder-code.test.ts create mode 100644 src/builder-code.ts diff --git a/src/builder-code.test.ts b/src/builder-code.test.ts new file mode 100644 index 0000000..8f7685c --- /dev/null +++ b/src/builder-code.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { BLOCKRUN_SERVICE_CODE, withBuilderCodeServiceCode } from "./builder-code.js"; + +describe("withBuilderCodeServiceCode", () => { + it("attaches the BlockRun service code to an empty payload", () => { + const ext = withBuilderCodeServiceCode(undefined); + expect((ext["builder-code"] as any).info.s).toEqual([BLOCKRUN_SERVICE_CODE]); + }); + + it("preserves the server-echoed app code (a) when adding s", () => { + const ext = withBuilderCodeServiceCode({ + "builder-code": { info: { a: "blockrun" } }, + }); + const info = (ext["builder-code"] as any).info; + expect(info.a).toBe("blockrun"); + expect(info.s).toEqual([BLOCKRUN_SERVICE_CODE]); + }); + + it("preserves unrelated extensions", () => { + const ext = withBuilderCodeServiceCode({ "some-ext": { foo: 1 } }); + expect(ext["some-ext"]).toEqual({ foo: 1 }); + }); + + it("does not mutate the input object", () => { + const input = {}; + withBuilderCodeServiceCode(input); + expect(input).toEqual({}); + }); +}); diff --git a/src/builder-code.ts b/src/builder-code.ts new file mode 100644 index 0000000..f760e9a --- /dev/null +++ b/src/builder-code.ts @@ -0,0 +1,32 @@ +/** + * BlockRun x402 builder-code attribution. + * + * Tags every payment XClawRouter signs with the ERC-8021 Schema 2 service code + * (`s`) so BlockRun-originated traffic is attributed on-chain. The CDP + * facilitator reads `builder-code.info.s` from the payment payload and encodes + * it into the settlement calldata suffix — no CBOR/encoding happens here. + * + * See https://docs.cdp.coinbase.com/x402/core-concepts/builder-codes + */ + +/** BlockRun's builder code (must match `^[a-z0-9_]{1,32}$`). */ +export const BLOCKRUN_SERVICE_CODE = "blockrun"; + +/** + * Merge BlockRun's service code (`s`) into a payment payload's `builder-code` + * extension, preserving any app code (`a`) the server echoed back in its 402. + * + * Returns a new extensions object; the input is not mutated. + */ +export function withBuilderCodeServiceCode( + extensions?: Record, +): Record { + const merged: Record = { ...(extensions ?? {}) }; + const existing = + (merged["builder-code"] as { info?: Record } | undefined) ?? {}; + merged["builder-code"] = { + ...existing, + info: { ...(existing.info ?? {}), s: [BLOCKRUN_SERVICE_CODE] }, + }; + return merged; +} diff --git a/src/proxy.ts b/src/proxy.ts index b76e814..40c12e3 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -37,6 +37,7 @@ import { base } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; import { x402Client } from "@x402/fetch"; import { createPayFetchWithPreAuth } from "./payment-preauth.js"; +import { withBuilderCodeServiceCode } from "./builder-code.js"; import { registerExactEvmScheme } from "@x402/evm/exact/client"; import { toClientEvmSigner } from "@x402/evm"; import { @@ -1821,6 +1822,19 @@ export async function startProxy(options: ProxyOptions): Promise { ); } + // Stamp BlockRun's builder-code service code (`s`) onto every signed payment + // for on-chain attribution. Mirrors @x402/extensions' BuilderCodeClientExtension + // but fires unconditionally (independent of whether the server advertises the + // extension) and needs no @x402 upgrade. The EIP-712 signature covers the + // authorization, not the extensions, so stamping post-creation is safe. Any app + // code (`a`) the server echoed back is preserved. + x402.onAfterPaymentCreation(async (context) => { + const payload = context.paymentPayload as { + extensions?: Record; + }; + payload.extensions = withBuilderCodeServiceCode(payload.extensions); + }); + // Log which chain is used for each payment and capture actual payment amount x402.onAfterPaymentCreation(async (context) => { const network = context.selectedRequirements.network;