Skip to content
Merged
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
30 changes: 28 additions & 2 deletions src/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
): Record<string, unknown> {
const merged: Record<string, unknown> = { ...(extensions || {}) };
const existing =
(merged["builder-code"] as { info?: Record<string, unknown> } | undefined) || {};
merged["builder-code"] = {
...existing,
info: { ...(existing.info || {}), s: [BLOCKRUN_SERVICE_CODE] },
};
return merged;
}

export interface CreatePaymentOptions {
resourceUrl?: string;
resourceDescription?: string;
Expand Down Expand Up @@ -130,7 +156,7 @@ export async function createPaymentPayload(
nonce,
},
},
extensions: options.extensions || {},
extensions: withBuilderCodeServiceCode(options.extensions),
};

// Encode as base64
Expand Down Expand Up @@ -259,7 +285,7 @@ export async function createSolanaPaymentPayload(
payload: {
transaction: serializedTx,
},
extensions: options.extensions || {},
extensions: withBuilderCodeServiceCode(options.extensions),
};

// Encode as base64
Expand Down
28 changes: 28 additions & 0 deletions test/unit/x402.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading