Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions typescript/packages/extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
176 changes: 176 additions & 0 deletions typescript/packages/extensions/src/builder-code/cbor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* 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)
* "w" — wallet/facilitator builder code (string)
* "s" — service codes (string array)
*/

import { type Hex } from "viem";
import { ERC_8021_MARKER, SCHEMA_2_ID, type BuilderCodeExtensionData } from "./types";

/**

Check failure on line 16 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @returns declaration

Check failure on line 16 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @param "data" declaration
* Encodes a CBOR map from builder code extension data.
*
* Produces a minimal CBOR map with:
* - "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.
*/
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.w) {
mapSize++;
entries.push(encodeCborString("w"));
entries.push(encodeCborString(data.w));
}

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;
}

/**

Check failure on line 66 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @returns declaration

Check failure on line 66 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @param "value" declaration
* 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;
}

/**

Check failure on line 78 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @returns declaration

Check failure on line 78 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @param "values" declaration
* 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;
}

/**

Check failure on line 100 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @returns declaration

Check failure on line 100 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @param "value" declaration

Check failure on line 100 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc @param "majorType" declaration
* 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 {

Check failure on line 164 in typescript/packages/extensions/src/builder-code/cbor.ts

View workflow job for this annotation

GitHub Actions / check-lint-typescript

Missing JSDoc comment
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("");
}
75 changes: 75 additions & 0 deletions typescript/packages/extensions/src/builder-code/facilitator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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 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
*/

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" (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
*/
buildCalldataSuffix(payloadExtensions?: Record<string, unknown>): Hex | undefined {
const extData = payloadExtensions?.[BUILDER_CODE] as
| BuilderCodeExtensionData
| undefined;

const suffixData: BuilderCodeExtensionData = {
a: extData?.a,
w: this.config.builderCode,
s: extData?.s ? [...extData.s] : undefined,
};

return encodeBuilderCodeSuffix(suffixData);
}
}
67 changes: 67 additions & 0 deletions typescript/packages/extensions/src/builder-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* 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.
*
* 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
*
* The service can optionally include related on-chain services in the "s" array
* (e.g., Morpho, Aerodrome) to attribute protocols it depends on.
*
* ## Usage
*
* ### 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"),
* }
*
* // With related on-chain services
* extensions: {
* [BUILDER_CODE]: declareBuilderCodeExtension("bc_my_service", ["bc_morpho", "bc_aerodrome"]),
* }
* ```
*
* ### 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,
} from "./types";

export {
BUILDER_CODE,
BUILDER_CODE_PATTERN,
ERC_8021_MARKER,
SCHEMA_2_ID,
} from "./types";

// CBOR encoding
export { encodeBuilderCodeSuffix } from "./cbor";

// Resource Server
export {
declareBuilderCodeExtension,
builderCodeResourceServerExtension,
} from "./resourceServer";

// Facilitator
export { BuilderCodeFacilitatorExtension } from "./facilitator";
Loading
Loading