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
4 changes: 4 additions & 0 deletions examples/typescript/clients/builder-code/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
EVM_PRIVATE_KEY=
SVM_PRIVATE_KEY=
RESOURCE_SERVER_URL=http://localhost:4021
ENDPOINT_PATH=/weather
8 changes: 8 additions & 0 deletions examples/typescript/clients/builder-code/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
docs/
dist/
node_modules/
coverage/
.github/
src/client
**/**/*.json
*.md
11 changes: 11 additions & 0 deletions examples/typescript/clients/builder-code/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"proseWrap": "never"
}
42 changes: 42 additions & 0 deletions examples/typescript/clients/builder-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Builder Code Example Client

Example client for the [builder-code server](../../servers/builder-code/). Makes a paid request and verifies that ERC-8021 builder-code attribution was appended to the settlement transaction calldata.

```typescript
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(process.env.EVM_PRIVATE_KEY!)));

const response = await wrapFetchWithPayment(fetch, client)("http://localhost:4021/weather");
console.log(await response.json());
```

## Prerequisites

- Node.js v20+
- pnpm v10
- Running [builder-code server](../../servers/builder-code/) and [builder-code facilitator](../../facilitator/builder-code/)
- EVM private key funded on Base Sepolia

## Setup

1. Install and build from the typescript examples root:

```bash
cd ../../
pnpm install && pnpm build
cd clients/builder-code
```

2. Copy `.env-local` to `.env` and set `EVM_PRIVATE_KEY`.

3. Run the client:

```bash
pnpm dev
```

On success, the client prints the settlement transaction hash and the builder codes parsed from on-chain calldata (for example `a` for the service app code and `w` for the facilitator wallet code).
72 changes: 72 additions & 0 deletions examples/typescript/clients/builder-code/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import js from "@eslint/js";
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import prettier from "eslint-plugin-prettier";
import jsdoc from "eslint-plugin-jsdoc";
import importPlugin from "eslint-plugin-import";

export default [
{
ignores: ["dist/**", "node_modules/**"],
},
{
files: ["**/*.ts"],
languageOptions: {
parser: tsParser,
sourceType: "module",
ecmaVersion: 2020,
globals: {
process: "readonly",
__dirname: "readonly",
module: "readonly",
require: "readonly",
Buffer: "readonly",
exports: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
},
},
plugins: {
"@typescript-eslint": ts,
prettier: prettier,
jsdoc: jsdoc,
import: importPlugin,
},
rules: {
...ts.configs.recommended.rules,
"import/first": "error",
"prettier/prettier": "error",
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }],
"jsdoc/tag-lines": ["error", "any", { startLines: 1 }],
"jsdoc/check-alignment": "error",
"jsdoc/no-undefined-types": "off",
"jsdoc/check-param-names": "error",
"jsdoc/check-tag-names": "error",
"jsdoc/check-types": "error",
"jsdoc/implements-on-classes": "error",
"jsdoc/require-description": "error",
"jsdoc/require-jsdoc": [
"error",
{
require: {
FunctionDeclaration: true,
MethodDefinition: true,
ClassDeclaration: true,
ArrowFunctionExpression: false,
FunctionExpression: false,
},
},
],
"jsdoc/require-param": "error",
"jsdoc/require-param-description": "error",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns": "error",
"jsdoc/require-returns-description": "error",
"jsdoc/require-returns-type": "off",
"jsdoc/require-hyphen-before-param-description": ["error", "always"],
},
},
];
79 changes: 79 additions & 0 deletions examples/typescript/clients/builder-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { config } from "dotenv";
import { x402Client, wrapFetchWithPayment, x402HTTPClient } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import {
BuilderCodeClientExtension,
parseBuilderCodeSuffixFromCalldata,
} from "@x402/extensions/builder-code";
import { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, http, type Hex } from "viem";
import { baseSepolia } from "viem/chains";

config();

const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}`;
const evmRpcUrl = process.env.EVM_RPC_URL ?? "https://sepolia.base.org";
const clientBuilderCode = process.env.CLIENT_BUILDER_CODE || "bc_example_client";
const baseURL = process.env.RESOURCE_SERVER_URL || "http://localhost:4021";
const endpointPath = process.env.ENDPOINT_PATH || "/weather";
const url = `${baseURL}${endpointPath}`;

/**
* Example client for builder-code attribution on x402-protected endpoints.
*
* Required environment variables:
* - EVM_PRIVATE_KEY: The private key of the EVM signer
*
* Optional environment variables:
* - EVM_RPC_URL: JSON-RPC endpoint for onchain verification (defaults to Base Sepolia)
* - CLIENT_BUILDER_CODE: Builder code for client attribution (defaults to "bc_example_client")
* - RESOURCE_SERVER_URL: Resource server base URL
* - ENDPOINT_PATH: Paid endpoint path
*/
async function main(): Promise<void> {
const evmSigner = privateKeyToAccount(evmPrivateKey);
const rpcOptions = { rpcUrl: evmRpcUrl };

const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(evmSigner, rpcOptions));
client.registerExtension(new BuilderCodeClientExtension(clientBuilderCode));

const fetchWithPayment = wrapFetchWithPayment(fetch, client);

console.log(`Making request to: ${url}\n`);
const response = await fetchWithPayment(url, { method: "GET" });
const contentType = response.headers.get("content-type") ?? "";
const body = contentType.includes("application/json")
? await response.json()
: await response.text();
console.log("Response body:", body);

const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name =>
response.headers.get(name),
);
console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2));

if (!paymentResponse?.success || !paymentResponse.transaction) {
throw new Error("Settlement did not return a transaction hash");
}

const txHash = paymentResponse.transaction as Hex;
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(evmRpcUrl),
});
const tx = await publicClient.getTransaction({ hash: txHash });

const attribution = parseBuilderCodeSuffixFromCalldata(tx.input);
if (!attribution) {
throw new Error(`ERC-8021 builder-code suffix not found in calldata for ${txHash}`);
}

console.log("\nBuilder-code attribution verified onchain:", attribution);
console.log(`Explorer: https://sepolia.basescan.org/tx/${txHash}`);
}

main().catch(error => {
console.error(error?.response?.data?.error ?? error);
process.exit(1);
});
33 changes: 33 additions & 0 deletions examples/typescript/clients/builder-code/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@x402/builder-code-client-example",
"private": true,
"type": "module",
"scripts": {
"start": "tsx index.ts",
"dev": "tsx index.ts",
"format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"",
"format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"",
"lint": "eslint . --ext .ts --fix",
"lint:check": "eslint . --ext .ts"
},
"dependencies": {
"@x402/evm": "workspace:*",
"@x402/extensions": "workspace:*",
"@x402/fetch": "workspace:*",
"dotenv": "^16.4.7",
"viem": "^2.48.11"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@types/node": "^22.13.4",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-prettier": "^5.2.6",
"prettier": "3.5.2",
"tsx": "^4.21.0",
"typescript": "^5.7.3"
}
}
15 changes: 15 additions & 0 deletions examples/typescript/clients/builder-code/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
"baseUrl": ".",
"types": ["node"]
},
"include": ["index.ts"]
}
3 changes: 3 additions & 0 deletions examples/typescript/facilitator/builder-code/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PORT=
EVM_PRIVATE_KEY=
SVM_PRIVATE_KEY=
Loading
Loading