@paynodelabs/paynode-402-cli 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # ⚔ paynode-402-cli
2
+
3
+ [![Base L2](https://img.shields.io/badge/Network-Base%20L2-0052FF?style=for-the-badge&logo=base)](https://base.org)
4
+ [![HTTP 402](https://img.shields.io/badge/Protocol-x402-00ff88?style=for-the-badge)](https://paynode.dev)
5
+
6
+ The official command-line interface for the [PayNode](https://github.com/PayNodeLabs) protocol. Designed specifically for the **Agentic Era**, allowing AI Agents to execute zero-install, stateless micro-payments via the HTTP 402 standard.
7
+
8
+ ---
9
+
10
+ ## šŸ› ļø Configuration & Credentials
11
+
12
+ To enable automated blockchain signing, this tool requires a private key provided via environment variables.
13
+
14
+ > [!IMPORTANT]
15
+ > **Mandatory Variable**: `CLIENT_PRIVATE_KEY` (0x...)
16
+ > All signature and payment operations rely on this key. Ensure it is set in your environment before execution.
17
+
18
+ > [!CAUTION]
19
+ > **BURNER WALLET ONLY**: NEVER use a primary or "cold storage" wallet. This tool is designed for autonomous agents; always use a dedicated burner wallet with minimal funds (< 10 USDC).
20
+
21
+ ### Setup Example (Unix)
22
+ ```bash
23
+ export CLIENT_PRIVATE_KEY="0x..."
24
+ bunx @paynodelabs/paynode-402-cli check
25
+ ```
26
+
27
+ ---
28
+
29
+ ## šŸ¤– For AI Agents (Zero-Install)
30
+
31
+ Your agent can use this tool dynamically via `bunx` without any manual installation:
32
+
33
+ ### 1. Check Readiness (Gas & USDC)
34
+ ```bash
35
+ bunx @paynodelabs/paynode-402-cli check --network testnet --json
36
+ ```
37
+
38
+ ### 2. Access a 402-Protected API
39
+ The CLI automatically detects the 402 challenge, performs the handshake, signs the payment (on-chain or EIP-3009), and returns the final resource.
40
+ ```bash
41
+ bunx @paynodelabs/paynode-402-cli request "https://api.example.com/data" --network testnet --json
42
+ ```
43
+
44
+ ### 3. Mint Test USDC (Base Sepolia)
45
+ ```bash
46
+ bunx @paynodelabs/paynode-402-cli mint --amount 100 --network testnet
47
+ ```
48
+
49
+ ---
50
+
51
+ ## šŸ“‘ Command Summary
52
+
53
+ | Command | Description |
54
+ | :--- | :--- |
55
+ | `check` | Check ETH/USDC balances and readiness on Base L2 |
56
+ | `mint` | Mint Mock USDC on Base Sepolia for testing |
57
+ | `request <URL>` | Access a protected resource by handling the 402 challenge |
58
+ | `list-paid-apis` | Discover payable APIs from the PayNode Marketplace |
59
+ | `get-api-detail <id>` | Inspect one marketplace API |
60
+ | `invoke-paid-api <id>` | Invoke a marketplace API using the 402 flow |
61
+
62
+ ### Global Flags
63
+ - `--network <name>`: `mainnet` or `testnet` (default: `testnet`).
64
+ - `--json`: Format output as machine-readable JSON (preferred for Agents).
65
+ - `--confirm-mainnet`: Explicit flag required for real USDC transactions on mainnet.
66
+ - `--background`: Execute in background and return a `task_id` for long-running handshakes.
67
+
68
+ ---
69
+
70
+ ## šŸ”— References
71
+ - **Marketplace**: [https://mk.paynode.dev](https://mk.paynode.dev)
72
+ - **Protocol SPEC**: [PayNode Docs](https://docs.paynode.dev)
73
+ - **GitHub**: [PayNodeLabs/paynode-402-cli](https://github.com/PayNodeLabs/paynode-402-cli)
@@ -0,0 +1,126 @@
1
+ import { ethers } from '@paynodelabs/sdk-js';
2
+ import {
3
+ getPrivateKey,
4
+ resolveNetwork,
5
+ requireMainnetConfirmation,
6
+ reportError,
7
+ jsonEnvelope,
8
+ withRetry,
9
+ EXIT_CODES,
10
+ BaseCliOptions
11
+ } from '../utils.ts';
12
+
13
+ interface CheckOptions extends BaseCliOptions {
14
+ }
15
+
16
+ // Minimum gas threshold: 0.001 ETH (in wei)
17
+ const MIN_GAS_WEI = ethers.parseEther('0.001');
18
+
19
+ export async function checkAction(options: CheckOptions) {
20
+ const isJson = !!options.json;
21
+ const pk = getPrivateKey(isJson);
22
+
23
+ try {
24
+ const { provider, usdcAddress, chainId, networkName, isSandbox } = await resolveNetwork(
25
+ options.rpc,
26
+ options.network,
27
+ options.rpcTimeout
28
+ );
29
+
30
+ // Mainnet safety gate
31
+ requireMainnetConfirmation(isSandbox, !!options.confirmMainnet, isJson);
32
+
33
+ const wallet = new ethers.Wallet(pk, provider);
34
+ const address = wallet.address;
35
+
36
+ // Fetch balances with retry
37
+ const [ethBalance, usdcBalance] = await withRetry(
38
+ () => Promise.all([
39
+ provider.getBalance(address),
40
+ new ethers.Contract(
41
+ usdcAddress,
42
+ ['function balanceOf(address) view returns (uint256)'],
43
+ provider
44
+ ).balanceOf(address)
45
+ ]),
46
+ 'balanceCheck'
47
+ );
48
+
49
+ // BigInt comparisons are used for logic (no precision loss).
50
+ // parseFloat is only for human-readable display and non-critical JSON values.
51
+ const ethValue = parseFloat(ethers.formatEther(ethBalance));
52
+ const usdcValue = parseFloat(ethers.formatUnits(usdcBalance, 6));
53
+ const isGasReady = ethBalance >= MIN_GAS_WEI;
54
+ const isTokensReady = usdcBalance > 0n;
55
+
56
+ if (isJson) {
57
+ console.log(
58
+ jsonEnvelope({
59
+ status: 'success',
60
+ address,
61
+ eth: ethValue,
62
+ usdc: usdcValue,
63
+ network: networkName,
64
+ chainId,
65
+ is_sandbox: isSandbox,
66
+ checks: {
67
+ gas_ready: isGasReady,
68
+ tokens_ready: isTokensReady,
69
+ can_pay: isGasReady && isTokensReady
70
+ }
71
+ })
72
+ );
73
+ } else {
74
+ console.log(`\nšŸ’Ž **PayNode Wallet Status**`);
75
+ console.log(`──────────────────────────────────────────────────`);
76
+ console.log(`šŸ‘¤ **Address**: \`${address}\``);
77
+ console.log(`🌐 **Network**: ${networkName}`);
78
+ console.log(`──────────────────────────────────────────────────`);
79
+ console.log(`⛽ **ETH (Gas)**: ${ethValue.toFixed(6)} ETH ${isGasReady ? 'āœ… Ready' : 'āš ļø Low balance'}`);
80
+ console.log(`šŸ’µ **USDC**: ${usdcValue.toFixed(2)} USDC ${isTokensReady ? 'āœ… Ready' : 'āŒ Empty'}`);
81
+ console.log(`──────────────────────────────────────────────────`);
82
+
83
+ if (isGasReady && isTokensReady) {
84
+ console.log(`šŸš€ **Status**: Ready to handle x402 autonomous payments.`);
85
+ } else {
86
+ console.log(`āŒ **Status**: Action required. See tips below.`);
87
+ }
88
+
89
+ // Safety Warning for Burner Wallets (Mainnet Only)
90
+ if (!isSandbox && usdcValue > 10) {
91
+ console.warn(
92
+ `\n> [!CAUTION]\n> High balance detected ($${usdcValue.toFixed(2)} USDC). This is a burner wallet. \n> Consider sweeping excess funds to cold storage to minimize risk.`
93
+ );
94
+ }
95
+
96
+ if (!isGasReady) {
97
+ if (isSandbox) {
98
+ console.warn(
99
+ `\n> [!WARNING]\n> Gas balance is critically low (< 0.001 ETH). Please deposit ETH to \`${address}\` on ${networkName}.`
100
+ );
101
+ console.warn(
102
+ `> **Faucet**: [console.optimism.io/faucet](https://console.optimism.io/faucet) (Recommended)`
103
+ );
104
+ } else {
105
+ console.warn(
106
+ `\n> [!WARNING]\n> Gas balance is critically low (< 0.001 ETH). Please deposit ETH to \`${address}\` on ${networkName}.`
107
+ );
108
+ }
109
+ }
110
+ if (!isTokensReady) {
111
+ if (isSandbox) {
112
+ console.warn(
113
+ `\n> [!TIP]\n> You're on Testnet! Run \`bun run paynode-402 mint --network testnet\` to get 1,000 free Test USDC.`
114
+ );
115
+ } else {
116
+ console.warn(
117
+ `\n> [!NOTE]\n> Mainnet USDC balance is 0. This wallet is currently restricted to free trials.`
118
+ );
119
+ }
120
+ }
121
+ console.log(``);
122
+ }
123
+ } catch (error: any) {
124
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
125
+ }
126
+ }
@@ -0,0 +1,30 @@
1
+ import { MarketplaceClient } from '../marketplace/client.ts';
2
+ import { jsonEnvelope, reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
3
+
4
+ interface GetApiDetailOptions extends BaseCliOptions {
5
+ }
6
+
7
+ export async function getApiDetailAction(apiId: string, options: GetApiDetailOptions) {
8
+ const isJson = !!options.json;
9
+
10
+ try {
11
+ const client = new MarketplaceClient({
12
+ baseUrl: options.marketUrl,
13
+ json: isJson
14
+ });
15
+
16
+ const detail = await client.getApiDetail(apiId, options.network);
17
+
18
+ if (isJson) {
19
+ console.log(jsonEnvelope({
20
+ status: 'success',
21
+ api: detail
22
+ }));
23
+ return;
24
+ }
25
+
26
+ console.log(JSON.stringify(detail, null, 2));
27
+ } catch (error: any) {
28
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
29
+ }
30
+ }
@@ -0,0 +1,91 @@
1
+ import { requestAction } from './request.ts';
2
+ import { MarketplaceClient } from '../marketplace/client.ts';
3
+ import { reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
4
+
5
+ interface InvokePaidApiOptions extends BaseCliOptions {
6
+ method?: string;
7
+ data?: string;
8
+ header?: string | string[];
9
+ background?: boolean;
10
+ output?: string;
11
+ maxAge?: number;
12
+ taskDir?: string;
13
+ taskId?: string;
14
+ }
15
+
16
+ function mergeHeaders(
17
+ marketplaceHeaders: Record<string, string> | undefined,
18
+ cliHeader: string | string[] | undefined
19
+ ): string[] {
20
+ const merged: string[] = [];
21
+
22
+ for (const [key, value] of Object.entries(marketplaceHeaders || {})) {
23
+ merged.push(`${key}: ${value}`);
24
+ }
25
+
26
+ if (Array.isArray(cliHeader)) {
27
+ merged.push(...cliHeader);
28
+ } else if (cliHeader) {
29
+ merged.push(cliHeader);
30
+ }
31
+
32
+ return merged;
33
+ }
34
+
35
+ function parsePayload(data?: string): any {
36
+ if (!data) return undefined;
37
+
38
+ try {
39
+ return JSON.parse(data);
40
+ } catch (err: any) {
41
+ const isJsonLike = data.trim().startsWith('{') || data.trim().startsWith('[');
42
+ if (isJsonLike) {
43
+ console.warn(`āš ļø [Warning] Invocation data looks like JSON but failed to parse: ${err.message}`);
44
+ console.warn(`Sending as raw string instead. Please verify your JSON syntax.`);
45
+ } else {
46
+ console.warn(`āš ļø [Warning] Invocation data is not valid JSON. Sending as raw string.`);
47
+ }
48
+ return data;
49
+ }
50
+ }
51
+
52
+ export async function invokePaidApiAction(apiId: string, options: InvokePaidApiOptions) {
53
+ const isJson = !!options.json;
54
+
55
+ try {
56
+ const client = new MarketplaceClient({
57
+ baseUrl: options.marketUrl,
58
+ json: isJson
59
+ });
60
+
61
+ const invoke = await client.prepareInvoke(apiId, {
62
+ network: options.network,
63
+ payload: parsePayload(options.data)
64
+ });
65
+
66
+ const requestHeaders = mergeHeaders(invoke.headers, options.header);
67
+ const hasPreparedBody = !!(invoke.body && typeof invoke.body === 'object' && Object.keys(invoke.body).length > 0);
68
+ const requestBody = hasPreparedBody
69
+ ? JSON.stringify(invoke.body)
70
+ : (options.data || undefined); // Use undefined if no data to trigger smart promotion if needed
71
+
72
+ await requestAction(invoke.invoke_url, [], {
73
+ json: options.json,
74
+ network: options.network || invoke.network, // Delegate fallback to resolveNetwork
75
+ rpc: options.rpc,
76
+ rpcTimeout: options.rpcTimeout,
77
+ confirmMainnet: options.confirmMainnet,
78
+ method: options.method || invoke.method || 'POST',
79
+ data: requestBody,
80
+ header: requestHeaders,
81
+ background: options.background,
82
+ dryRun: options.dryRun,
83
+ output: options.output,
84
+ maxAge: options.maxAge,
85
+ taskDir: options.taskDir,
86
+ taskId: options.taskId
87
+ });
88
+ } catch (error: any) {
89
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
90
+ }
91
+ }
@@ -0,0 +1,58 @@
1
+ import { MarketplaceClient } from '../marketplace/client.ts';
2
+ import { jsonEnvelope, reportError, EXIT_CODES, BaseCliOptions } from '../utils.ts';
3
+
4
+ interface ListPaidApisOptions extends BaseCliOptions {
5
+ limit?: string | number;
6
+ tag?: string | string[];
7
+ seller?: string;
8
+ }
9
+
10
+ export async function listPaidApisAction(options: ListPaidApisOptions) {
11
+ const isJson = !!options.json;
12
+
13
+ try {
14
+ const client = new MarketplaceClient({
15
+ baseUrl: options.marketUrl,
16
+ json: isJson
17
+ });
18
+
19
+ const tags = Array.isArray(options.tag)
20
+ ? options.tag
21
+ : options.tag
22
+ ? [options.tag]
23
+ : [];
24
+
25
+ const result = await client.listCatalog({
26
+ network: options.network,
27
+ limit: options.limit ? Number(options.limit) : undefined,
28
+ tag: tags,
29
+ seller: options.seller
30
+ });
31
+
32
+ if (isJson) {
33
+ console.log(jsonEnvelope({
34
+ status: 'success',
35
+ total: result.total || result.items.length,
36
+ items: result.items
37
+ }));
38
+ return;
39
+ }
40
+
41
+ if (result.items.length === 0) {
42
+ console.log('No paid APIs found.');
43
+ return;
44
+ }
45
+
46
+ for (const item of result.items) {
47
+ const price = item.price_per_call ? `${item.price_per_call} ${item.currency || 'USDC'}` : 'unpriced';
48
+ const network = item.network || 'unspecified';
49
+ const tagsLine = item.tags && item.tags.length > 0 ? ` [${item.tags.join(', ')}]` : '';
50
+ console.log(`- ${item.id}: ${item.name} | ${price} | ${network}${tagsLine}`);
51
+ if (item.description) {
52
+ console.log(` ${item.description}`);
53
+ }
54
+ }
55
+ } catch (error: any) {
56
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
57
+ }
58
+ }
@@ -0,0 +1,97 @@
1
+ import { ethers } from '@paynodelabs/sdk-js';
2
+ import {
3
+ getPrivateKey,
4
+ resolveNetwork,
5
+ reportError,
6
+ jsonEnvelope,
7
+ withRetry,
8
+ EXIT_CODES,
9
+ BaseCliOptions
10
+ } from '../utils.ts';
11
+
12
+ interface MintOptions extends BaseCliOptions {
13
+ amount?: string;
14
+ }
15
+
16
+ export async function mintAction(options: MintOptions) {
17
+ const isJson = !!options.json;
18
+ const pk = getPrivateKey(isJson);
19
+
20
+ try {
21
+ const { provider, usdcAddress, chainId, networkName, isSandbox } = await resolveNetwork(
22
+ options.rpc,
23
+ options.network || 'testnet',
24
+ options.rpcTimeout
25
+ );
26
+
27
+ if (!isSandbox) {
28
+ throw new Error(`Minting is only supported on Sepolia. Current ChainID: ${chainId}`);
29
+ }
30
+
31
+ const wallet = new ethers.Wallet(pk, provider);
32
+ const mintAmountStr = String(options.amount || '1000');
33
+
34
+ // Gas check
35
+ const balance = await provider.getBalance(wallet.address);
36
+ if (balance === 0n) {
37
+ reportError(
38
+ `Gas balance is 0 ETH on ${networkName}. Please fund \`${wallet.address}\` to continue.\n` +
39
+ `šŸ’” **Faucet**: [console.optimism.io/faucet](https://console.optimism.io/faucet) — 0.01 ETH daily (Recommended)`,
40
+ isJson,
41
+ EXIT_CODES.INSUFFICIENT_FUNDS
42
+ );
43
+ }
44
+
45
+ // Progress messages are sent to stderr to avoid polluting stdout
46
+ // when valid JSON output is expected by the caller (e.g. via --json)
47
+ if (!isJson) {
48
+ console.error(`šŸ’° Connecting to ${networkName}...`);
49
+ console.error(`šŸ”— Minting ${mintAmountStr} USDC for address: ${wallet.address}`);
50
+ }
51
+
52
+ const abi = ['function mint(address to, uint256 amount) external'];
53
+ const usdc = new ethers.Contract(usdcAddress, abi, wallet);
54
+
55
+ const amount = ethers.parseUnits(mintAmountStr, 6);
56
+
57
+ if (!isJson) console.error('ā³ Sending mint transaction...');
58
+ const tx = await withRetry(
59
+ () => usdc.mint(wallet.address, amount),
60
+ 'mint'
61
+ );
62
+
63
+ if (!isJson) console.error('ā³ Waiting for confirmation...');
64
+ const receipt: any = await withRetry(
65
+ () => tx.wait(),
66
+ 'mintConfirm'
67
+ );
68
+
69
+ if (!receipt || receipt.status !== 1) {
70
+ throw new Error('Transaction reverted or failed.');
71
+ }
72
+
73
+ if (isJson) {
74
+ console.log(
75
+ jsonEnvelope({
76
+ status: 'success',
77
+ txHash: tx.hash,
78
+ address: wallet.address,
79
+ amount: mintAmountStr,
80
+ token: 'MockUSDC',
81
+ network: networkName
82
+ })
83
+ );
84
+ } else {
85
+ console.log(`\nāœ… **Success!**`);
86
+ console.log(`──────────────────────────────────────────────────`);
87
+ console.log(`šŸ’° **Minted**: ${mintAmountStr} Test USDC`);
88
+ console.log(`🌐 **Network**: ${networkName}`);
89
+ console.log(`šŸ”— **Tx Hash**: \`${tx.hash}\``);
90
+ console.log(`──────────────────────────────────────────────────`);
91
+ console.log(`šŸš€ **Your wallet (\`${wallet.address}\`) is now funded and ready for testing.**`);
92
+ console.log(``);
93
+ }
94
+ } catch (error: any) {
95
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
96
+ }
97
+ }