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
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: cli-ci

on:
pull_request:
push:
branches: [main]
tags: ["v*", "test-v*"]
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20.11"
cache: npm

- name: Install dependencies
run: npm ci

- name: Test
run: npm test

- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high

- name: Verify packed CLI
run: npm run test:package
96 changes: 74 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
TypeScript command-line client for BankofAI x402 payments. This version uses
the npm TypeScript SDK packages only:

- `@bankofai/x402-core@1.0.0`
- `@bankofai/x402-evm@1.0.0`
- `@bankofai/x402-tron@1.0.0`
- `@bankofai/x402-core@1.0.1`
- `@bankofai/x402-evm@1.0.1`
- `@bankofai/x402-tron@1.0.1`

Stablecoin payments use `scheme=exact` with
`extra.assetTransferMethod=permit2`.
Stablecoin payments support `scheme=exact` and TRON `scheme=exact_gasfree`.
The GasFree flow lets the relayer pay network energy while deducting its fee
from the payment token, so the payer does not need TRX.

## Install

Install the CLI package:

```bash
npm install -g @bankofai/x402-cli@beta
x402-cli --version
```

For repository development:

```bash
npm install
npm run build
Expand All @@ -20,7 +30,7 @@ npm run build
Run from source during development:

```bash
npm run dev -- serve --pay-to <recipient> --amount 0.0001 --network tron:nile --token USDT
npm run dev -- serve --pay-to <recipient> --amount 0.0001 --network tron:0xcd8690dc --token USDT
```

Run the compiled CLI:
Expand Down Expand Up @@ -48,10 +58,10 @@ envelope with `ok`, `command`, `result`, or structured `error` fields.
Start a local x402 paywall endpoint:

```bash
node dist/cli.js serve \
x402-cli serve \
--pay-to <recipient> \
--amount 0.0001 \
--network tron:nile \
--network tron:0xcd8690dc \
--token USDT \
--port 4020
```
Expand All @@ -60,49 +70,90 @@ The server exposes:

- `GET /health`
- `GET /.well-known/x402`
- `GET /pay` returns `402 Payment Required`
- `POST /pay` verifies and settles with the facilitator
- `/pay` returns `402 Payment Required` without a payment signature
- The signed retry uses the same HTTP method, then verifies and settles with the facilitator

### Pay

Pay an x402-protected URL:

```bash
TRON_PRIVATE_KEY=<hex> \
node dist/cli.js pay http://127.0.0.1:4020/pay \
--network tron:nile \
x402-cli pay http://127.0.0.1:4020/pay \
--network tron:0xcd8690dc \
--token USDT
```

For automated or unfamiliar endpoints, set `--max-amount` or
`--max-raw-amount` before allowing the CLI to sign a payment.

Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
the server challenge):

```bash
TRON_PRIVATE_KEY=<hex> \
x402-cli pay https://api.example.com/pay \
--network tron:0xcd8690dc \
--token USDT \
--scheme exact_gasfree
```

Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
default relayer endpoint.

GasFree fees are separate from the advertised payment amount. Set a fee limit
so the CLI estimates the relayer fee and rejects the payment before signing if
the estimate is too high:

```bash
x402-cli pay https://api.example.com/pay \
--scheme exact_gasfree \
--max-amount 0.01 \
--max-gasfree-fee 0.5 \
--json
```

Use `--max-gasfree-fee-raw` to express the fee limit in the token's smallest
unit. Successful and failed paid responses distinguish `settled` (payment
completed) from `delivered` (HTTP business response succeeded). A settled
upstream failure has `paid=true`, `settled=true`, and `delivered=false` and
includes its transaction information.

For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
Prefer environment variables over `--private-key` in shared environments,
because command-line arguments may be visible to other local processes.

If the gateway settles a payment but the upstream request fails, JSON error
output includes `error.details.paymentResponse` for reconciliation. Do not retry
such a request blindly; inspect the transaction and provider behavior first.

### Roundtrip

Start a temporary local server and immediately pay it:

```bash
TRON_PRIVATE_KEY=<hex> \
node dist/cli.js roundtrip \
x402-cli roundtrip \
--pay-to <recipient> \
--amount 0.0001 \
--network tron:nile \
--network tron:0xcd8690dc \
--token USDT
```

## Networks

Supported built-in token registry:

- `tron:mainnet` USDT, USDD
- `tron:nile` USDT, USDD
- `tron:shasta` USDT
- `tron:0x2b6653dc` USDT, USDD
- `tron:0xcd8690dc` USDT, USDD
- `tron:0x94a9059e` USDT
- `eip155:56` USDT
- `eip155:97` USDT, USDC

Aliases accepted:
Non-CAIP TRON aliases are rejected. Use the canonical TRON IDs above.

EVM convenience aliases accepted:

- `tron-mainnet` -> `tron:mainnet`
- `tron-nile` -> `tron:nile`
- `bsc-mainnet` -> `eip155:56`
- `bsc-testnet` -> `eip155:97`

Expand All @@ -114,5 +165,6 @@ Pass a facilitator URL when needed:
x402-cli serve --facilitator-url https://facilitator.bankofai.io ...
```

CLI payment challenges and payload selection always emit `scheme: "exact"` for
the SDK 1.0 Permit2 path.
`serve --scheme exact_gasfree` advertises a TRON GasFree requirement. The
configured facilitator must advertise and settle `exact_gasfree` for that
network and token.
69 changes: 24 additions & 45 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bankofai/x402-cli",
"version": "1.0.0",
"version": "1.0.1-beta.8",
"type": "module",
"files": [
"dist"
Expand All @@ -13,17 +13,18 @@
"bundle:gateway": "node scripts/bundle-gateway.mjs",
"prepack": "npm run build && npm run bundle:gateway",
"test": "npm run build && node --test tests/*.test.mjs",
"test:package": "node scripts/test-package.mjs",
"start": "node dist/cli.js",
"dev": "tsx src/cli.ts"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"@bankofai/x402-core": "1.0.0",
"@bankofai/x402-evm": "1.0.0",
"@bankofai/x402-gateway": "^1.0.0",
"@bankofai/x402-tron": "1.0.0",
"@bankofai/x402-core": "1.0.1",
"@bankofai/x402-evm": "1.0.1",
"@bankofai/x402-gateway": "1.0.1-beta.7",
"@bankofai/x402-tron": "1.0.1",
"tronweb": "6.4.0",
"viem": "^2.55.0",
"yaml": "^2.8.2"
Expand All @@ -32,5 +33,8 @@
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"overrides": {
"ws": "8.21.0"
}
}
26 changes: 26 additions & 0 deletions scripts/test-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";

const root = path.resolve(import.meta.dirname, "..");
const temp = mkdtempSync(path.join(os.tmpdir(), "x402-cli-pack-"));
try {
const packOutput = execFileSync("npm", ["pack", "--json", "--silent"], { cwd: root, encoding: "utf8" });
const jsonStart = packOutput.indexOf("[\n");
assert.notEqual(jsonStart, -1, `npm pack did not return JSON: ${packOutput.slice(0, 200)}`);
const packed = JSON.parse(packOutput.slice(jsonStart));
const tarball = path.join(root, packed[0].filename);
execFileSync("npm", ["init", "-y"], { cwd: temp, stdio: "ignore" });
execFileSync("npm", ["install", "--ignore-scripts", tarball], { cwd: temp, stdio: "ignore" });
const cli = path.join(temp, "node_modules", ".bin", "x402-cli");
const version = execFileSync(cli, ["--version"], { encoding: "utf8" }).trim();
assert.match(version, /^1\.0\.1-/);
const gatewayHelp = execFileSync(cli, ["gateway", "--help"], { encoding: "utf8" });
assert.match(gatewayHelp, /gateway/iu);
rmSync(tarball, { force: true });
process.stdout.write(`verified packed CLI ${version}\n`);
} finally {
rmSync(temp, { recursive: true, force: true });
}
Loading
Loading