agentic-x402 0.2.33 → 0.3.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.
@@ -0,0 +1,453 @@
1
+ // 8 Agent Tools for the OpenClaw plugin
2
+ // Each wraps existing command logic with dynamic imports to avoid loading viem at registration
3
+
4
+ import type { PluginTool } from './types.js';
5
+ import type { PaymentWatcher } from './watcher.js';
6
+
7
+ export function createTools(watcher: PaymentWatcher | null): PluginTool[] {
8
+ return [
9
+ // 1. x402_balance
10
+ {
11
+ name: 'x402_balance',
12
+ description: 'Check wallet USDC and ETH balances on Base',
13
+ inputSchema: { type: 'object', properties: {}, required: [] },
14
+ async execute() {
15
+ const { getClient, getWalletAddress, getUsdcBalance, getEthBalance } = await import('../core/client.js');
16
+ const client = getClient();
17
+ const address = getWalletAddress();
18
+ const [usdc, eth] = await Promise.all([getUsdcBalance(), getEthBalance()]);
19
+
20
+ return {
21
+ address,
22
+ network: client.config.network,
23
+ chainId: client.config.chainId,
24
+ balances: {
25
+ usdc: { raw: usdc.raw.toString(), formatted: usdc.formatted },
26
+ eth: { raw: eth.raw.toString(), formatted: eth.formatted },
27
+ },
28
+ };
29
+ },
30
+ },
31
+
32
+ // 2. x402_pay
33
+ {
34
+ name: 'x402_pay',
35
+ description: 'Pay for an x402-gated resource. Returns the response body after payment.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ url: { type: 'string', description: 'URL of the x402-gated resource' },
40
+ method: { type: 'string', description: 'HTTP method (default: GET)' },
41
+ body: { type: 'string', description: 'Request body for POST/PUT' },
42
+ maxPaymentUsd: { type: 'number', description: 'Maximum payment in USD' },
43
+ dryRun: { type: 'boolean', description: 'If true, show payment details without paying' },
44
+ },
45
+ required: ['url'],
46
+ },
47
+ async execute(params) {
48
+ const url = params.url as string;
49
+ const method = (params.method as string) || 'GET';
50
+ const body = params.body as string | undefined;
51
+ const maxPaymentUsd = params.maxPaymentUsd as number | undefined;
52
+ const dryRun = params.dryRun as boolean | undefined;
53
+
54
+ const { getClient } = await import('../core/client.js');
55
+ const client = getClient();
56
+
57
+ const headers: Record<string, string> = { Accept: 'application/json' };
58
+ if (body) headers['Content-Type'] = 'application/json';
59
+
60
+ // Probe for 402
61
+ const probe = await fetch(url, { method, body, headers });
62
+
63
+ if (probe.status !== 402) {
64
+ const contentType = probe.headers.get('content-type');
65
+ const responseBody = contentType?.includes('json')
66
+ ? await probe.json()
67
+ : await probe.text();
68
+ return { paid: false, status: probe.status, response: responseBody };
69
+ }
70
+
71
+ // Parse payment info
72
+ let paymentInfo: Record<string, unknown> | null = null;
73
+ const xPayment = probe.headers.get('x-payment');
74
+ if (xPayment) {
75
+ try { paymentInfo = JSON.parse(Buffer.from(xPayment, 'base64').toString()); } catch { /* */ }
76
+ }
77
+ if (!paymentInfo) {
78
+ try { paymentInfo = await probe.json() as Record<string, unknown>; } catch { /* */ }
79
+ }
80
+
81
+ const accepts = (paymentInfo as Record<string, unknown>)?.accepts as Array<Record<string, unknown>> | undefined;
82
+ const priceStr = String(accepts?.[0]?.price ?? accepts?.[0]?.maxAmountRequired ?? '0');
83
+ const priceNum = parseFloat(priceStr.replace(/[$,]/g, ''));
84
+ const effectiveMax = maxPaymentUsd ?? client.config.maxPaymentUsd;
85
+
86
+ if (priceNum > effectiveMax) {
87
+ return { paid: false, error: `Price $${priceNum} exceeds max $${effectiveMax}`, paymentInfo };
88
+ }
89
+
90
+ if (dryRun) {
91
+ return { paid: false, dryRun: true, price: priceNum, paymentInfo };
92
+ }
93
+
94
+ // Execute payment
95
+ const response = await client.fetchWithPayment(url, { method, body, headers });
96
+
97
+ if (!response.ok) {
98
+ return { paid: false, error: `${response.status} ${response.statusText}` };
99
+ }
100
+
101
+ const contentType = response.headers.get('content-type');
102
+ const responseBody = contentType?.includes('json')
103
+ ? await response.json()
104
+ : await response.text();
105
+
106
+ let txHash: string | undefined;
107
+ const paymentResponse = response.headers.get('x-payment-response');
108
+ if (paymentResponse) {
109
+ try {
110
+ const receipt = JSON.parse(Buffer.from(paymentResponse, 'base64').toString());
111
+ txHash = receipt.transactionHash ?? receipt.txHash;
112
+ } catch { /* */ }
113
+ }
114
+
115
+ return { paid: true, price: priceNum, transactionHash: txHash, response: responseBody };
116
+ },
117
+ },
118
+
119
+ // 3. x402_fetch
120
+ {
121
+ name: 'x402_fetch',
122
+ description: 'Fetch a URL with automatic x402 payment handling. Simpler than x402_pay — just returns the content.',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ url: { type: 'string', description: 'URL to fetch' },
127
+ method: { type: 'string', description: 'HTTP method (default: GET)' },
128
+ body: { type: 'string', description: 'Request body for POST/PUT' },
129
+ },
130
+ required: ['url'],
131
+ },
132
+ async execute(params) {
133
+ const url = params.url as string;
134
+ const method = (params.method as string) || 'GET';
135
+ const body = params.body as string | undefined;
136
+
137
+ const { getClient } = await import('../core/client.js');
138
+ const client = getClient();
139
+
140
+ const headers: Record<string, string> = { Accept: 'application/json' };
141
+ if (body) headers['Content-Type'] = 'application/json';
142
+
143
+ const response = await client.fetchWithPayment(url, { method, body, headers });
144
+
145
+ if (!response.ok) {
146
+ return { success: false, status: response.status, error: response.statusText };
147
+ }
148
+
149
+ const contentType = response.headers.get('content-type');
150
+ const responseBody = contentType?.includes('json')
151
+ ? await response.json()
152
+ : await response.text();
153
+
154
+ return { success: true, status: response.status, response: responseBody };
155
+ },
156
+ },
157
+
158
+ // 4. x402_create_link
159
+ {
160
+ name: 'x402_create_link',
161
+ description: 'Create a payment link via 21.cash to sell gated content',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ name: { type: 'string', description: 'Name of the payment link' },
166
+ price: { type: 'string', description: 'Price in USD (e.g., "5.00")' },
167
+ url: { type: 'string', description: 'URL to gate behind payment' },
168
+ text: { type: 'string', description: 'Text content to gate behind payment' },
169
+ description: { type: 'string', description: 'Description of the link' },
170
+ webhookUrl: { type: 'string', description: 'Webhook URL for payment notifications' },
171
+ },
172
+ required: ['name', 'price'],
173
+ },
174
+ async execute(params) {
175
+ const name = params.name as string;
176
+ const price = params.price as string;
177
+ const gatedUrl = params.url as string | undefined;
178
+ const gatedText = params.text as string | undefined;
179
+ const description = params.description as string | undefined;
180
+ const webhookUrl = params.webhookUrl as string | undefined;
181
+
182
+ if (!gatedUrl && !gatedText) {
183
+ return { success: false, error: 'Either url or text is required' };
184
+ }
185
+
186
+ const { getClient, getWalletAddress } = await import('../core/client.js');
187
+ const { getConfig } = await import('../core/config.js');
188
+
189
+ const config = getConfig();
190
+ const client = getClient();
191
+ const creatorAddress = getWalletAddress();
192
+
193
+ const requestBody: Record<string, unknown> = {
194
+ name,
195
+ price,
196
+ creatorAddress,
197
+ chainId: config.chainId,
198
+ };
199
+ if (description) requestBody.description = description;
200
+ if (gatedUrl) requestBody.gatedUrl = gatedUrl;
201
+ if (gatedText) requestBody.gatedText = gatedText;
202
+ if (webhookUrl) requestBody.webhookUrl = webhookUrl;
203
+
204
+ const apiUrl = `${config.x402LinksApiUrl}/api/links/programmatic`;
205
+ const response = await client.fetchWithPayment(apiUrl, {
206
+ method: 'POST',
207
+ headers: { 'Content-Type': 'application/json' },
208
+ body: JSON.stringify(requestBody),
209
+ });
210
+
211
+ const data = await response.json();
212
+ if (!response.ok || !(data as Record<string, unknown>).success) {
213
+ return { success: false, error: (data as Record<string, unknown>).error ?? 'Unknown error' };
214
+ }
215
+
216
+ return data;
217
+ },
218
+ },
219
+
220
+ // 5. x402_link_info
221
+ {
222
+ name: 'x402_link_info',
223
+ description: 'Get details about a payment link by router address',
224
+ inputSchema: {
225
+ type: 'object',
226
+ properties: {
227
+ routerAddress: { type: 'string', description: 'Router contract address or payment URL' },
228
+ },
229
+ required: ['routerAddress'],
230
+ },
231
+ async execute(params) {
232
+ let routerAddress = params.routerAddress as string;
233
+
234
+ // Extract address from URL if needed
235
+ if (routerAddress.startsWith('http')) {
236
+ const url = new URL(routerAddress);
237
+ const parts = url.pathname.split('/');
238
+ routerAddress = parts[parts.length - 1];
239
+ }
240
+
241
+ if (!routerAddress.startsWith('0x') || routerAddress.length !== 42) {
242
+ return { success: false, error: 'Invalid router address' };
243
+ }
244
+
245
+ const { getConfig } = await import('../core/config.js');
246
+ const config = getConfig();
247
+
248
+ const apiUrl = `${config.x402LinksApiUrl}/api/links/${routerAddress}/details`;
249
+ const response = await fetch(apiUrl);
250
+ const data = await response.json();
251
+
252
+ if (!response.ok || !(data as Record<string, unknown>).success) {
253
+ return { success: false, error: (data as Record<string, unknown>).error ?? 'Link not found' };
254
+ }
255
+
256
+ return data;
257
+ },
258
+ },
259
+
260
+ // 6. x402_routers
261
+ {
262
+ name: 'x402_routers',
263
+ description: 'List payment routers where your wallet is a beneficiary',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ withBalances: { type: 'boolean', description: 'Fetch on-chain USDC balance for each router' },
268
+ },
269
+ },
270
+ async execute(params) {
271
+ const withBalances = params.withBalances as boolean | undefined;
272
+
273
+ const { getWalletAddress } = await import('../core/client.js');
274
+ const { getConfig, getUsdcAddress } = await import('../core/config.js');
275
+
276
+ const config = getConfig();
277
+ const address = getWalletAddress();
278
+
279
+ const apiUrl = `${config.x402LinksApiUrl}/api/links/beneficiary/${address}`;
280
+ const response = await fetch(apiUrl);
281
+ const data = await response.json() as { success: boolean; links?: Array<Record<string, unknown>>; error?: string };
282
+
283
+ if (!response.ok || !data.success) {
284
+ return { success: false, error: data.error ?? 'Failed to fetch routers' };
285
+ }
286
+
287
+ const links = data.links ?? [];
288
+
289
+ if (!withBalances) {
290
+ return {
291
+ success: true,
292
+ address,
293
+ routers: links.map((l) => ({
294
+ routerAddress: l.router_address,
295
+ name: (l.metadata as Record<string, unknown>)?.name ?? 'Unnamed',
296
+ chainId: l.chain_id,
297
+ sharePercent: l.beneficiary_percentage,
298
+ createdAt: l.created_at,
299
+ })),
300
+ };
301
+ }
302
+
303
+ // Fetch balances
304
+ const { getClient } = await import('../core/client.js');
305
+ const { formatUnits } = await import('viem');
306
+ const client = getClient();
307
+ const usdcAddress = getUsdcAddress(config.chainId);
308
+
309
+ const ERC20_ABI = [{
310
+ name: 'balanceOf' as const,
311
+ type: 'function' as const,
312
+ stateMutability: 'view' as const,
313
+ inputs: [{ name: 'account', type: 'address' }],
314
+ outputs: [{ name: '', type: 'uint256' }],
315
+ }] as const;
316
+
317
+ const routers = await Promise.all(links.map(async (l) => {
318
+ const routerAddr = l.router_address as `0x${string}`;
319
+ const share = (l.beneficiary_percentage as number) / 100;
320
+ let balance = '0';
321
+ try {
322
+ const bal = await client.publicClient.readContract({
323
+ address: usdcAddress,
324
+ abi: ERC20_ABI,
325
+ functionName: 'balanceOf',
326
+ args: [routerAddr],
327
+ });
328
+ balance = formatUnits(bal, 6);
329
+ } catch { /* */ }
330
+
331
+ return {
332
+ routerAddress: l.router_address,
333
+ name: (l.metadata as Record<string, unknown>)?.name ?? 'Unnamed',
334
+ chainId: l.chain_id,
335
+ sharePercent: l.beneficiary_percentage,
336
+ balance,
337
+ estimatedWithdrawal: (parseFloat(balance) * share).toFixed(6),
338
+ createdAt: l.created_at,
339
+ };
340
+ }));
341
+
342
+ return { success: true, address, routers };
343
+ },
344
+ },
345
+
346
+ // 7. x402_distribute
347
+ {
348
+ name: 'x402_distribute',
349
+ description: 'Distribute (withdraw) USDC from a PaymentRouter contract',
350
+ inputSchema: {
351
+ type: 'object',
352
+ properties: {
353
+ routerAddress: { type: 'string', description: 'PaymentRouter contract address' },
354
+ amount: { type: 'string', description: 'USDC amount to distribute (defaults to full balance)' },
355
+ },
356
+ required: ['routerAddress'],
357
+ },
358
+ async execute(params) {
359
+ const routerAddress = params.routerAddress as string;
360
+ const specifiedAmount = params.amount as string | undefined;
361
+
362
+ if (!routerAddress.startsWith('0x') || routerAddress.length !== 42) {
363
+ return { success: false, error: 'Invalid router address' };
364
+ }
365
+
366
+ const { getClient } = await import('../core/client.js');
367
+ const { getConfig, getUsdcAddress } = await import('../core/config.js');
368
+ const { formatUnits, parseUnits } = await import('viem');
369
+
370
+ const config = getConfig();
371
+ const client = getClient();
372
+ const usdcAddress = getUsdcAddress(config.chainId);
373
+ const routerAddr = routerAddress as `0x${string}`;
374
+
375
+ const ERC20_ABI = [{
376
+ name: 'balanceOf' as const,
377
+ type: 'function' as const,
378
+ stateMutability: 'view' as const,
379
+ inputs: [{ name: 'account', type: 'address' }],
380
+ outputs: [{ name: '', type: 'uint256' }],
381
+ }] as const;
382
+
383
+ const ROUTER_ABI = [{
384
+ name: 'distribute' as const,
385
+ type: 'function' as const,
386
+ stateMutability: 'nonpayable' as const,
387
+ inputs: [
388
+ { name: 'token', type: 'address' },
389
+ { name: 'amount', type: 'uint256' },
390
+ ],
391
+ outputs: [],
392
+ }] as const;
393
+
394
+ const routerBalance = await client.publicClient.readContract({
395
+ address: usdcAddress,
396
+ abi: ERC20_ABI,
397
+ functionName: 'balanceOf',
398
+ args: [routerAddr],
399
+ });
400
+
401
+ if (routerBalance === 0n) {
402
+ return { success: false, error: 'Router has no USDC balance to distribute' };
403
+ }
404
+
405
+ let distributeAmount: bigint;
406
+ if (specifiedAmount) {
407
+ distributeAmount = parseUnits(specifiedAmount, 6);
408
+ if (distributeAmount > routerBalance) {
409
+ return {
410
+ success: false,
411
+ error: `Requested ${specifiedAmount} USDC exceeds balance ${formatUnits(routerBalance, 6)} USDC`,
412
+ };
413
+ }
414
+ } else {
415
+ distributeAmount = routerBalance;
416
+ }
417
+
418
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
419
+ const txHash = await (client.walletClient as any).writeContract({
420
+ address: routerAddr,
421
+ abi: ROUTER_ABI,
422
+ functionName: 'distribute',
423
+ args: [usdcAddress, distributeAmount],
424
+ });
425
+
426
+ const receipt = await client.publicClient.waitForTransactionReceipt({ hash: txHash });
427
+
428
+ return {
429
+ success: receipt.status === 'success',
430
+ routerAddress,
431
+ amount: formatUnits(distributeAmount, 6),
432
+ amountRaw: distributeAmount.toString(),
433
+ transactionHash: txHash,
434
+ blockNumber: receipt.blockNumber.toString(),
435
+ status: receipt.status,
436
+ };
437
+ },
438
+ },
439
+
440
+ // 8. x402_watcher_status
441
+ {
442
+ name: 'x402_watcher_status',
443
+ description: 'Get the status of the background payment watcher (tracked routers, payments detected)',
444
+ inputSchema: { type: 'object', properties: {}, required: [] },
445
+ async execute() {
446
+ if (!watcher) {
447
+ return { running: false, error: 'Watcher is not enabled' };
448
+ }
449
+ return watcher.getStatus();
450
+ },
451
+ },
452
+ ];
453
+ }
@@ -0,0 +1,107 @@
1
+ // OpenClaw Plugin API types for agentic-x402
2
+
3
+ /** Logger provided by OpenClaw to plugins */
4
+ export interface PluginLogger {
5
+ info(message: string): void;
6
+ warn(message: string): void;
7
+ error(message: string): void;
8
+ debug(message: string): void;
9
+ }
10
+
11
+ /** A background service registered by a plugin */
12
+ export interface PluginService {
13
+ id: string;
14
+ start(): Promise<void>;
15
+ stop(): Promise<void>;
16
+ }
17
+
18
+ /** JSON Schema for tool input parameters */
19
+ export interface ToolInputSchema {
20
+ type: 'object';
21
+ properties: Record<string, {
22
+ type: string;
23
+ description: string;
24
+ enum?: string[];
25
+ default?: unknown;
26
+ }>;
27
+ required?: string[];
28
+ }
29
+
30
+ /** An agent tool registered by a plugin */
31
+ export interface PluginTool {
32
+ name: string;
33
+ description: string;
34
+ inputSchema: ToolInputSchema;
35
+ execute(params: Record<string, unknown>): Promise<unknown>;
36
+ }
37
+
38
+ /** Commander.js-style program for CLI registration */
39
+ export interface CliProgram {
40
+ command(name: string): CliCommand;
41
+ }
42
+
43
+ export interface CliCommand {
44
+ command(name: string): CliCommand;
45
+ description(desc: string): CliCommand;
46
+ argument(name: string, desc?: string): CliCommand;
47
+ option(flags: string, desc?: string, defaultValue?: unknown): CliCommand;
48
+ action(fn: (...args: unknown[]) => void | Promise<void>): CliCommand;
49
+ }
50
+
51
+ /** The API that OpenClaw passes to plugin register() */
52
+ export interface OpenClawPluginApi {
53
+ logger: PluginLogger;
54
+ config: Record<string, unknown>;
55
+ gatewayPort: number;
56
+ registerService(service: PluginService): void;
57
+ registerTool(tool: PluginTool): void;
58
+ registerCli(
59
+ setup: (ctx: { program: CliProgram }) => void,
60
+ opts: { commands: string[] },
61
+ ): void;
62
+ }
63
+
64
+ /** Plugin config schema matching openclaw.plugin.json */
65
+ export interface X402PluginConfig {
66
+ evmPrivateKey?: string;
67
+ network?: 'mainnet' | 'testnet';
68
+ maxPaymentUsd?: number;
69
+ x402LinksApiUrl?: string;
70
+ watcher?: {
71
+ enabled?: boolean;
72
+ pollIntervalMs?: number;
73
+ notifyOnPayment?: boolean;
74
+ };
75
+ }
76
+
77
+ /** Tracked router state in the watcher */
78
+ export interface TrackedRouter {
79
+ address: string;
80
+ name: string;
81
+ lastBalance: bigint;
82
+ lastChecked: number;
83
+ }
84
+
85
+ /** Payment detection event */
86
+ export interface PaymentEvent {
87
+ routerAddress: string;
88
+ routerName: string;
89
+ previousBalance: string;
90
+ newBalance: string;
91
+ increase: string;
92
+ detectedAt: string;
93
+ }
94
+
95
+ /** Watcher status for introspection */
96
+ export interface WatcherStatus {
97
+ running: boolean;
98
+ pollIntervalMs: number;
99
+ trackedRouters: Array<{
100
+ address: string;
101
+ name: string;
102
+ balance: string;
103
+ lastChecked: string;
104
+ }>;
105
+ paymentsDetected: number;
106
+ lastPollAt: string | null;
107
+ }