Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/builder-code.test.ts
Original file line number Diff line number Diff line change
@@ -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({});
});
});
32 changes: 32 additions & 0 deletions src/builder-code.ts
Original file line number Diff line number Diff line change
@@ -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<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;
}
14 changes: 14 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1821,6 +1822,19 @@ export async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
);
}

// 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<string, unknown>;
};
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;
Expand Down
Loading