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
5 changes: 5 additions & 0 deletions examples/typescript/clients/allowance/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RESOURCE_SERVER_URL=http://localhost:4025
ENDPOINT_PATH=/demo
PRIVATE_KEY=
TOKEN_ADDRESS=
FACILITATOR_ADDRESS=
8 changes: 8 additions & 0 deletions examples/typescript/clients/allowance/.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/allowance/.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"
}
41 changes: 41 additions & 0 deletions examples/typescript/clients/allowance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Allowance Based Client Example

This example demonstrates how to pre-approve a payment facilitator using the ERC-20 `approve` function so that future x402 payments can be automatically charged using `transferFrom` without additional user signatures.

The demo pairs with the example server in `../servers/allowance` which expects a simple JSON payment header containing the sender address and amount.

## Prerequisites

- Node.js v20+
- pnpm v10
- A running allowance example server (`examples/typescript/servers/allowance`)
- A funded Ethereum private key on Base Sepolia

## Setup

1. Install dependencies from the examples root and build the packages:

```bash
cd ../../
pnpm install
pnpm build
cd clients/allowance
```

2. Copy `.env-local` to `.env` and fill in the required values:

```bash
cp .env-local .env
```

3. Approve the facilitator address to spend your USDC:

```bash
pnpm approve
```

4. Run the demo client:

```bash
pnpm dev
```
40 changes: 40 additions & 0 deletions examples/typescript/clients/allowance/approve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { config } from "dotenv";
import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
import { Hex, Address } from "viem";
import { erc20Abi } from "viem";

config();

const { PRIVATE_KEY, TOKEN_ADDRESS, FACILITATOR_ADDRESS } = process.env;

if (!PRIVATE_KEY || !TOKEN_ADDRESS || !FACILITATOR_ADDRESS) {
console.error("Missing environment variables in .env");
process.exit(1);
}

const account = privateKeyToAccount(PRIVATE_KEY as Hex);

const wallet = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
}).extend(publicActions);

/**
* Approves the facilitator to spend USDC on behalf of the user.
*/
async function main() {
const maxUint256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
const tx = await wallet.writeContract({
address: TOKEN_ADDRESS as Address,
abi: erc20Abi,
functionName: "approve",
args: [FACILITATOR_ADDRESS as Address, maxUint256],
});
await wallet.waitForTransactionReceipt({ hash: tx });
console.log(`Approved ${FACILITATOR_ADDRESS}`);
}

main();
72 changes: 72 additions & 0 deletions examples/typescript/clients/allowance/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"],
},
},
];
82 changes: 82 additions & 0 deletions examples/typescript/clients/allowance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { config } from "dotenv";
import axios from "axios";
import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { Hex, Address } from "viem";
import { baseSepolia } from "viem/chains";
import { erc20Abi } from "viem";

config();

const { RESOURCE_SERVER_URL, ENDPOINT_PATH, PRIVATE_KEY, TOKEN_ADDRESS, FACILITATOR_ADDRESS } =
process.env;

if (
!RESOURCE_SERVER_URL ||
!ENDPOINT_PATH ||
!PRIVATE_KEY ||
!TOKEN_ADDRESS ||
!FACILITATOR_ADDRESS
) {
console.error("Missing environment variables in .env");
process.exit(1);
}

const account = privateKeyToAccount(PRIVATE_KEY as Hex);
const wallet = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
}).extend(publicActions);

const api = axios.create({ baseURL: RESOURCE_SERVER_URL });

api.interceptors.response.use(
response => response,
async error => {
if (!error.response || error.response.status !== 402) {
return Promise.reject(error);
}

const { accepts } = error.response.data as {
accepts: Array<{
maxAmountRequired: string;
scheme: string;
}>;
};
const req = accepts[0];
const amount = BigInt(req.maxAmountRequired);

const allowance: bigint = await wallet.readContract({
address: TOKEN_ADDRESS as Address,
abi: erc20Abi,
functionName: "allowance",
args: [account.address as Address, FACILITATOR_ADDRESS as Address],
});

if (allowance < amount) {
throw new Error("Insufficient allowance, please run pnpm approve first.");
}

const paymentHeader = JSON.stringify({
from: account.address,
amount: req.maxAmountRequired,
});

error.config.headers = error.config.headers || {};
error.config.headers["X-PAYMENT"] = paymentHeader;
error.config.headers["Access-Control-Expose-Headers"] = "X-PAYMENT-RESPONSE";

return api.request(error.config);
},
);

api
.get(ENDPOINT_PATH)
.then(res => {
console.log(res.data);
console.log(res.headers["x-payment-response"]);
})
.catch(err => {
console.error(err.message);
});
30 changes: 30 additions & 0 deletions examples/typescript/clients/allowance/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "allowance-client-example",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx index.ts",
"approve": "tsx approve.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": {
"axios": "^1.7.9",
"dotenv": "^16.5.0",
"viem": "^2.23.1"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@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.7.0",
"typescript": "^5.3.0"
}
}
15 changes: 15 additions & 0 deletions examples/typescript/clients/allowance/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", "approve.ts"]
}
3 changes: 3 additions & 0 deletions examples/typescript/servers/allowance/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PRIVATE_KEY=
PAY_TO=
TOKEN_ADDRESS=
8 changes: 8 additions & 0 deletions examples/typescript/servers/allowance/.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/servers/allowance/.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"
}
21 changes: 21 additions & 0 deletions examples/typescript/servers/allowance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Allowance Based Server Example

This server works with the allowance client example. It requires callers to pre-approve a facilitator address for USDC transfers. Clients send a simple JSON payment header containing the sender address and amount. The server verifies the allowance and pulls payment using `transferFrom`.

## Setup

1. Copy `.env-local` to `.env` and configure the addresses.
2. Install dependencies from the examples root and build the packages:

```bash
cd ../../
pnpm install
pnpm build
cd servers/allowance
```

3. Start the server:

```bash
pnpm dev
```
Loading