From f3daf3e3fc8889c432829c456cc46b47af765615 Mon Sep 17 00:00:00 2001 From: Peter Kim Date: Thu, 2 Apr 2026 17:33:55 -0700 Subject: [PATCH 1/2] feat: add builder-code extension for ERC-8021 attribution in x402 payments Adds a new builder-code extension that enables ERC-8021 Schema 2 attribution tracking for x402 payments. Three parties can attach their builder code to settlement transactions: - Agent (client): sets the "a" field via BuilderCodeClientExtension - Service (server): declares in 402 response via declareBuilderCodeExtension() - Facilitator: adds to "s" array at settlement via BuilderCodeFacilitatorExtension At settlement, the facilitator encodes all builder codes as a Schema 2 CBOR suffix and appends it to the transferWithAuthorization calldata. The EVM ignores trailing calldata bytes, so the transfer executes normally while indexers (Coindexer, Beacon, Dune) can parse the suffix. Changes: - New extension package: @x402/extensions/builder-code - EVM mechanism: executeTransferWithAuthorization accepts optional calldataSuffix - settleEIP3009 reads builder code extension from FacilitatorContext - scheme.ts passes context through to EIP-3009 settlement --- typescript/packages/extensions/package.json | 10 ++ .../extensions/src/builder-code/cbor.ts | 168 ++++++++++++++++++ .../extensions/src/builder-code/client.ts | 80 +++++++++ .../src/builder-code/facilitator.ts | 85 +++++++++ .../extensions/src/builder-code/index.ts | 75 ++++++++ .../src/builder-code/resourceServer.ts | 63 +++++++ .../extensions/src/builder-code/types.ts | 68 +++++++ typescript/packages/extensions/src/index.ts | 4 + typescript/packages/extensions/tsup.config.ts | 1 + .../src/exact/facilitator/eip3009-utils.ts | 55 +++++- .../evm/src/exact/facilitator/eip3009.ts | 19 ++ .../evm/src/exact/facilitator/scheme.ts | 2 +- 12 files changed, 622 insertions(+), 8 deletions(-) create mode 100644 typescript/packages/extensions/src/builder-code/cbor.ts create mode 100644 typescript/packages/extensions/src/builder-code/client.ts create mode 100644 typescript/packages/extensions/src/builder-code/facilitator.ts create mode 100644 typescript/packages/extensions/src/builder-code/index.ts create mode 100644 typescript/packages/extensions/src/builder-code/resourceServer.ts create mode 100644 typescript/packages/extensions/src/builder-code/types.ts diff --git a/typescript/packages/extensions/package.json b/typescript/packages/extensions/package.json index 696ae43a1f..b17090bf26 100644 --- a/typescript/packages/extensions/package.json +++ b/typescript/packages/extensions/package.json @@ -103,6 +103,16 @@ "types": "./dist/cjs/payment-identifier/index.d.ts", "default": "./dist/cjs/payment-identifier/index.js" } + }, + "./builder-code": { + "import": { + "types": "./dist/esm/builder-code/index.d.mts", + "default": "./dist/esm/builder-code/index.mjs" + }, + "require": { + "types": "./dist/cjs/builder-code/index.d.ts", + "default": "./dist/cjs/builder-code/index.js" + } } }, "files": [ diff --git a/typescript/packages/extensions/src/builder-code/cbor.ts b/typescript/packages/extensions/src/builder-code/cbor.ts new file mode 100644 index 0000000000..6ba0ca4e41 --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/cbor.ts @@ -0,0 +1,168 @@ +/** + * ERC-8021 Schema 2 CBOR encoding for builder code suffixes. + * + * Schema 2 suffix format: + * [cbor_data (variable)] [suffix_data_length (2 bytes)] [schema_id = 0x02 (1 byte)] [ERC-8021 marker (16 bytes)] + * + * CBOR payload uses single-letter keys: + * "a" — app builder code (string) + * "s" — service codes (string array) + */ + +import { type Hex } from "viem"; +import { ERC_8021_MARKER, SCHEMA_2_ID, type BuilderCodeExtensionData } from "./types"; + +/** + * Encodes a CBOR map from builder code extension data. + * + * Produces a minimal CBOR map with: + * - "a" key (major type 3, length 1) → string value + * - "s" key (major type 3, length 1) → array of strings + * + * Uses hand-rolled CBOR to avoid adding a dependency. + */ +function encodeCborMap(data: BuilderCodeExtensionData): Uint8Array { + const entries: Uint8Array[] = []; + let mapSize = 0; + + if (data.a) { + mapSize++; + entries.push(encodeCborString("a")); + entries.push(encodeCborString(data.a)); + } + + if (data.s && data.s.length > 0) { + mapSize++; + entries.push(encodeCborString("s")); + entries.push(encodeCborArray(data.s)); + } + + // CBOR map header + const header = encodeCborMajorType(5, mapSize); // major type 5 = map + + const totalLength = header.length + entries.reduce((sum, e) => sum + e.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + result.set(header, offset); + offset += header.length; + + for (const entry of entries) { + result.set(entry, offset); + offset += entry.length; + } + + return result; +} + +/** + * Encodes a CBOR text string (major type 3). + */ +function encodeCborString(value: string): Uint8Array { + const encoded = new TextEncoder().encode(value); + const header = encodeCborMajorType(3, encoded.length); // major type 3 = text string + const result = new Uint8Array(header.length + encoded.length); + result.set(header, 0); + result.set(encoded, header.length); + return result; +} + +/** + * Encodes a CBOR array of strings (major type 4). + */ +function encodeCborArray(values: string[]): Uint8Array { + const header = encodeCborMajorType(4, values.length); // major type 4 = array + const encodedValues = values.map(encodeCborString); + + const totalLength = header.length + encodedValues.reduce((sum, e) => sum + e.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + result.set(header, offset); + offset += header.length; + + for (const encoded of encodedValues) { + result.set(encoded, offset); + offset += encoded.length; + } + + return result; +} + +/** + * Encodes a CBOR major type with an argument value. + * + * CBOR encoding rules: + * - 0-23: single byte (major type << 5 | value) + * - 24-255: two bytes (major type << 5 | 24, value) + * - 256-65535: three bytes (major type << 5 | 25, value high, value low) + */ +function encodeCborMajorType(majorType: number, value: number): Uint8Array { + const mt = majorType << 5; + + if (value <= 23) { + return new Uint8Array([mt | value]); + } + if (value <= 0xff) { + return new Uint8Array([mt | 24, value]); + } + if (value <= 0xffff) { + return new Uint8Array([mt | 25, (value >> 8) & 0xff, value & 0xff]); + } + + throw new Error(`CBOR value too large: ${value}`); +} + +/** + * Builds a complete ERC-8021 Schema 2 data suffix from builder code data. + * + * Format: [cbor_data][suffix_data_length (2 bytes)][schema_id (1 byte)][marker (16 bytes)] + * + * The suffix_data_length covers the cbor_data only (not itself, schema_id, or marker). + * + * @param data - Builder code extension data with "a" and/or "s" fields + * @returns Hex-encoded suffix bytes (without 0x prefix) ready to append to calldata + */ +export function encodeBuilderCodeSuffix(data: BuilderCodeExtensionData): Hex { + const cborBytes = encodeCborMap(data); + const cborLength = cborBytes.length; + + // suffix_data_length is 2 bytes, big-endian + const lengthHigh = (cborLength >> 8) & 0xff; + const lengthLow = cborLength & 0xff; + + // Build the full suffix: [cbor][length 2B][schema_id 1B][marker 16B] + const suffixBytes = new Uint8Array(cborLength + 2 + 1 + 16); + let offset = 0; + + // CBOR data + suffixBytes.set(cborBytes, offset); + offset += cborLength; + + // Suffix data length (2 bytes, big-endian) + suffixBytes[offset++] = lengthHigh; + suffixBytes[offset++] = lengthLow; + + // Schema ID + suffixBytes[offset++] = SCHEMA_2_ID; + + // ERC-8021 marker (16 bytes) + const markerBytes = hexToBytes(ERC_8021_MARKER); + suffixBytes.set(markerBytes, offset); + + return `0x${bytesToHex(suffixBytes)}`; +} + +function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} diff --git a/typescript/packages/extensions/src/builder-code/client.ts b/typescript/packages/extensions/src/builder-code/client.ts new file mode 100644 index 0000000000..9027735a22 --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/client.ts @@ -0,0 +1,80 @@ +/** + * Client-side extension for the Builder Code Extension. + * + * Implements the ClientExtension interface to automatically inject + * the agent's builder code into payment payloads and echo the service's + * builder code from the 402 response. + */ + +import type { ClientExtension } from "@x402/core/client"; +import type { PaymentPayload, PaymentRequired } from "@x402/core/types"; +import { + BUILDER_CODE, + BUILDER_CODE_PATTERN, + type BuilderCodeClientConfig, + type BuilderCodeExtensionData, +} from "./types"; + +/** + * Creates a BuilderCode client extension that automatically enriches + * payment payloads with the agent's builder code. + * + * The extension: + * 1. Reads the service's builder code from the 402 response extensions + * 2. Adds the agent's own builder code as the "a" (app) field + * 3. Preserves the service's codes in the "s" (services) array + * + * @param config - Configuration with the agent's builder code + * @returns A ClientExtension ready to register with x402Client + * + * @example + * ```typescript + * import { createBuilderCodeClientExtension } from '@x402/extensions/builder-code'; + * + * const client = new x402Client(); + * client.registerExtension(createBuilderCodeClientExtension({ + * builderCode: "bc_my_agent", + * })); + * ``` + */ +export function createBuilderCodeClientExtension( + config: BuilderCodeClientConfig, +): ClientExtension { + if (!BUILDER_CODE_PATTERN.test(config.builderCode)) { + throw new Error( + `Invalid builder code: "${config.builderCode}". ` + + `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, + ); + } + + return { + key: BUILDER_CODE, + + async enrichPaymentPayload( + paymentPayload: PaymentPayload, + paymentRequired: PaymentRequired, + ): Promise { + // Read existing builder-code extension data from the 402 response + const serverExtData = paymentRequired.extensions?.[BUILDER_CODE] as + | BuilderCodeExtensionData + | undefined; + + // Build the merged extension data + const builderCodeData: BuilderCodeExtensionData = { + a: config.builderCode, + s: serverExtData?.s ? [...serverExtData.s] : undefined, + }; + + // Merge into payload extensions + const extensions = { + ...paymentPayload.extensions, + [BUILDER_CODE]: builderCodeData, + }; + + return { + ...paymentPayload, + extensions, + }; + }, + }; +} diff --git a/typescript/packages/extensions/src/builder-code/facilitator.ts b/typescript/packages/extensions/src/builder-code/facilitator.ts new file mode 100644 index 0000000000..ef9a28b4ac --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/facilitator.ts @@ -0,0 +1,85 @@ +/** + * Facilitator-side extension for the Builder Code Extension. + * + * At settlement time, the facilitator: + * 1. Reads builder code data from the payment payload extensions + * 2. Adds its own builder code to the "s" array + * 3. Encodes the combined data as an ERC-8021 Schema 2 CBOR suffix + * 4. The suffix is appended to the settlement transaction calldata + */ + +import type { FacilitatorExtension } from "@x402/core/types"; +import type { Hex } from "viem"; +import { encodeBuilderCodeSuffix } from "./cbor"; +import { + BUILDER_CODE, + BUILDER_CODE_PATTERN, + type BuilderCodeExtensionData, + type BuilderCodeFacilitatorConfig, +} from "./types"; + +/** + * Facilitator extension that manages builder code attribution at settlement time. + * + * Register this with the x402Facilitator to enable builder code support. + * The extension reads builder code data from payment payloads and provides + * the encoded ERC-8021 suffix for the settlement mechanism to append. + * + * @example + * ```typescript + * import { BuilderCodeFacilitatorExtension } from '@x402/extensions/builder-code'; + * + * const facilitator = new x402Facilitator(); + * facilitator.registerExtension(new BuilderCodeFacilitatorExtension({ + * builderCode: "bc_my_facilitator", + * })); + * ``` + */ +export class BuilderCodeFacilitatorExtension implements FacilitatorExtension { + readonly key = BUILDER_CODE; + private readonly config: BuilderCodeFacilitatorConfig; + + constructor(config: BuilderCodeFacilitatorConfig) { + if (!BUILDER_CODE_PATTERN.test(config.builderCode)) { + throw new Error( + `Invalid builder code: "${config.builderCode}". ` + + `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, + ); + } + this.config = config; + } + + /** + * Builds the ERC-8021 Schema 2 calldata suffix from payment payload extensions. + * + * Reads "a" (agent code) and "s" (service codes) from the payment payload, + * prepends the facilitator's own code to "s", and encodes as Schema 2 CBOR. + * + * @param payloadExtensions - The extensions from the payment payload + * @returns Hex-encoded suffix to append to settlement calldata, or undefined if no builder codes + */ + buildCalldataSuffix(payloadExtensions?: Record): Hex | undefined { + const extData = payloadExtensions?.[BUILDER_CODE] as + | BuilderCodeExtensionData + | undefined; + + // Collect service codes: facilitator's own code first, then any from the payload + const serviceCodes: string[] = [this.config.builderCode]; + if (extData?.s) { + for (const code of extData.s) { + if (!serviceCodes.includes(code)) { + serviceCodes.push(code); + } + } + } + + const suffixData: BuilderCodeExtensionData = { + a: extData?.a, + s: serviceCodes, + }; + + // If we only have the facilitator's code and nothing else, still encode + // (facilitator attribution alone is still valuable) + return encodeBuilderCodeSuffix(suffixData); + } +} diff --git a/typescript/packages/extensions/src/builder-code/index.ts b/typescript/packages/extensions/src/builder-code/index.ts new file mode 100644 index 0000000000..3137abf3bd --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/index.ts @@ -0,0 +1,75 @@ +/** + * Builder Code Extension for x402 (ERC-8021) + * + * Enables attribution tracking for x402 payments by appending ERC-8021 + * Schema 2 builder codes to settlement transaction calldata. + * + * Three parties can attach their builder code: + * - Agent (client): Sets the "a" field via BuilderCodeClientExtension + * - Service (server): Declares in 402 response via declareBuilderCodeExtension() + * - Facilitator: Adds to "s" array at settlement via BuilderCodeFacilitatorExtension + * + * ## Usage + * + * ### For Agents (Clients) + * + * ```typescript + * import { createBuilderCodeClientExtension } from '@x402/extensions/builder-code'; + * + * const client = new x402Client(); + * client.registerExtension(createBuilderCodeClientExtension({ + * builderCode: "bc_my_agent", + * })); + * ``` + * + * ### For Services (Resource Servers) + * + * ```typescript + * import { declareBuilderCodeExtension, BUILDER_CODE } from '@x402/extensions/builder-code'; + * + * // In paywall config extensions + * extensions: { + * [BUILDER_CODE]: declareBuilderCodeExtension("bc_my_service"), + * } + * ``` + * + * ### For Facilitators + * + * ```typescript + * import { BuilderCodeFacilitatorExtension } from '@x402/extensions/builder-code'; + * + * const facilitator = new x402Facilitator(); + * facilitator.registerExtension(new BuilderCodeFacilitatorExtension({ + * builderCode: "bc_my_facilitator", + * })); + * ``` + */ + +// Types +export type { + BuilderCodeExtensionData, + BuilderCodeFacilitatorConfig, + BuilderCodeClientConfig, +} from "./types"; + +export { + BUILDER_CODE, + BUILDER_CODE_PATTERN, + ERC_8021_MARKER, + SCHEMA_2_ID, +} from "./types"; + +// CBOR encoding +export { encodeBuilderCodeSuffix } from "./cbor"; + +// Client +export { createBuilderCodeClientExtension } from "./client"; + +// Resource Server +export { + declareBuilderCodeExtension, + builderCodeResourceServerExtension, +} from "./resourceServer"; + +// Facilitator +export { BuilderCodeFacilitatorExtension } from "./facilitator"; diff --git a/typescript/packages/extensions/src/builder-code/resourceServer.ts b/typescript/packages/extensions/src/builder-code/resourceServer.ts new file mode 100644 index 0000000000..e074846277 --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/resourceServer.ts @@ -0,0 +1,63 @@ +/** + * Resource Server utilities for the Builder Code Extension. + * + * Services use this to declare their builder code in the 402 response, + * which gets echoed back by the client and forwarded to the facilitator. + */ + +import type { ResourceServerExtension } from "@x402/core/types"; +import { + BUILDER_CODE, + BUILDER_CODE_PATTERN, + type BuilderCodeExtensionData, +} from "./types"; + +/** + * Declares the builder-code extension for inclusion in PaymentRequired.extensions. + * + * The service's builder code is placed in the "s" (services) array. + * The client will echo this back in the payment payload, and the facilitator + * will include it in the ERC-8021 Schema 2 suffix at settlement. + * + * @param builderCode - The service's builder code (e.g., "bc_weather_svc") + * @returns A BuilderCodeExtensionData object for PaymentRequired.extensions + * + * @example + * ```typescript + * import { declareBuilderCodeExtension, BUILDER_CODE } from '@x402/extensions/builder-code'; + * + * // In your paywall config + * const paymentRequired = { + * x402Version: 2, + * accepts: [ ... ], + * extensions: { + * [BUILDER_CODE]: declareBuilderCodeExtension("bc_weather_svc"), + * }, + * }; + * ``` + */ +export function declareBuilderCodeExtension( + builderCode: string, +): BuilderCodeExtensionData { + if (!BUILDER_CODE_PATTERN.test(builderCode)) { + throw new Error( + `Invalid builder code: "${builderCode}". ` + + `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, + ); + } + + return { + s: [builderCode], + }; +} + +/** + * ResourceServerExtension implementation for builder-code. + * + * Register this with the resource server to advertise builder code support. + * The actual builder code value is set via declareBuilderCodeExtension() + * in the paywall configuration. + */ +export const builderCodeResourceServerExtension: ResourceServerExtension = { + key: BUILDER_CODE, +}; diff --git a/typescript/packages/extensions/src/builder-code/types.ts b/typescript/packages/extensions/src/builder-code/types.ts new file mode 100644 index 0000000000..6bbb9d6bff --- /dev/null +++ b/typescript/packages/extensions/src/builder-code/types.ts @@ -0,0 +1,68 @@ +/** + * Type definitions for the Builder Code Extension (ERC-8021) + * + * Enables attribution tracking for x402 payments by appending + * ERC-8021 Schema 2 builder codes to settlement transaction calldata. + */ + +/** + * Extension identifier constant + */ +export const BUILDER_CODE = "builder-code"; + +/** + * ERC-8021 marker bytes (16 bytes) appended at the end of every suffix + */ +export const ERC_8021_MARKER = "80218021802180218021802180218021"; + +/** + * Schema 2 identifier byte + */ +export const SCHEMA_2_ID = 0x02; + +/** + * Pattern for valid builder codes (lowercase alphanumeric + underscore, 1-32 chars) + */ +export const BUILDER_CODE_PATTERN = /^[a-z0-9_]{1,32}$/; + +/** + * Builder code extension data as it appears in PaymentRequired/PaymentPayload extensions. + * + * Maps to ERC-8021 Schema 2 fields: + * - a: app/agent code (the entity that initiated the payment) + * - s: service codes array (facilitator, service endpoint, platform, etc.) + */ +export interface BuilderCodeExtensionData { + /** + * App/agent builder code — the entity that initiated the payment. + * Maps to the "a" field in ERC-8021 Schema 2. + */ + a?: string; + + /** + * Service builder codes — service-layer participants involved in the transaction. + * Maps to the "s" field in ERC-8021 Schema 2. + * Includes: facilitator, service endpoint, platform, etc. + */ + s?: string[]; +} + +/** + * Configuration for the builder code facilitator extension. + */ +export interface BuilderCodeFacilitatorConfig { + /** + * The facilitator's own builder code, added to the "s" array at settlement. + */ + builderCode: string; +} + +/** + * Configuration for the builder code client extension. + */ +export interface BuilderCodeClientConfig { + /** + * The agent/app's own builder code, set as the "a" field. + */ + builderCode: string; +} diff --git a/typescript/packages/extensions/src/index.ts b/typescript/packages/extensions/src/index.ts index a8e18bfd60..c7c7e5eded 100644 --- a/typescript/packages/extensions/src/index.ts +++ b/typescript/packages/extensions/src/index.ts @@ -20,3 +20,7 @@ export * from "./eip2612-gas-sponsoring"; // ERC-20 Approval Gas Sponsoring extension export * from "./erc20-approval-gas-sponsoring"; + +// Builder Code extension (ERC-8021) +export * from "./builder-code"; +export { builderCodeResourceServerExtension } from "./builder-code/resourceServer"; diff --git a/typescript/packages/extensions/tsup.config.ts b/typescript/packages/extensions/tsup.config.ts index bd85a73925..fe76c754d1 100644 --- a/typescript/packages/extensions/tsup.config.ts +++ b/typescript/packages/extensions/tsup.config.ts @@ -7,6 +7,7 @@ const baseConfig = { "sign-in-with-x/index": "src/sign-in-with-x/index.ts", "offer-receipt/index": "src/offer-receipt/index.ts", "payment-identifier/index": "src/payment-identifier/index.ts", + "builder-code/index": "src/builder-code/index.ts", }, dts: { resolve: true, diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts index 2d8f2c4a69..e55fe09db2 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts @@ -191,9 +191,15 @@ export async function diagnoseEip3009SimulationFailure( /** * Executes transferWithAuthorization onchain. * + * When a calldataSuffix is provided (e.g., ERC-8021 builder code attribution), + * the function manually encodes the calldata, appends the suffix, and uses + * sendTransaction instead of writeContract. The EVM ignores trailing calldata + * bytes, so the transfer executes normally while indexers can read the suffix. + * * @param signer - EVM signer for contract writes * @param erc20Address - ERC-20 token contract address * @param payload - EIP-3009 transfer authorization payload + * @param calldataSuffix - Optional hex bytes to append after the ABI-encoded calldata * * @returns Transaction hash */ @@ -201,6 +207,7 @@ export async function executeTransferWithAuthorization( signer: FacilitatorEvmSigner, erc20Address: `0x${string}`, payload: ExactEIP3009Payload, + calldataSuffix?: Hex, ): Promise { const { signature } = parseErc6492Signature(payload.signature!); const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; @@ -216,10 +223,36 @@ export async function executeTransferWithAuthorization( auth.nonce, ] as const; - if (isECDSA) { - const parsedSig = parseSignature(signature); + // If no suffix, use the standard writeContract path (unchanged behavior) + if (!calldataSuffix) { + if (isECDSA) { + const parsedSig = parseSignature(signature); + return signer.writeContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [ + ...baseArgs, + (parsedSig.v as number | undefined) || parsedSig.yParity, + parsedSig.r, + parsedSig.s, + ], + }); + } + return signer.writeContract({ address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...baseArgs, signature], + }); + } + + // With suffix: encode calldata manually, append suffix, use sendTransaction + let calldata: Hex; + if (isECDSA) { + const parsedSig = parseSignature(signature); + calldata = encodeFunctionData({ abi: eip3009ABI, functionName: "transferWithAuthorization", args: [ @@ -229,12 +262,20 @@ export async function executeTransferWithAuthorization( parsedSig.s, ], }); + } else { + calldata = encodeFunctionData({ + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...baseArgs, signature], + }); } - return signer.writeContract({ - address: erc20Address, - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [...baseArgs, signature], + // Append the suffix (strip 0x prefix from suffix before concatenating) + const suffixHex = calldataSuffix.startsWith("0x") ? calldataSuffix.slice(2) : calldataSuffix; + const calldataWithSuffix = `${calldata}${suffixHex}` as Hex; + + return signer.sendTransaction({ + to: erc20Address, + data: calldataWithSuffix, }); } diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts index 69056a938b..f57fc8a6f1 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts @@ -1,6 +1,7 @@ import { PaymentPayload, PaymentRequirements, + FacilitatorContext, SettleResponse, VerifyResponse, } from "@x402/core/types"; @@ -235,11 +236,16 @@ export async function verifyEIP3009( /** * Settles an EIP-3009 payment by executing transferWithAuthorization. * + * If a BuilderCodeFacilitatorExtension is registered, the facilitator will + * append an ERC-8021 Schema 2 suffix to the settlement transaction calldata + * containing builder codes from the agent, service, and facilitator. + * * @param signer - The facilitator signer for contract writes * @param payload - The payment payload to settle * @param requirements - The payment requirements * @param eip3009Payload - The EIP-3009 specific payload * @param config - Facilitator configuration + * @param context - Optional facilitator context for extension capabilities * @returns Promise resolving to settlement response */ export async function settleEIP3009( @@ -248,6 +254,7 @@ export async function settleEIP3009( requirements: PaymentRequirements, eip3009Payload: ExactEIP3009Payload, config: EIP3009FacilitatorConfig, + context?: FacilitatorContext, ): Promise { const payer = eip3009Payload.authorization.from; @@ -293,10 +300,22 @@ export async function settleEIP3009( } } + // Build ERC-8021 calldata suffix if builder code extension is registered + let calldataSuffix: Hex | undefined; + if (context) { + const builderCodeExt = context.getExtension("builder-code"); + if (builderCodeExt && "buildCalldataSuffix" in builderCodeExt) { + calldataSuffix = (builderCodeExt as { buildCalldataSuffix: (ext?: Record) => Hex | undefined }).buildCalldataSuffix( + payload.extensions as Record | undefined, + ); + } + } + const tx = await executeTransferWithAuthorization( signer, getAddress(requirements.asset), eip3009Payload, + calldataSuffix, ); // Wait for transaction confirmation diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts index d5bcd5afc6..8904333beb 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts @@ -121,6 +121,6 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { } const eip3009Payload: ExactEIP3009Payload = rawPayload; - return settleEIP3009(this.signer, payload, requirements, eip3009Payload, this.config); + return settleEIP3009(this.signer, payload, requirements, eip3009Payload, this.config, context); } } From 1b2fbfc07e95a872c9f149239934e5cc9bb8c2ea Mon Sep 17 00:00:00 2001 From: Peter Kim Date: Thu, 9 Apr 2026 10:53:09 -0700 Subject: [PATCH 2/2] refactor: service as app (a), facilitator as wallet (w), remove client extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Service declares its builder code as "a" (app) — it's the application exposing the x402 endpoint - Facilitator adds its code as "w" (wallet) — it's the entity that signs and broadcasts the settlement transaction - Service can optionally include related on-chain services in "s" array (e.g., Morpho, Aerodrome) - Remove BuilderCodeClientExtension — agent doesn't attach builder codes, the service's extension data passes through untouched - CBOR encoder now handles "a", "w", and "s" fields --- .../extensions/src/builder-code/cbor.ts | 12 ++- .../extensions/src/builder-code/client.ts | 80 ------------------- .../src/builder-code/facilitator.ts | 22 ++--- .../extensions/src/builder-code/index.ts | 30 +++---- .../src/builder-code/resourceServer.ts | 60 +++++++++----- .../extensions/src/builder-code/types.ts | 32 ++++---- typescript/packages/extensions/src/index.ts | 1 - 7 files changed, 84 insertions(+), 153 deletions(-) delete mode 100644 typescript/packages/extensions/src/builder-code/client.ts diff --git a/typescript/packages/extensions/src/builder-code/cbor.ts b/typescript/packages/extensions/src/builder-code/cbor.ts index 6ba0ca4e41..d45ba272e1 100644 --- a/typescript/packages/extensions/src/builder-code/cbor.ts +++ b/typescript/packages/extensions/src/builder-code/cbor.ts @@ -6,6 +6,7 @@ * * CBOR payload uses single-letter keys: * "a" — app builder code (string) + * "w" — wallet/facilitator builder code (string) * "s" — service codes (string array) */ @@ -16,8 +17,9 @@ import { ERC_8021_MARKER, SCHEMA_2_ID, type BuilderCodeExtensionData } from "./t * Encodes a CBOR map from builder code extension data. * * Produces a minimal CBOR map with: - * - "a" key (major type 3, length 1) → string value - * - "s" key (major type 3, length 1) → array of strings + * - "a" key (major type 3, length 1) → string value (app/service code) + * - "w" key (major type 3, length 1) → string value (facilitator code) + * - "s" key (major type 3, length 1) → array of strings (related services) * * Uses hand-rolled CBOR to avoid adding a dependency. */ @@ -31,6 +33,12 @@ function encodeCborMap(data: BuilderCodeExtensionData): Uint8Array { entries.push(encodeCborString(data.a)); } + if (data.w) { + mapSize++; + entries.push(encodeCborString("w")); + entries.push(encodeCborString(data.w)); + } + if (data.s && data.s.length > 0) { mapSize++; entries.push(encodeCborString("s")); diff --git a/typescript/packages/extensions/src/builder-code/client.ts b/typescript/packages/extensions/src/builder-code/client.ts deleted file mode 100644 index 9027735a22..0000000000 --- a/typescript/packages/extensions/src/builder-code/client.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Client-side extension for the Builder Code Extension. - * - * Implements the ClientExtension interface to automatically inject - * the agent's builder code into payment payloads and echo the service's - * builder code from the 402 response. - */ - -import type { ClientExtension } from "@x402/core/client"; -import type { PaymentPayload, PaymentRequired } from "@x402/core/types"; -import { - BUILDER_CODE, - BUILDER_CODE_PATTERN, - type BuilderCodeClientConfig, - type BuilderCodeExtensionData, -} from "./types"; - -/** - * Creates a BuilderCode client extension that automatically enriches - * payment payloads with the agent's builder code. - * - * The extension: - * 1. Reads the service's builder code from the 402 response extensions - * 2. Adds the agent's own builder code as the "a" (app) field - * 3. Preserves the service's codes in the "s" (services) array - * - * @param config - Configuration with the agent's builder code - * @returns A ClientExtension ready to register with x402Client - * - * @example - * ```typescript - * import { createBuilderCodeClientExtension } from '@x402/extensions/builder-code'; - * - * const client = new x402Client(); - * client.registerExtension(createBuilderCodeClientExtension({ - * builderCode: "bc_my_agent", - * })); - * ``` - */ -export function createBuilderCodeClientExtension( - config: BuilderCodeClientConfig, -): ClientExtension { - if (!BUILDER_CODE_PATTERN.test(config.builderCode)) { - throw new Error( - `Invalid builder code: "${config.builderCode}". ` + - `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, - ); - } - - return { - key: BUILDER_CODE, - - async enrichPaymentPayload( - paymentPayload: PaymentPayload, - paymentRequired: PaymentRequired, - ): Promise { - // Read existing builder-code extension data from the 402 response - const serverExtData = paymentRequired.extensions?.[BUILDER_CODE] as - | BuilderCodeExtensionData - | undefined; - - // Build the merged extension data - const builderCodeData: BuilderCodeExtensionData = { - a: config.builderCode, - s: serverExtData?.s ? [...serverExtData.s] : undefined, - }; - - // Merge into payload extensions - const extensions = { - ...paymentPayload.extensions, - [BUILDER_CODE]: builderCodeData, - }; - - return { - ...paymentPayload, - extensions, - }; - }, - }; -} diff --git a/typescript/packages/extensions/src/builder-code/facilitator.ts b/typescript/packages/extensions/src/builder-code/facilitator.ts index ef9a28b4ac..79b8300b7f 100644 --- a/typescript/packages/extensions/src/builder-code/facilitator.ts +++ b/typescript/packages/extensions/src/builder-code/facilitator.ts @@ -3,7 +3,7 @@ * * At settlement time, the facilitator: * 1. Reads builder code data from the payment payload extensions - * 2. Adds its own builder code to the "s" array + * 2. Adds its own builder code as the "w" (wallet) field * 3. Encodes the combined data as an ERC-8021 Schema 2 CBOR suffix * 4. The suffix is appended to the settlement transaction calldata */ @@ -52,8 +52,9 @@ export class BuilderCodeFacilitatorExtension implements FacilitatorExtension { /** * Builds the ERC-8021 Schema 2 calldata suffix from payment payload extensions. * - * Reads "a" (agent code) and "s" (service codes) from the payment payload, - * prepends the facilitator's own code to "s", and encodes as Schema 2 CBOR. + * Reads "a" (app/service code) and "s" (related service codes) from the + * payment payload, adds the facilitator's own code as "w" (wallet), and + * encodes as Schema 2 CBOR. * * @param payloadExtensions - The extensions from the payment payload * @returns Hex-encoded suffix to append to settlement calldata, or undefined if no builder codes @@ -63,23 +64,12 @@ export class BuilderCodeFacilitatorExtension implements FacilitatorExtension { | BuilderCodeExtensionData | undefined; - // Collect service codes: facilitator's own code first, then any from the payload - const serviceCodes: string[] = [this.config.builderCode]; - if (extData?.s) { - for (const code of extData.s) { - if (!serviceCodes.includes(code)) { - serviceCodes.push(code); - } - } - } - const suffixData: BuilderCodeExtensionData = { a: extData?.a, - s: serviceCodes, + w: this.config.builderCode, + s: extData?.s ? [...extData.s] : undefined, }; - // If we only have the facilitator's code and nothing else, still encode - // (facilitator attribution alone is still valuable) return encodeBuilderCodeSuffix(suffixData); } } diff --git a/typescript/packages/extensions/src/builder-code/index.ts b/typescript/packages/extensions/src/builder-code/index.ts index 3137abf3bd..b10f0ecea6 100644 --- a/typescript/packages/extensions/src/builder-code/index.ts +++ b/typescript/packages/extensions/src/builder-code/index.ts @@ -4,23 +4,14 @@ * Enables attribution tracking for x402 payments by appending ERC-8021 * Schema 2 builder codes to settlement transaction calldata. * - * Three parties can attach their builder code: - * - Agent (client): Sets the "a" field via BuilderCodeClientExtension - * - Service (server): Declares in 402 response via declareBuilderCodeExtension() - * - Facilitator: Adds to "s" array at settlement via BuilderCodeFacilitatorExtension + * Two parties attach their builder code: + * - Service (server): Declares as "a" (app) in 402 response via declareBuilderCodeExtension() + * - Facilitator: Adds as "w" (wallet) at settlement via BuilderCodeFacilitatorExtension * - * ## Usage - * - * ### For Agents (Clients) - * - * ```typescript - * import { createBuilderCodeClientExtension } from '@x402/extensions/builder-code'; + * The service can optionally include related on-chain services in the "s" array + * (e.g., Morpho, Aerodrome) to attribute protocols it depends on. * - * const client = new x402Client(); - * client.registerExtension(createBuilderCodeClientExtension({ - * builderCode: "bc_my_agent", - * })); - * ``` + * ## Usage * * ### For Services (Resource Servers) * @@ -31,6 +22,11 @@ * extensions: { * [BUILDER_CODE]: declareBuilderCodeExtension("bc_my_service"), * } + * + * // With related on-chain services + * extensions: { + * [BUILDER_CODE]: declareBuilderCodeExtension("bc_my_service", ["bc_morpho", "bc_aerodrome"]), + * } * ``` * * ### For Facilitators @@ -49,7 +45,6 @@ export type { BuilderCodeExtensionData, BuilderCodeFacilitatorConfig, - BuilderCodeClientConfig, } from "./types"; export { @@ -62,9 +57,6 @@ export { // CBOR encoding export { encodeBuilderCodeSuffix } from "./cbor"; -// Client -export { createBuilderCodeClientExtension } from "./client"; - // Resource Server export { declareBuilderCodeExtension, diff --git a/typescript/packages/extensions/src/builder-code/resourceServer.ts b/typescript/packages/extensions/src/builder-code/resourceServer.ts index e074846277..891ff97f43 100644 --- a/typescript/packages/extensions/src/builder-code/resourceServer.ts +++ b/typescript/packages/extensions/src/builder-code/resourceServer.ts @@ -1,8 +1,10 @@ /** * Resource Server utilities for the Builder Code Extension. * - * Services use this to declare their builder code in the 402 response, - * which gets echoed back by the client and forwarded to the facilitator. + * Services use this to declare their builder code in the 402 response. + * The service's code goes in the "a" (app) field since the service is + * the application exposing the x402 endpoint. Optionally, the service + * can include related on-chain services in the "s" array. */ import type { ResourceServerExtension } from "@x402/core/types"; @@ -15,40 +17,60 @@ import { /** * Declares the builder-code extension for inclusion in PaymentRequired.extensions. * - * The service's builder code is placed in the "s" (services) array. - * The client will echo this back in the payment payload, and the facilitator - * will include it in the ERC-8021 Schema 2 suffix at settlement. + * The service's builder code is placed in the "a" (app) field — the service + * is the application that exposed the x402 endpoint. Related on-chain services + * the app depends on (e.g., Morpho, Aerodrome) can be listed in the "s" array. * - * @param builderCode - The service's builder code (e.g., "bc_weather_svc") + * @param appCode - The service's builder code (e.g., "bc_weather_svc") + * @param serviceCodes - Optional array of related service builder codes * @returns A BuilderCodeExtensionData object for PaymentRequired.extensions * * @example * ```typescript * import { declareBuilderCodeExtension, BUILDER_CODE } from '@x402/extensions/builder-code'; * - * // In your paywall config - * const paymentRequired = { - * x402Version: 2, - * accepts: [ ... ], - * extensions: { - * [BUILDER_CODE]: declareBuilderCodeExtension("bc_weather_svc"), - * }, - * }; + * // Simple: just the service's own code + * extensions: { + * [BUILDER_CODE]: declareBuilderCodeExtension("bc_weather_svc"), + * } + * + * // With related services + * extensions: { + * [BUILDER_CODE]: declareBuilderCodeExtension("bc_lending_app", ["bc_morpho", "bc_aerodrome"]), + * } * ``` */ export function declareBuilderCodeExtension( - builderCode: string, + appCode: string, + serviceCodes?: string[], ): BuilderCodeExtensionData { - if (!BUILDER_CODE_PATTERN.test(builderCode)) { + if (!BUILDER_CODE_PATTERN.test(appCode)) { throw new Error( - `Invalid builder code: "${builderCode}". ` + + `Invalid builder code: "${appCode}". ` + `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, ); } - return { - s: [builderCode], + if (serviceCodes) { + for (const code of serviceCodes) { + if (!BUILDER_CODE_PATTERN.test(code)) { + throw new Error( + `Invalid service builder code: "${code}". ` + + `Must be 1-32 characters, lowercase alphanumeric and underscores only.`, + ); + } + } + } + + const data: BuilderCodeExtensionData = { + a: appCode, }; + + if (serviceCodes && serviceCodes.length > 0) { + data.s = serviceCodes; + } + + return data; } /** diff --git a/typescript/packages/extensions/src/builder-code/types.ts b/typescript/packages/extensions/src/builder-code/types.ts index 6bbb9d6bff..457c315e0f 100644 --- a/typescript/packages/extensions/src/builder-code/types.ts +++ b/typescript/packages/extensions/src/builder-code/types.ts @@ -29,20 +29,30 @@ export const BUILDER_CODE_PATTERN = /^[a-z0-9_]{1,32}$/; * Builder code extension data as it appears in PaymentRequired/PaymentPayload extensions. * * Maps to ERC-8021 Schema 2 fields: - * - a: app/agent code (the entity that initiated the payment) - * - s: service codes array (facilitator, service endpoint, platform, etc.) + * - a: app code (the x402 service that exposed the endpoint) + * - w: wallet code (the facilitator that settled the payment on-chain) + * - s: service codes array (related on-chain services the app depends on) */ export interface BuilderCodeExtensionData { /** - * App/agent builder code — the entity that initiated the payment. + * App builder code — the x402 service that exposed the paid endpoint. * Maps to the "a" field in ERC-8021 Schema 2. + * Set by the service in the 402 response. */ a?: string; /** - * Service builder codes — service-layer participants involved in the transaction. + * Wallet builder code — the facilitator that settled the payment on-chain. + * Maps to the "w" field in ERC-8021 Schema 2. + * Set by the facilitator at settlement time. + */ + w?: string; + + /** + * Service builder codes — related on-chain services the app depends on. * Maps to the "s" field in ERC-8021 Schema 2. - * Includes: facilitator, service endpoint, platform, etc. + * Optionally set by the service to attribute protocols it interacts with + * (e.g., Morpho for lending, Aerodrome for swaps). */ s?: string[]; } @@ -52,17 +62,7 @@ export interface BuilderCodeExtensionData { */ export interface BuilderCodeFacilitatorConfig { /** - * The facilitator's own builder code, added to the "s" array at settlement. - */ - builderCode: string; -} - -/** - * Configuration for the builder code client extension. - */ -export interface BuilderCodeClientConfig { - /** - * The agent/app's own builder code, set as the "a" field. + * The facilitator's own builder code, set as the "w" field at settlement. */ builderCode: string; } diff --git a/typescript/packages/extensions/src/index.ts b/typescript/packages/extensions/src/index.ts index c7c7e5eded..1663543ff8 100644 --- a/typescript/packages/extensions/src/index.ts +++ b/typescript/packages/extensions/src/index.ts @@ -23,4 +23,3 @@ export * from "./erc20-approval-gas-sponsoring"; // Builder Code extension (ERC-8021) export * from "./builder-code"; -export { builderCodeResourceServerExtension } from "./builder-code/resourceServer";