From 0e1eae21c5f1597c20bfed51e1074abaa2da760f Mon Sep 17 00:00:00 2001 From: Fsocietyhhh <1211904451@qq.com> Date: Sun, 5 Jul 2026 14:29:59 -0700 Subject: [PATCH] feat(x402): attach BlockRun builder-code service code to payments Tag every x402 payment this SDK signs with the ERC-8021 Schema 2 service code s: ["blockrun"] so BlockRun-originated traffic is attributed on-chain. Merged into builder-code.info.s in both the EVM and Solana payload builders, preserving any app code (a) the server echoes back in its 402. Encoding into settlement calldata is handled by the CDP facilitator; the client only sets the JSON field, so no new dependency or CBOR is required. Docs: https://docs.cdp.coinbase.com/x402/core-concepts/builder-codes --- src/x402.ts | 30 ++++++++++++++++++++++++++++-- test/unit/x402.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/x402.ts b/src/x402.ts index c4da0d9..2fb8369 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -50,6 +50,32 @@ function createNonce(): `0x${string}` { return `0x${Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`; } +/** + * BlockRun's x402 builder code — the ERC-8021 Schema 2 service code (`s`) that + * tags every payment this SDK signs as BlockRun-originated for on-chain + * attribution. See https://docs.cdp.coinbase.com/x402/core-concepts/builder-codes + */ +export const BLOCKRUN_SERVICE_CODE = "blockrun"; + +/** + * Merge BlockRun's service code (`s`) into the payload's `builder-code` + * extension, preserving any app code (`a`) the server echoed back in its 402. + * The CDP facilitator reads `builder-code.info.s` and encodes it into the + * settlement calldata suffix — no CBOR/encoding happens client-side. + */ +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; +} + export interface CreatePaymentOptions { resourceUrl?: string; resourceDescription?: string; @@ -130,7 +156,7 @@ export async function createPaymentPayload( nonce, }, }, - extensions: options.extensions || {}, + extensions: withBuilderCodeServiceCode(options.extensions), }; // Encode as base64 @@ -259,7 +285,7 @@ export async function createSolanaPaymentPayload( payload: { transaction: serializedTx, }, - extensions: options.extensions || {}, + extensions: withBuilderCodeServiceCode(options.extensions), }; // Encode as base64 diff --git a/test/unit/x402.test.ts b/test/unit/x402.test.ts index a765011..5222655 100644 --- a/test/unit/x402.test.ts +++ b/test/unit/x402.test.ts @@ -33,6 +33,34 @@ describe("x402 Payment Protocol", () => { expect(decoded.payload.authorization.to).toBe(TEST_RECIPIENT); }); + it("should attach BlockRun builder-code service code (s)", async () => { + const payload = await createPaymentPayload( + TEST_PRIVATE_KEY, + TEST_ACCOUNT.address, + TEST_RECIPIENT, + "1000000", + "eip155:8453" + ); + + const decoded = JSON.parse(atob(payload)); + expect(decoded.extensions["builder-code"].info.s).toEqual(["blockrun"]); + }); + + it("should preserve server-echoed app code (a) when adding service code", async () => { + const payload = await createPaymentPayload( + TEST_PRIVATE_KEY, + TEST_ACCOUNT.address, + TEST_RECIPIENT, + "1000000", + "eip155:8453", + { extensions: { "builder-code": { info: { a: "blockrun" } } } } + ); + + const decoded = JSON.parse(atob(payload)); + expect(decoded.extensions["builder-code"].info.a).toBe("blockrun"); + expect(decoded.extensions["builder-code"].info.s).toEqual(["blockrun"]); + }); + it("should include resource info when provided", async () => { const payload = await createPaymentPayload( TEST_PRIVATE_KEY,