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
296 changes: 260 additions & 36 deletions examples/typescript/pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions typescript/packages/x402-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
*.log
.DS_Store
126 changes: 126 additions & 0 deletions typescript/packages/x402-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# x402-cli

CLI for testing x402 payment endpoints.

## What it does

- Test x402 endpoints and make payments
- Discover available x402 endpoints
- Check payment requirements without paying
- Verify transactions on-chain

## Installation

```bash
npm install -g x402-cli
```

Or use directly with npx:

```bash
npx x402-cli <command>
```

## Quick Start

Check what an endpoint accepts:
```bash
x402 info https://api.example.com/resource
```

Find available x402 APIs:
```bash
x402 discover
```

Test paying for something:
```bash
x402 test https://api.example.com/resource --key YOUR_PRIVATE_KEY
```

## Commands

### `x402 test <url>`

Test an endpoint by making a payment.

Options:
- `-k, --key <privateKey>` - Private key for signing payments (or set X402_PRIVATE_KEY env var)
- `-a, --amount <amount>` - Override payment amount
- `-v, --verbose` - Show detailed payment flow

**Example:**
```bash
x402 test https://api.example.com/weather --verbose
```

### `x402 discover`

Find x402 endpoints.

Options:
- `-f, --filter <type>` - Filter by resource type
- `-l, --limit <number>` - Limit number of results (default: 20)

**Example:**
```bash
x402 discover --filter api --limit 10
```

### `x402 info <url>`

Get payment info without paying.

Options:
- `-v, --verbose` - Show full payment requirements JSON

**Example:**
```bash
x402 info https://api.example.com/premium --verbose
```

### `x402 verify <txHash>`

Check if a transaction was an x402 payment.

Options:
- `-n, --network <network>` - Network to check (default: base-sepolia)

**Example:**
```bash
x402 verify 0x1234... --network base-mainnet
```

## Configuration

Create a `.env` file in your working directory:

```bash
X402_PRIVATE_KEY=your_private_key_here
X402_FACILITATOR_URL=https://x402-facilitator.base.org
```

## Development

```bash
# Install dependencies
npm install

# Run in development mode
npm run dev test https://example.com

# Build
npm run build

# Test locally
npm link
x402 --help
```

## Contributing

PRs welcome. This is meant to make testing x402 endpoints easier.

## License

MIT
44 changes: 44 additions & 0 deletions typescript/packages/x402-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "x402-cli",
"version": "0.7.0",
"description": "CLI for testing x402 payment endpoints",
"main": "dist/index.js",
"bin": {
"x402": "./dist/cli.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/cli.ts",
"prepublishOnly": "npm run build"
},
"keywords": [
"x402",
"payments",
"cli",
"http",
"testing"
],
"author": "Coinbase Inc.",
"repository": "https://github.com/coinbase/x402",
"license": "Apache-2.0",
"dependencies": {
"commander": "^12.0.0",
"chalk": "^4.1.2",
"dotenv": "^16.4.0",
"axios": "^1.6.0",
"ora": "^5.4.1",
"prompts": "^2.4.2",
"x402": "^0.7.3",
"x402-axios": "^0.7.2",
"@coinbase/x402": "^0.7.3"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/prompts": "^2.4.9",
"typescript": "^5.3.0",
"tsx": "^4.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
43 changes: 43 additions & 0 deletions typescript/packages/x402-cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node

import { Command } from 'commander';
import { testEndpoint } from './commands/test';
import { discoverEndpoints } from './commands/discover';
import { getEndpointInfo } from './commands/info';
import { verifyTransaction } from './commands/verify';

const program = new Command();

program
.name('x402')
.description('CLI tool for testing and interacting with x402 payment endpoints')
.version('0.1.0');

program
.command('test <url>')
.description('Test an x402 endpoint by making a payment and receiving the resource')
.option('-k, --key <privateKey>', 'Private key for signing payments (or set X402_PRIVATE_KEY env var)')
.option('-a, --amount <amount>', 'Override payment amount')
.option('-v, --verbose', 'Show detailed payment flow')
.action(testEndpoint);

program
.command('discover')
.description('Discover available x402 endpoints in the network')
.option('-f, --filter <type>', 'Filter by resource type')
.option('-l, --limit <number>', 'Limit number of results', '20')
.action(discoverEndpoints);

program
.command('info <url>')
.description('Get payment requirements for an endpoint without paying')
.option('-v, --verbose', 'Show full payment requirements JSON')
.action(getEndpointInfo);

program
.command('verify <txHash>')
.description('Verify a transaction hash corresponds to an x402 payment')
.option('-n, --network <network>', 'Network to check (e.g., base-sepolia)', 'base-sepolia')
.action(verifyTransaction);

program.parse();
74 changes: 74 additions & 0 deletions typescript/packages/x402-cli/src/commands/discover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import axios from 'axios';
import { logger } from '../utils/logger';
import { getFacilitatorUrl } from '../utils/config';
import ora from 'ora';

interface DiscoverOptions {
filter?: string;
limit?: string;
}

export async function discoverEndpoints(options: DiscoverOptions) {
logger.header('Discovering x402 Endpoints');

const spinner = ora('Fetching available endpoints').start();

try {
const facilitatorUrl = getFacilitatorUrl();
const limit = parseInt(options.limit || '20');

// This endpoint structure is based on the x402 discovery example
const response = await axios.get(`${facilitatorUrl}/list`, {
params: {
limit,
...(options.filter && { type: options.filter })
}
});

spinner.succeed(`Found ${response.data.items?.length || 0} endpoints`);

if (!response.data.items || response.data.items.length === 0) {
logger.info('No endpoints found');
return;
}

logger.header('Available Endpoints');

response.data.items.forEach((item: any, index: number) => {
console.log(`\n${index + 1}. ${item.resource}`);
logger.keyValue('Type', item.type || 'unknown');
logger.keyValue('X402 Version', item.x402Version?.toString() || 'N/A');

if (item.metadata?.name) {
logger.keyValue('Name', item.metadata.name);
}

if (item.metadata?.description) {
logger.keyValue('Description', item.metadata.description);
}

if (item.accepts && item.accepts.length > 0) {
logger.keyValue('Accepts', `${item.accepts.length} payment option(s)`);
item.accepts.forEach((accept: any, i: number) => {
console.log(` ${i + 1}. ${accept.network} - ${accept.scheme}`);
});
}

if (item.lastUpdated) {
logger.keyValue('Last Updated', new Date(item.lastUpdated).toLocaleString());
}
});

logger.log(`\n${logger.info('Run')} x402 info <url> ${logger.info('to see payment details for an endpoint')}`);

} catch (error: any) {
spinner.fail('Discovery failed');
logger.error(error.message);

if (error.code === 'ECONNREFUSED') {
logger.warn('Could not connect to facilitator. Check your network or try a different facilitator URL.');
}

process.exit(1);
}
}
97 changes: 97 additions & 0 deletions typescript/packages/x402-cli/src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import axios from 'axios';
import { logger } from '../utils/logger';
import ora from 'ora';

interface InfoOptions {
verbose?: boolean;
}

export async function getEndpointInfo(url: string, options: InfoOptions) {
logger.header('x402 Endpoint Information');
logger.info(`URL: ${url}`);

const spinner = ora('Fetching payment requirements').start();

try {
const response = await axios.get(url, {
validateStatus: (status) => status === 402 || status === 200
});

if (response.status === 200) {
spinner.succeed('Endpoint is publicly accessible (no payment required)');
logger.success('This endpoint does not require payment');
return;
}

if (response.status !== 402) {
spinner.warn(`Unexpected status: ${response.status}`);
logger.warn('This endpoint may not be an x402 endpoint');
return;
}

spinner.succeed('Payment requirements retrieved');

const paymentData = response.data;

if (options.verbose) {
logger.header('Full Payment Requirements');
logger.json(paymentData);
return;
}

logger.header('Payment Requirements');
logger.keyValue('x402 Version', paymentData.x402Version?.toString() || 'N/A');

if (paymentData.error) {
logger.error(`Server Error: ${paymentData.error}`);
}

if (!paymentData.accepts || paymentData.accepts.length === 0) {
logger.warn('No payment options available');
return;
}

logger.log(`\nAccepts ${paymentData.accepts.length} payment option(s):\n`);

paymentData.accepts.forEach((requirement: any, index: number) => {
console.log(logger.step(`Option ${index + 1}`));
logger.keyValue(' Network', requirement.network);
logger.keyValue(' Scheme', requirement.scheme);
logger.keyValue(' Amount', requirement.maxAmountRequired);
logger.keyValue(' Asset', requirement.asset);
logger.keyValue(' Pay To', requirement.payTo);

if (requirement.description) {
logger.keyValue(' Description', requirement.description);
}

if (requirement.mimeType) {
logger.keyValue(' Response Type', requirement.mimeType);
}

if (requirement.maxTimeoutSeconds) {
logger.keyValue(' Max Timeout', `${requirement.maxTimeoutSeconds}s`);
}

console.log();
});

logger.info('Use --verbose flag to see full JSON');

} catch (error: any) {
spinner.fail('Request failed');
logger.error(error.message);

if (error.code === 'ENOTFOUND') {
logger.warn('Could not resolve hostname. Check the URL.');
} else if (error.code === 'ECONNREFUSED') {
logger.warn('Connection refused. Is the server running?');
}

if (options.verbose && error.response) {
logger.json(error.response.data);
}

process.exit(1);
}
}
Loading