agentic-x402 0.2.33 → 0.3.1

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.
@@ -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
+ });
@@ -0,0 +1,94 @@
1
+ // CLI commands for the OpenClaw plugin
2
+ // Registered via api.registerCli with Commander.js-style program
3
+
4
+ import { getWalletAddress } from '../core/client.js';
5
+ import type { PluginLogger, OpenClawPluginApi } from './types.js';
6
+ import type { PaymentWatcher } from './watcher.js';
7
+
8
+ export function registerCliCommands(
9
+ api: OpenClawPluginApi,
10
+ watcher: PaymentWatcher | null,
11
+ logger: PluginLogger,
12
+ ): void {
13
+ api.registerCli(({ program }) => {
14
+ const x402 = program.command('x402').description('x402 payment tools');
15
+
16
+ x402
17
+ .command('watch')
18
+ .description('Start the payment watcher in foreground (for debugging)')
19
+ .option('--interval <ms>', 'Poll interval in milliseconds', '30000')
20
+ .action(async (opts: unknown) => {
21
+ const { interval: intervalStr } = opts as { interval: string };
22
+ if (watcher) {
23
+ console.log('Watcher is already running as a background service.');
24
+ const status = watcher.getStatus();
25
+ console.log(`Tracking ${status.trackedRouters.length} router(s)`);
26
+ console.log(`Payments detected: ${status.paymentsDetected}`);
27
+ return;
28
+ }
29
+
30
+ // Start a standalone watcher for debugging
31
+ const { PaymentWatcher: WatcherClass } = await import('./watcher.js');
32
+ const { applyConfigBridge } = await import('./config-bridge.js');
33
+
34
+ applyConfigBridge({});
35
+
36
+ const interval = parseInt(intervalStr, 10);
37
+
38
+ const debugWatcher = new WatcherClass({
39
+ logger,
40
+ gatewayPort: 0,
41
+ pollIntervalMs: interval,
42
+ notifyOnPayment: false,
43
+ });
44
+
45
+ console.log('Starting payment watcher in foreground...');
46
+ console.log('Press Ctrl+C to stop.\n');
47
+
48
+ process.on('SIGINT', async () => {
49
+ await debugWatcher.stop();
50
+ process.exit(0);
51
+ });
52
+
53
+ await debugWatcher.start();
54
+
55
+ // Keep alive
56
+ await new Promise(() => {});
57
+ });
58
+
59
+ x402
60
+ .command('status')
61
+ .description('Show payment watcher status, tracked routers, and payment count')
62
+ .action(() => {
63
+ if (!watcher) {
64
+ console.log('Payment watcher is not running.');
65
+ console.log('Enable it in your OpenClaw plugin config or start it with: openclaw x402 watch');
66
+ return;
67
+ }
68
+
69
+ const status = watcher.getStatus();
70
+ const address = getWalletAddress();
71
+
72
+ console.log('x402 Payment Watcher Status');
73
+ console.log('===========================\n');
74
+ console.log(`Running: ${status.running ? 'Yes' : 'No'}`);
75
+ console.log(`Poll interval: ${status.pollIntervalMs / 1000}s`);
76
+ console.log(`Payments detected: ${status.paymentsDetected}`);
77
+ console.log(`Last poll: ${status.lastPollAt ?? 'Never'}`);
78
+ console.log(`Wallet address: ${address}`);
79
+
80
+ if (status.trackedRouters.length === 0) {
81
+ console.log('\nNo routers tracked yet.');
82
+ } else {
83
+ console.log(`\nTracked Routers (${status.trackedRouters.length}):`);
84
+ for (const r of status.trackedRouters) {
85
+ console.log(` ${r.name}`);
86
+ console.log(` Address: ${r.address}`);
87
+ console.log(` Balance: ${r.balance} USDC`);
88
+ console.log(` Checked: ${r.lastChecked}`);
89
+ console.log('');
90
+ }
91
+ }
92
+ });
93
+ }, { commands: ['x402'] });
94
+ }
@@ -0,0 +1,31 @@
1
+ // Maps OpenClaw plugin config → process.env vars
2
+ // Only sets vars that are not already defined (env vars take priority)
3
+
4
+ import type { X402PluginConfig } from './types.js';
5
+
6
+ const CONFIG_MAP: Record<keyof Omit<X402PluginConfig, 'watcher'>, string> = {
7
+ evmPrivateKey: 'EVM_PRIVATE_KEY',
8
+ network: 'X402_NETWORK',
9
+ maxPaymentUsd: 'X402_MAX_PAYMENT_USD',
10
+ x402LinksApiUrl: 'X402_LINKS_API_URL',
11
+ };
12
+
13
+ /**
14
+ * Inject plugin config values into process.env if not already set.
15
+ *
16
+ * Priority chain:
17
+ * 1. Real env vars (highest — already in process.env)
18
+ * 2. Plugin config (injected here)
19
+ * 3. ~/.x402/.env (loaded by core/config.ts with override: false)
20
+ * 4. Hardcoded defaults in core/config.ts
21
+ */
22
+ export function applyConfigBridge(pluginConfig: Record<string, unknown>): void {
23
+ const config = pluginConfig as X402PluginConfig;
24
+
25
+ for (const [key, envVar] of Object.entries(CONFIG_MAP)) {
26
+ const value = config[key as keyof Omit<X402PluginConfig, 'watcher'>];
27
+ if (value !== undefined && value !== null && !process.env[envVar]) {
28
+ process.env[envVar] = String(value);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,49 @@
1
+ // OpenClaw Plugin Entry Point for agentic-x402
2
+
3
+ import type { OpenClawPluginApi, X402PluginConfig } from './types.js';
4
+ import { applyConfigBridge } from './config-bridge.js';
5
+ import { PaymentWatcher } from './watcher.js';
6
+ import { createTools } from './tools.js';
7
+ import { registerCliCommands } from './cli.js';
8
+
9
+ export default {
10
+ id: 'agentic-x402',
11
+ name: 'x402 Payments',
12
+
13
+ register(api: OpenClawPluginApi): void {
14
+ const { logger, config, gatewayPort } = api;
15
+ const pluginConfig = config as X402PluginConfig;
16
+
17
+ // 1. Apply config bridge: inject plugin config → process.env
18
+ applyConfigBridge(config);
19
+ logger.debug('x402 config bridge applied');
20
+
21
+ // 2. Register background watcher service (unless disabled)
22
+ let watcher: PaymentWatcher | null = null;
23
+ const watcherEnabled = pluginConfig.watcher?.enabled !== false;
24
+
25
+ if (watcherEnabled) {
26
+ watcher = new PaymentWatcher({
27
+ logger,
28
+ gatewayPort,
29
+ pollIntervalMs: pluginConfig.watcher?.pollIntervalMs,
30
+ notifyOnPayment: pluginConfig.watcher?.notifyOnPayment,
31
+ });
32
+ api.registerService(watcher);
33
+ logger.debug('x402 payment watcher registered');
34
+ } else {
35
+ logger.debug('x402 payment watcher disabled by config');
36
+ }
37
+
38
+ // 3. Register agent tools
39
+ const tools = createTools(watcher);
40
+ for (const tool of tools) {
41
+ api.registerTool(tool);
42
+ }
43
+ logger.debug(`Registered ${tools.length} x402 agent tools`);
44
+
45
+ // 4. Register CLI commands
46
+ registerCliCommands(api, watcher, logger);
47
+ logger.debug('x402 CLI commands registered');
48
+ },
49
+ };