agentic-x402 0.2.2 → 0.2.4

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/bin/cli.ts CHANGED
@@ -52,6 +52,16 @@ const commands: Record<string, Command> = {
52
52
  description: 'Get info about a payment link',
53
53
  category: 'links',
54
54
  },
55
+ routers: {
56
+ script: 'commands/routers.ts',
57
+ description: 'List routers where your wallet is a beneficiary',
58
+ category: 'links',
59
+ },
60
+ distribute: {
61
+ script: 'commands/distribute.ts',
62
+ description: 'Distribute USDC from a PaymentRouter',
63
+ category: 'links',
64
+ },
55
65
  };
56
66
 
57
67
  function showHelp() {
@@ -73,6 +83,8 @@ Payment Commands:
73
83
  Link Commands (21cash integration):
74
84
  create-link Create a payment link to sell content
75
85
  link-info <addr> Get info about a payment link
86
+ routers List routers where your wallet is a beneficiary
87
+ distribute <addr> Distribute USDC from a PaymentRouter
76
88
 
77
89
  Options:
78
90
  -h, --help Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-x402",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Agent skill for x402 payments - pay for and sell gated content",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,26 +60,22 @@ Shows:
60
60
  console.log('x402 Wallet Balance');
61
61
  console.log('===================');
62
62
  console.log('');
63
- console.log(`Address: ${fullAddress ? address : truncateAddress(address)}`);
64
- console.log(`Network: ${client.config.network} (chain ${client.config.chainId})`);
63
+ console.log(`Address: ${address}`);
64
+ const networkName = client.config.chainId === 8453 ? 'Base mainnet' : 'Base Sepolia';
65
+ console.log(`Network: ${networkName} (chain ${client.config.chainId})`);
65
66
  console.log('');
66
67
  console.log('Balances:');
67
68
  console.log(` USDC: ${formatCrypto(usdc.formatted, 'USDC', 2)}`);
68
69
  console.log(` ETH: ${formatCrypto(eth.formatted, 'ETH', 6)}`);
69
70
 
70
71
  // Warnings
71
- console.log('');
72
72
  const usdcNum = parseFloat(usdc.formatted);
73
- const ethNum = parseFloat(eth.formatted);
74
73
 
75
74
  if (usdcNum < 1) {
75
+ console.log('');
76
76
  console.log('Warning: Low USDC balance. Fund your wallet to make payments.');
77
77
  }
78
78
 
79
- if (ethNum < 0.0001) {
80
- console.log('Warning: Low ETH balance. You may need ETH for gas fees.');
81
- }
82
-
83
79
  } catch (error) {
84
80
  if (jsonOutput) {
85
81
  console.log(JSON.stringify({
@@ -56,7 +56,7 @@ Options:
56
56
  -h, --help Show this help
57
57
 
58
58
  Environment:
59
- X402_LINKS_API_URL Base URL of x402-links-server (required)
59
+ X402_LINKS_API_URL Base URL of x402-links-server (default: https://21.cash)
60
60
 
61
61
  Examples:
62
62
  x402 create-link --name "Premium Guide" --price 5.00 --url https://mysite.com/guide.pdf
@@ -68,12 +68,6 @@ Examples:
68
68
 
69
69
  const config = getConfig();
70
70
 
71
- if (!config.x402LinksApiUrl) {
72
- console.error('Error: X402_LINKS_API_URL environment variable is required');
73
- console.error('Set it to the base URL of your x402-links-server instance');
74
- process.exit(1);
75
- }
76
-
77
71
  const name = flags.name as string;
78
72
  const price = flags.price as string;
79
73
  const gatedUrl = flags.url as string | undefined;
@@ -129,7 +123,7 @@ Examples:
129
123
  console.log(` Name: ${name}`);
130
124
  console.log(` Price: ${formatUsd(parseFloat(price))}`);
131
125
  console.log(` Creator: ${truncateAddress(creatorAddress)}`);
132
- console.log(` Network: ${config.network} (chain ${config.chainId})`);
126
+ console.log(` Network: ${config.chainId === 8453 ? 'Base mainnet' : 'Base Sepolia'} (chain ${config.chainId})`);
133
127
  console.log('');
134
128
  }
135
129
 
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Distribute accumulated USDC from a PaymentRouter
3
+
4
+ import { getClient, getWalletAddress, getEthBalance } from '../core/client.js';
5
+ import { parseArgs, formatCrypto, truncateAddress, formatError } from '../core/utils.js';
6
+ import { getConfig, getUsdcAddress } from '../core/config.js';
7
+ import { formatUnits, parseUnits } from 'viem';
8
+
9
+ const ERC20_BALANCE_ABI = [{
10
+ name: 'balanceOf',
11
+ type: 'function',
12
+ stateMutability: 'view',
13
+ inputs: [{ name: 'account', type: 'address' }],
14
+ outputs: [{ name: '', type: 'uint256' }],
15
+ }] as const;
16
+
17
+ const PAYMENT_ROUTER_ABI = [{
18
+ name: 'distribute',
19
+ type: 'function',
20
+ stateMutability: 'nonpayable',
21
+ inputs: [
22
+ { name: 'token', type: 'address' },
23
+ { name: 'amount', type: 'uint256' },
24
+ ],
25
+ outputs: [],
26
+ }] as const;
27
+
28
+ const MIN_ETH_FOR_GAS = 0.0001; // Minimum ETH required for gas
29
+
30
+ async function main() {
31
+ const { positional, flags } = parseArgs(process.argv.slice(2));
32
+
33
+ if (positional.length === 0 || flags.help || flags.h) {
34
+ console.log(`
35
+ x402 distribute - Distribute USDC from a PaymentRouter
36
+
37
+ Usage: x402 distribute <router-address> [options]
38
+
39
+ Arguments:
40
+ router-address The PaymentRouter contract address
41
+
42
+ Options:
43
+ --amount <amt> Distribute a specific USDC amount (e.g., "10.00"). Defaults to full balance.
44
+ --force Skip gas balance warning
45
+ --json Output as JSON
46
+ -h, --help Show this help
47
+
48
+ Examples:
49
+ x402 distribute 0x1234...5678
50
+ x402 distribute 0x1234...5678 --amount 5.00
51
+ x402 distribute 0x1234...5678 --force --json
52
+ `);
53
+ process.exit(0);
54
+ }
55
+
56
+ const config = getConfig();
57
+ const jsonOutput = flags.json === true;
58
+ const force = flags.force === true;
59
+ const specifiedAmount = flags.amount as string | undefined;
60
+
61
+ // Validate router address
62
+ const routerAddress = positional[0];
63
+ if (!routerAddress.startsWith('0x') || routerAddress.length !== 42) {
64
+ const msg = 'Invalid router address. Must be a 0x-prefixed 40-character hex string.';
65
+ if (jsonOutput) {
66
+ console.log(JSON.stringify({ success: false, error: msg }));
67
+ } else {
68
+ console.error(`Error: ${msg}`);
69
+ }
70
+ process.exit(1);
71
+ }
72
+
73
+ try {
74
+ const client = getClient();
75
+ const walletAddress = getWalletAddress();
76
+ const usdcAddress = getUsdcAddress(config.chainId);
77
+ const routerAddr = routerAddress as `0x${string}`;
78
+
79
+ // Read router's USDC balance
80
+ const routerBalance = await client.publicClient.readContract({
81
+ address: usdcAddress,
82
+ abi: ERC20_BALANCE_ABI,
83
+ functionName: 'balanceOf',
84
+ args: [routerAddr],
85
+ });
86
+
87
+ const routerBalanceFormatted = formatUnits(routerBalance, 6);
88
+
89
+ if (routerBalance === 0n) {
90
+ if (jsonOutput) {
91
+ console.log(JSON.stringify({ success: false, error: 'Router has no USDC balance to distribute' }));
92
+ } else {
93
+ console.log('Router has no USDC balance to distribute.');
94
+ }
95
+ return;
96
+ }
97
+
98
+ // Determine amount to distribute
99
+ let distributeAmount: bigint;
100
+ if (specifiedAmount) {
101
+ distributeAmount = parseUnits(specifiedAmount, 6);
102
+ if (distributeAmount > routerBalance) {
103
+ const msg = `Requested amount (${specifiedAmount} USDC) exceeds router balance (${routerBalanceFormatted} USDC)`;
104
+ if (jsonOutput) {
105
+ console.log(JSON.stringify({ success: false, error: msg }));
106
+ } else {
107
+ console.error(`Error: ${msg}`);
108
+ }
109
+ process.exit(1);
110
+ }
111
+ } else {
112
+ distributeAmount = routerBalance;
113
+ }
114
+
115
+ // Check ETH for gas
116
+ if (!force) {
117
+ const eth = await getEthBalance();
118
+ const ethNum = parseFloat(eth.formatted);
119
+ if (ethNum < MIN_ETH_FOR_GAS) {
120
+ const msg = `Low ETH balance (${formatCrypto(eth.formatted, 'ETH', 6)}). Need at least ${MIN_ETH_FOR_GAS} ETH for gas. Use --force to skip this check.`;
121
+ if (jsonOutput) {
122
+ console.log(JSON.stringify({ success: false, error: msg }));
123
+ } else {
124
+ console.error(`Warning: ${msg}`);
125
+ }
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ const distributeFormatted = formatUnits(distributeAmount, 6);
131
+
132
+ if (!jsonOutput) {
133
+ console.log('Distributing USDC from PaymentRouter');
134
+ console.log('====================================');
135
+ console.log('');
136
+ console.log(`Router: ${routerAddress}`);
137
+ console.log(`Balance: ${formatCrypto(routerBalanceFormatted, 'USDC', 2)}`);
138
+ console.log(`Amount: ${formatCrypto(distributeFormatted, 'USDC', 2)}`);
139
+ console.log(`Caller: ${truncateAddress(walletAddress)}`);
140
+ console.log('');
141
+ console.log('Submitting transaction...');
142
+ }
143
+
144
+ // Call distribute(token, amount)
145
+ const txHash = await client.walletClient.writeContract({
146
+ address: routerAddr,
147
+ abi: PAYMENT_ROUTER_ABI,
148
+ functionName: 'distribute',
149
+ args: [usdcAddress, distributeAmount],
150
+ });
151
+
152
+ if (!jsonOutput) {
153
+ console.log(`TX submitted: ${txHash}`);
154
+ console.log('Waiting for confirmation...');
155
+ }
156
+
157
+ // Wait for receipt
158
+ const receipt = await client.publicClient.waitForTransactionReceipt({ hash: txHash });
159
+
160
+ const success = receipt.status === 'success';
161
+
162
+ if (jsonOutput) {
163
+ console.log(JSON.stringify({
164
+ success,
165
+ routerAddress,
166
+ amount: distributeFormatted,
167
+ amountRaw: distributeAmount.toString(),
168
+ transactionHash: txHash,
169
+ blockNumber: receipt.blockNumber.toString(),
170
+ status: receipt.status,
171
+ }));
172
+ return;
173
+ }
174
+
175
+ if (success) {
176
+ console.log('');
177
+ console.log('Distribution successful!');
178
+ console.log(`TX: ${txHash}`);
179
+ console.log(`Block: ${receipt.blockNumber}`);
180
+ console.log(`Distributed: ${formatCrypto(distributeFormatted, 'USDC', 2)}`);
181
+ } else {
182
+ console.error('');
183
+ console.error('Transaction reverted.');
184
+ console.error(`TX: ${txHash}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ } catch (error) {
189
+ if (jsonOutput) {
190
+ console.log(JSON.stringify({ success: false, error: formatError(error) }));
191
+ } else {
192
+ console.error('Error:', formatError(error));
193
+ }
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ main().catch(err => {
199
+ console.error('Error:', err.message);
200
+ process.exit(1);
201
+ });
@@ -50,7 +50,8 @@ Examples:
50
50
  console.log(`Fetching: ${url}`);
51
51
  console.log(`Method: ${method}`);
52
52
  console.log(`Wallet: ${truncateAddress(client.account.address)}`);
53
- console.log(`Network: ${client.config.network} (chain ${client.config.chainId})`);
53
+ const networkName = client.config.chainId === 8453 ? 'Base mainnet' : 'Base Sepolia';
54
+ console.log(`Network: ${networkName} (chain ${client.config.chainId})`);
54
55
  console.log('');
55
56
 
56
57
  try {
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env npx tsx
2
+ // List routers where the agent's wallet is a beneficiary
3
+
4
+ import { getClient, getWalletAddress } from '../core/client.js';
5
+ import { parseArgs, formatCrypto, formatUsd, truncateAddress, formatError } from '../core/utils.js';
6
+ import { getConfig, getUsdcAddress } from '../core/config.js';
7
+ import { formatUnits } from 'viem';
8
+
9
+ const ERC20_BALANCE_ABI = [{
10
+ name: 'balanceOf',
11
+ type: 'function',
12
+ stateMutability: 'view',
13
+ inputs: [{ name: 'account', type: 'address' }],
14
+ outputs: [{ name: '', type: 'uint256' }],
15
+ }] as const;
16
+
17
+ interface BeneficiaryLink {
18
+ id: string;
19
+ router_address: string;
20
+ metadata: {
21
+ name: string;
22
+ description?: string;
23
+ chainId?: number;
24
+ chainName?: string;
25
+ };
26
+ amount: string;
27
+ chain_id: number;
28
+ token_address: string;
29
+ beneficiary_percentage: number;
30
+ created_at: string;
31
+ }
32
+
33
+ async function main() {
34
+ const { flags } = parseArgs(process.argv.slice(2));
35
+
36
+ if (flags.help || flags.h) {
37
+ console.log(`
38
+ x402 routers - List routers where your wallet is a beneficiary
39
+
40
+ Usage: x402 routers [options]
41
+
42
+ Options:
43
+ --with-balance Fetch on-chain USDC balance for each router
44
+ --json Output as JSON
45
+ -h, --help Show this help
46
+
47
+ Shows:
48
+ - Router address and link name
49
+ - Your beneficiary share percentage
50
+ - On-chain USDC balance (with --with-balance)
51
+ - Estimated withdrawal amount based on your share
52
+ `);
53
+ process.exit(0);
54
+ }
55
+
56
+ const config = getConfig();
57
+ const jsonOutput = flags.json === true;
58
+ const withBalance = flags['with-balance'] === true;
59
+
60
+ try {
61
+ const address = getWalletAddress();
62
+ const apiUrl = `${config.x402LinksApiUrl}/api/links/beneficiary/${address}`;
63
+
64
+ const response = await fetch(apiUrl);
65
+ const data = await response.json();
66
+
67
+ if (!response.ok || !data.success) {
68
+ if (jsonOutput) {
69
+ console.log(JSON.stringify({ success: false, error: data.error || 'Failed to fetch routers' }));
70
+ } else {
71
+ console.error('Error:', data.error || 'Failed to fetch routers');
72
+ }
73
+ process.exit(1);
74
+ }
75
+
76
+ const links: BeneficiaryLink[] = data.links;
77
+
78
+ if (links.length === 0) {
79
+ if (jsonOutput) {
80
+ console.log(JSON.stringify({ success: true, routers: [] }));
81
+ } else {
82
+ console.log('No routers found where your wallet is a beneficiary.');
83
+ }
84
+ return;
85
+ }
86
+
87
+ // Optionally fetch on-chain balances
88
+ let balances: Map<string, { raw: bigint; formatted: string }> | null = null;
89
+
90
+ if (withBalance) {
91
+ const client = getClient();
92
+ const usdcAddress = getUsdcAddress(config.chainId);
93
+ balances = new Map();
94
+
95
+ const balancePromises = links.map(async (link) => {
96
+ const routerAddr = link.router_address as `0x${string}`;
97
+ try {
98
+ const balance = await client.publicClient.readContract({
99
+ address: usdcAddress,
100
+ abi: ERC20_BALANCE_ABI,
101
+ functionName: 'balanceOf',
102
+ args: [routerAddr],
103
+ });
104
+ return { router: routerAddr, raw: balance, formatted: formatUnits(balance, 6) };
105
+ } catch {
106
+ return { router: routerAddr, raw: 0n, formatted: '0' };
107
+ }
108
+ });
109
+
110
+ const results = await Promise.all(balancePromises);
111
+ for (const r of results) {
112
+ balances.set(r.router.toLowerCase(), { raw: r.raw, formatted: r.formatted });
113
+ }
114
+ }
115
+
116
+ if (jsonOutput) {
117
+ const routers = links.map((link) => {
118
+ const bal = balances?.get(link.router_address.toLowerCase());
119
+ const share = link.beneficiary_percentage / 100;
120
+ return {
121
+ routerAddress: link.router_address,
122
+ name: link.metadata?.name || 'Unnamed',
123
+ chainId: link.chain_id,
124
+ sharePercent: link.beneficiary_percentage,
125
+ ...(bal ? {
126
+ balance: bal.formatted,
127
+ balanceRaw: bal.raw.toString(),
128
+ estimatedWithdrawal: (parseFloat(bal.formatted) * share).toFixed(6),
129
+ } : {}),
130
+ createdAt: link.created_at,
131
+ };
132
+ });
133
+ console.log(JSON.stringify({ success: true, address, routers }));
134
+ return;
135
+ }
136
+
137
+ // Human-readable output
138
+ console.log('Your Payment Routers');
139
+ console.log('====================');
140
+ console.log('');
141
+ console.log(`Wallet: ${address}`);
142
+ console.log(`Found: ${links.length} router(s)`);
143
+ console.log('');
144
+
145
+ for (const link of links) {
146
+ const name = link.metadata?.name || 'Unnamed';
147
+ const share = link.beneficiary_percentage;
148
+
149
+ console.log(` ${name}`);
150
+ console.log(` Router: ${link.router_address}`);
151
+ console.log(` Share: ${share}%`);
152
+
153
+ if (balances) {
154
+ const bal = balances.get(link.router_address.toLowerCase());
155
+ if (bal) {
156
+ const balNum = parseFloat(bal.formatted);
157
+ const est = balNum * (share / 100);
158
+ console.log(` Balance: ${formatCrypto(bal.formatted, 'USDC', 2)}`);
159
+ console.log(` Est. withdrawal: ${formatCrypto(est.toFixed(2), 'USDC', 2)}`);
160
+ }
161
+ }
162
+ console.log('');
163
+ }
164
+
165
+ if (!withBalance) {
166
+ console.log('Tip: Use --with-balance to see on-chain USDC balances');
167
+ }
168
+ console.log('Use "x402 distribute <router-address>" to withdraw funds');
169
+
170
+ } catch (error) {
171
+ if (jsonOutput) {
172
+ console.log(JSON.stringify({ success: false, error: formatError(error) }));
173
+ } else {
174
+ console.error('Error:', formatError(error));
175
+ }
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ main().catch(err => {
181
+ console.error('Error:', err.message);
182
+ process.exit(1);
183
+ });
@@ -71,7 +71,7 @@ export function getConfig(): X402Config {
71
71
  network,
72
72
  chainId,
73
73
  facilitatorUrl: getOptionalEnv('X402_FACILITATOR_URL', defaultFacilitator),
74
- x402LinksApiUrl: process.env.X402_LINKS_API_URL,
74
+ x402LinksApiUrl: process.env.X402_LINKS_API_URL || 'https://21.cash',
75
75
  maxPaymentUsd: parseFloat(getOptionalEnv('X402_MAX_PAYMENT_USD', '10')),
76
76
  slippageBps: parseInt(getOptionalEnv('X402_SLIPPAGE_BPS', '50'), 10),
77
77
  verbose: getOptionalEnv('X402_VERBOSE', '0') === '1',