x402-proxy 0.2.0 → 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.
- package/README.md +44 -36
- package/dist/bin/cli.js +130 -113
- package/dist/index.d.ts +3 -1
- package/dist/index.js +12 -1
- package/dist/status-BFDDA6oN.js +4 -0
- package/dist/{status-DeCY-cLR.js → status-DRSZjSq9.js} +313 -146
- package/package.json +13 -16
- package/dist/status-CPBQ0UBZ.js +0 -4
package/README.md
CHANGED
|
@@ -1,54 +1,27 @@
|
|
|
1
1
|
# x402-proxy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`curl` for [x402](https://www.x402.org/) paid APIs. Auto-pays HTTP 402 responses with USDC on Base and Solana - zero crypto code on the buyer side.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx x402-proxy
|
|
9
|
-
npx x402-proxy wallet fund # see where to send USDC
|
|
10
|
-
npx x402-proxy https://example.com # make a paid request
|
|
8
|
+
npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
That's it. The endpoint returns 402, x402-proxy pays and streams the response.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
First time? Set up a wallet:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
x402-proxy
|
|
19
|
-
x402-proxy
|
|
20
|
-
x402-proxy setup # onboarding wizard
|
|
21
|
-
x402-proxy status # config + wallet + spend summary
|
|
22
|
-
x402-proxy wallet # show addresses
|
|
23
|
-
x402-proxy wallet history # payment history
|
|
24
|
-
x402-proxy wallet fund # funding instructions
|
|
25
|
-
x402-proxy wallet export-key <chain> # bare key to stdout (evm|solana)
|
|
16
|
+
npx x402-proxy setup # generate wallet from BIP-39 mnemonic
|
|
17
|
+
npx x402-proxy wallet fund # see where to send USDC
|
|
26
18
|
```
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
One mnemonic derives both EVM (Base) and Solana keypairs. Fund either chain and go.
|
|
29
21
|
|
|
30
|
-
##
|
|
22
|
+
## MCP Proxy
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
# GET request
|
|
34
|
-
x402-proxy https://twitter.surf.cascade.fyi/search?q=x402
|
|
35
|
-
|
|
36
|
-
# POST with body and headers
|
|
37
|
-
x402-proxy --method POST \
|
|
38
|
-
--header "Content-Type: application/json" \
|
|
39
|
-
--body '{"url":"https://x402.org"}' \
|
|
40
|
-
https://web.surf.cascade.fyi/v1/crawl
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Response body streams to stdout, payment info goes to stderr. Pipe-safe:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
x402-proxy https://api.example.com/data | jq '.results'
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## MCP Proxy (Alpha)
|
|
50
|
-
|
|
51
|
-
Wraps a remote MCP server with automatic x402 payment. Configure in your MCP client:
|
|
24
|
+
Let your AI agent consume any paid MCP server. Configure in Claude, Cursor, or any MCP client:
|
|
52
25
|
|
|
53
26
|
```json
|
|
54
27
|
{
|
|
@@ -64,6 +37,41 @@ Wraps a remote MCP server with automatic x402 payment. Configure in your MCP cli
|
|
|
64
37
|
}
|
|
65
38
|
```
|
|
66
39
|
|
|
40
|
+
The proxy sits between your agent and the remote server, intercepting 402 responses, paying automatically, and forwarding the result. Your agent never touches crypto.
|
|
41
|
+
|
|
42
|
+
## HTTP Requests
|
|
43
|
+
|
|
44
|
+
Works like curl. Response body streams to stdout, payment info goes to stderr.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# GET request
|
|
48
|
+
x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
|
|
49
|
+
|
|
50
|
+
# POST with body and headers
|
|
51
|
+
x402-proxy --method POST \
|
|
52
|
+
--header "Content-Type: application/json" \
|
|
53
|
+
--body '{"url":"https://x402.org"}' \
|
|
54
|
+
https://web.surf.cascade.fyi/v1/crawl
|
|
55
|
+
|
|
56
|
+
# Pipe-safe
|
|
57
|
+
x402-proxy https://api.example.com/data | jq '.results'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
x402-proxy <url> # paid HTTP request (default command)
|
|
64
|
+
x402-proxy mcp <url> # MCP stdio proxy for agents
|
|
65
|
+
x402-proxy setup # onboarding wizard
|
|
66
|
+
x402-proxy status # config + wallet + spend summary
|
|
67
|
+
x402-proxy wallet # show addresses
|
|
68
|
+
x402-proxy wallet history # payment history
|
|
69
|
+
x402-proxy wallet fund # funding instructions
|
|
70
|
+
x402-proxy wallet export-key <chain> # bare key to stdout (evm|solana)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
All commands support `--help` for details.
|
|
74
|
+
|
|
67
75
|
## Wallet
|
|
68
76
|
|
|
69
77
|
A single BIP-39 mnemonic derives both chains:
|
package/dist/bin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { S as
|
|
2
|
+
import { C as calcSpend, S as appendHistory, T as readHistory, _ as getWalletPath, a as resolveWallet, b as saveConfig, c as generateMnemonic, d as info, f as isTTY, g as getHistoryPath, h as getConfigDirShort, i as buildX402Client, l as dim, m as ensureConfigDir, n as statusCommand, o as deriveEvmKeypair, p as warn, r as walletInfoCommand, s as deriveSolanaKeypair, u as error, v as isConfigured, w as formatTxLine, x as saveWalletFile, y as loadConfig } from "../status-DRSZjSq9.js";
|
|
3
3
|
import { buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { decodePaymentResponseHeader, wrapFetchWithPayment } from "@x402/fetch";
|
|
@@ -49,15 +49,22 @@ function createX402ProxyHandler(opts) {
|
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/commands/fetch.ts
|
|
51
51
|
const fetchCommand = buildCommand({
|
|
52
|
-
docs: {
|
|
52
|
+
docs: {
|
|
53
|
+
brief: "Make a paid HTTP request (default command)",
|
|
54
|
+
fullDescription: `Make a paid HTTP request. Payment is automatic when the server returns 402.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
$ x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi
|
|
58
|
+
$ x402-proxy -X POST -d '{"url":"https://x402.org"}' https://web.surf.cascade.fyi/v1/crawl
|
|
59
|
+
$ x402-proxy https://api.example.com/data | jq '.results'`
|
|
60
|
+
},
|
|
53
61
|
parameters: {
|
|
54
62
|
flags: {
|
|
55
63
|
method: {
|
|
56
64
|
kind: "parsed",
|
|
57
65
|
brief: "HTTP method",
|
|
58
66
|
parse: String,
|
|
59
|
-
default: "GET"
|
|
60
|
-
optional: true
|
|
67
|
+
default: "GET"
|
|
61
68
|
},
|
|
62
69
|
body: {
|
|
63
70
|
kind: "parsed",
|
|
@@ -102,18 +109,38 @@ const fetchCommand = buildCommand({
|
|
|
102
109
|
async func(flags, url) {
|
|
103
110
|
if (!url) {
|
|
104
111
|
if (isConfigured()) {
|
|
105
|
-
const {
|
|
106
|
-
|
|
112
|
+
const { displayStatus } = await import("../status-BFDDA6oN.js");
|
|
113
|
+
await displayStatus();
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(pc.dim(" Commands:"));
|
|
116
|
+
console.log(` ${pc.cyan("$ npx x402-proxy <url>")} Fetch a paid API`);
|
|
117
|
+
console.log(` ${pc.cyan("$ npx x402-proxy mcp <url>")} MCP proxy for AI agents`);
|
|
118
|
+
console.log(` ${pc.cyan("$ npx x402-proxy setup")} Reconfigure wallet`);
|
|
119
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
|
|
120
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet history")} Full payment history`);
|
|
121
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet fund")} Funding instructions`);
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi"));
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(pc.dim(" https://github.com/cascade-protocol/x402-proxy"));
|
|
126
|
+
console.log();
|
|
107
127
|
} else {
|
|
108
128
|
console.log();
|
|
109
|
-
console.log(pc.cyan(
|
|
129
|
+
console.log(pc.cyan(pc.bold("x402-proxy")));
|
|
130
|
+
console.log(pc.dim("curl for x402 paid APIs"));
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(pc.dim(" Commands:"));
|
|
133
|
+
console.log(` ${pc.cyan("$ npx x402-proxy setup")} Create a wallet`);
|
|
134
|
+
console.log(` ${pc.cyan("$ npx x402-proxy <url>")} Fetch a paid API`);
|
|
135
|
+
console.log(` ${pc.cyan("$ npx x402-proxy mcp <url>")} MCP proxy for AI agents`);
|
|
136
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet")} Addresses and balances`);
|
|
137
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet history")} Payment history`);
|
|
138
|
+
console.log(` ${pc.cyan("$ npx x402-proxy wallet fund")} Funding instructions`);
|
|
139
|
+
console.log(` ${pc.cyan("$ npx x402-proxy --help")} All options`);
|
|
110
140
|
console.log();
|
|
111
|
-
console.log(pc.dim("
|
|
112
|
-
console.log(
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(` ${pc.cyan("x402-proxy mcp <url>")} MCP proxy for agents`);
|
|
115
|
-
console.log(` ${pc.cyan("x402-proxy wallet")} Wallet info`);
|
|
116
|
-
console.log(` ${pc.cyan("x402-proxy --help")} All commands`);
|
|
141
|
+
console.log(pc.dim(" try: ") + pc.cyan("$ npx x402-proxy setup"));
|
|
142
|
+
console.log();
|
|
143
|
+
console.log(pc.dim(" https://github.com/cascade-protocol/x402-proxy"));
|
|
117
144
|
console.log();
|
|
118
145
|
}
|
|
119
146
|
return;
|
|
@@ -134,7 +161,12 @@ const fetchCommand = buildCommand({
|
|
|
134
161
|
console.error(pc.dim(`Run ${pc.cyan("x402-proxy setup")} or set X402_PROXY_WALLET_MNEMONIC`));
|
|
135
162
|
process.exit(1);
|
|
136
163
|
}
|
|
137
|
-
const
|
|
164
|
+
const config = loadConfig();
|
|
165
|
+
const { x402Fetch, shiftPayment } = createX402ProxyHandler({ client: await buildX402Client(wallet, {
|
|
166
|
+
preferredNetwork: config?.defaultNetwork,
|
|
167
|
+
spendLimitDaily: config?.spendLimitDaily,
|
|
168
|
+
spendLimitPerTx: config?.spendLimitPerTx
|
|
169
|
+
}) });
|
|
138
170
|
const headers = new Headers();
|
|
139
171
|
if (flags.header) for (const h of flags.header) {
|
|
140
172
|
const idx = h.indexOf(":");
|
|
@@ -199,7 +231,15 @@ const fetchCommand = buildCommand({
|
|
|
199
231
|
//#endregion
|
|
200
232
|
//#region src/commands/mcp.ts
|
|
201
233
|
const mcpCommand = buildCommand({
|
|
202
|
-
docs: {
|
|
234
|
+
docs: {
|
|
235
|
+
brief: "Start MCP stdio proxy with x402 payment (alpha)",
|
|
236
|
+
fullDescription: `Start an MCP stdio proxy with automatic x402 payment for AI agents.
|
|
237
|
+
|
|
238
|
+
Add to your MCP client config (Claude, Cursor, etc.):
|
|
239
|
+
"command": "npx",
|
|
240
|
+
"args": ["x402-proxy", "mcp", "https://mcp.example.com/sse"],
|
|
241
|
+
"env": { "X402_PROXY_WALLET_MNEMONIC": "your 24 words" }`
|
|
242
|
+
},
|
|
203
243
|
parameters: {
|
|
204
244
|
flags: {
|
|
205
245
|
evmKey: {
|
|
@@ -236,7 +276,12 @@ const mcpCommand = buildCommand({
|
|
|
236
276
|
dim(`x402-proxy MCP proxy -> ${remoteUrl}`);
|
|
237
277
|
if (wallet.evmAddress) dim(` EVM: ${wallet.evmAddress}`);
|
|
238
278
|
if (wallet.solanaAddress) dim(` Solana: ${wallet.solanaAddress}`);
|
|
239
|
-
const
|
|
279
|
+
const config = loadConfig();
|
|
280
|
+
const x402PaymentClient = await buildX402Client(wallet, {
|
|
281
|
+
preferredNetwork: config?.defaultNetwork,
|
|
282
|
+
spendLimitDaily: config?.spendLimitDaily,
|
|
283
|
+
spendLimitPerTx: config?.spendLimitPerTx
|
|
284
|
+
});
|
|
240
285
|
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
241
286
|
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
242
287
|
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
@@ -245,12 +290,12 @@ const mcpCommand = buildCommand({
|
|
|
245
290
|
const { x402MCPClient } = await import("@x402/mcp");
|
|
246
291
|
const x402Mcp = new x402MCPClient(new Client({
|
|
247
292
|
name: "x402-proxy",
|
|
248
|
-
version: "0.
|
|
293
|
+
version: "0.3.0"
|
|
249
294
|
}), x402PaymentClient, {
|
|
250
295
|
autoPayment: true,
|
|
251
296
|
onPaymentRequested: (ctx) => {
|
|
252
297
|
const accept = ctx.paymentRequired.accepts?.[0];
|
|
253
|
-
if (accept) warn(` Payment: ${accept.
|
|
298
|
+
if (accept) warn(` Payment: ${accept.amount} on ${accept.network} for tool "${ctx.toolName}"`);
|
|
254
299
|
return true;
|
|
255
300
|
}
|
|
256
301
|
});
|
|
@@ -290,7 +335,7 @@ const mcpCommand = buildCommand({
|
|
|
290
335
|
dim(` ${tools.length} tools available`);
|
|
291
336
|
const localServer = new McpServer({
|
|
292
337
|
name: "x402-proxy",
|
|
293
|
-
version: "0.
|
|
338
|
+
version: "0.3.0"
|
|
294
339
|
});
|
|
295
340
|
for (const tool of tools) localServer.tool(tool.name, tool.description ?? "", tool.inputSchema?.properties ? Object.fromEntries(Object.entries(tool.inputSchema.properties).map(([k, v]) => [k, v])) : {}, async (args) => {
|
|
296
341
|
const result = await x402Mcp.callTool(tool.name, args);
|
|
@@ -365,7 +410,7 @@ const setupCommand = buildCommand({
|
|
|
365
410
|
} else {
|
|
366
411
|
const input = await prompts.text({
|
|
367
412
|
message: "Enter your 24-word mnemonic:",
|
|
368
|
-
validate: (v) => {
|
|
413
|
+
validate: (v = "") => {
|
|
369
414
|
const words = v.trim().split(/\s+/);
|
|
370
415
|
if (words.length !== 12 && words.length !== 24) return "Mnemonic must be 12 or 24 words";
|
|
371
416
|
}
|
|
@@ -378,7 +423,7 @@ const setupCommand = buildCommand({
|
|
|
378
423
|
}
|
|
379
424
|
const evm = deriveEvmKeypair(mnemonic);
|
|
380
425
|
const sol = deriveSolanaKeypair(mnemonic);
|
|
381
|
-
prompts.log.success(`
|
|
426
|
+
prompts.log.success(`Base address: ${pc.green(evm.address)}`);
|
|
382
427
|
prompts.log.success(`Solana address: ${pc.green(sol.address)}`);
|
|
383
428
|
saveWalletFile({
|
|
384
429
|
version: 1,
|
|
@@ -389,18 +434,61 @@ const setupCommand = buildCommand({
|
|
|
389
434
|
}
|
|
390
435
|
});
|
|
391
436
|
saveConfig({});
|
|
392
|
-
prompts.log.info(`Config directory: ${pc.dim(
|
|
437
|
+
prompts.log.info(`Config directory: ${pc.dim(getConfigDirShort())}`);
|
|
393
438
|
prompts.log.step("Fund your wallets to start using x402 resources:");
|
|
394
439
|
prompts.log.message(` Solana (USDC): Send USDC to ${pc.cyan(sol.address)}`);
|
|
395
|
-
prompts.log.message(`
|
|
440
|
+
prompts.log.message(` Base (USDC): Send USDC to ${pc.cyan(evm.address)}`);
|
|
441
|
+
prompts.log.step("Try your first request:");
|
|
442
|
+
prompts.log.message(` ${pc.cyan("$ npx x402-proxy https://twitter.surf.cascade.fyi/user/cascade_fyi")}`);
|
|
396
443
|
prompts.outro(pc.green("Setup complete!"));
|
|
397
444
|
}
|
|
398
445
|
});
|
|
399
446
|
|
|
400
447
|
//#endregion
|
|
401
|
-
//#region src/commands/wallet.ts
|
|
402
|
-
const
|
|
403
|
-
docs: { brief: "
|
|
448
|
+
//#region src/commands/wallet-export.ts
|
|
449
|
+
const walletExportCommand = buildCommand({
|
|
450
|
+
docs: { brief: "Export private key to stdout (pipe-safe)" },
|
|
451
|
+
parameters: {
|
|
452
|
+
flags: {},
|
|
453
|
+
positional: {
|
|
454
|
+
kind: "tuple",
|
|
455
|
+
parameters: [{
|
|
456
|
+
brief: "Chain to export: evm or solana",
|
|
457
|
+
parse: (input) => {
|
|
458
|
+
const v = input.toLowerCase();
|
|
459
|
+
if (v !== "evm" && v !== "solana") throw new Error("Must be 'evm' or 'solana'");
|
|
460
|
+
return v;
|
|
461
|
+
}
|
|
462
|
+
}]
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
async func(flags, chain) {
|
|
466
|
+
const wallet = resolveWallet();
|
|
467
|
+
if (wallet.source === "none") {
|
|
468
|
+
error("No wallet configured.");
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
if (chain === "evm" && !wallet.evmKey) {
|
|
472
|
+
error("No EVM key available.");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
if (chain === "solana" && !wallet.solanaKey) {
|
|
476
|
+
error("No Solana key available.");
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
if (process.stdout.isTTY) {
|
|
480
|
+
const confirmed = await prompts.confirm({ message: "This will print your private key to the terminal. Continue?" });
|
|
481
|
+
if (prompts.isCancel(confirmed) || !confirmed) process.exit(0);
|
|
482
|
+
} else warn("Warning: private key will be printed to stdout.");
|
|
483
|
+
if (chain === "evm") process.stdout.write(wallet.evmKey);
|
|
484
|
+
else process.stdout.write(base58.encode(wallet.solanaKey.slice(0, 32)));
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/commands/wallet-fund.ts
|
|
490
|
+
const walletFundCommand = buildCommand({
|
|
491
|
+
docs: { brief: "Show wallet funding instructions" },
|
|
404
492
|
parameters: {
|
|
405
493
|
flags: {},
|
|
406
494
|
positional: {
|
|
@@ -413,15 +501,24 @@ const walletInfoCommand = buildCommand({
|
|
|
413
501
|
if (wallet.source === "none") {
|
|
414
502
|
console.log(pc.yellow("No wallet configured."));
|
|
415
503
|
console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
416
|
-
console.log(pc.dim(`Or set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
|
|
417
504
|
process.exit(1);
|
|
418
505
|
}
|
|
419
506
|
console.log();
|
|
420
|
-
info("
|
|
507
|
+
info("Funding Instructions");
|
|
421
508
|
console.log();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
509
|
+
if (wallet.solanaAddress) {
|
|
510
|
+
console.log(pc.bold(" Solana (USDC):"));
|
|
511
|
+
console.log(` Send USDC to: ${pc.green(wallet.solanaAddress)}`);
|
|
512
|
+
console.log(pc.dim(" Network: Solana Mainnet"));
|
|
513
|
+
console.log();
|
|
514
|
+
}
|
|
515
|
+
if (wallet.evmAddress) {
|
|
516
|
+
console.log(pc.bold(" Base (USDC):"));
|
|
517
|
+
console.log(` Send USDC to: ${pc.green(wallet.evmAddress)}`);
|
|
518
|
+
console.log(pc.dim(" Network: Base (Chain ID 8453)"));
|
|
519
|
+
console.log();
|
|
520
|
+
}
|
|
521
|
+
console.log(pc.dim(" Tip: Most x402 services accept USDC on Base or Solana."));
|
|
425
522
|
console.log();
|
|
426
523
|
}
|
|
427
524
|
});
|
|
@@ -475,86 +572,6 @@ const walletHistoryCommand = buildCommand({
|
|
|
475
572
|
}
|
|
476
573
|
});
|
|
477
574
|
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region src/commands/wallet-fund.ts
|
|
480
|
-
const walletFundCommand = buildCommand({
|
|
481
|
-
docs: { brief: "Show wallet funding instructions" },
|
|
482
|
-
parameters: {
|
|
483
|
-
flags: {},
|
|
484
|
-
positional: {
|
|
485
|
-
kind: "tuple",
|
|
486
|
-
parameters: []
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
func() {
|
|
490
|
-
const wallet = resolveWallet();
|
|
491
|
-
if (wallet.source === "none") {
|
|
492
|
-
console.log(pc.yellow("No wallet configured."));
|
|
493
|
-
console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
494
|
-
process.exit(1);
|
|
495
|
-
}
|
|
496
|
-
console.log();
|
|
497
|
-
info("Funding Instructions");
|
|
498
|
-
console.log();
|
|
499
|
-
if (wallet.solanaAddress) {
|
|
500
|
-
console.log(pc.bold(" Solana (USDC):"));
|
|
501
|
-
console.log(` Send USDC to: ${pc.green(wallet.solanaAddress)}`);
|
|
502
|
-
console.log(pc.dim(" Network: Solana Mainnet"));
|
|
503
|
-
console.log();
|
|
504
|
-
}
|
|
505
|
-
if (wallet.evmAddress) {
|
|
506
|
-
console.log(pc.bold(" Base (USDC):"));
|
|
507
|
-
console.log(` Send USDC to: ${pc.green(wallet.evmAddress)}`);
|
|
508
|
-
console.log(pc.dim(" Network: Base (Chain ID 8453)"));
|
|
509
|
-
console.log();
|
|
510
|
-
}
|
|
511
|
-
console.log(pc.dim(" Tip: Most x402 services accept USDC on Base or Solana."));
|
|
512
|
-
console.log();
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
//#endregion
|
|
517
|
-
//#region src/commands/wallet-export.ts
|
|
518
|
-
const walletExportCommand = buildCommand({
|
|
519
|
-
docs: { brief: "Export private key to stdout (pipe-safe)" },
|
|
520
|
-
parameters: {
|
|
521
|
-
flags: {},
|
|
522
|
-
positional: {
|
|
523
|
-
kind: "tuple",
|
|
524
|
-
parameters: [{
|
|
525
|
-
brief: "Chain to export: evm or solana",
|
|
526
|
-
parse: (input) => {
|
|
527
|
-
const v = input.toLowerCase();
|
|
528
|
-
if (v !== "evm" && v !== "solana") throw new Error("Must be 'evm' or 'solana'");
|
|
529
|
-
return v;
|
|
530
|
-
}
|
|
531
|
-
}]
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
func(_flags, chain) {
|
|
535
|
-
const wallet = resolveWallet();
|
|
536
|
-
if (wallet.source === "none") {
|
|
537
|
-
error("No wallet configured.");
|
|
538
|
-
process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
if (chain === "evm") {
|
|
541
|
-
if (!wallet.evmKey) {
|
|
542
|
-
error("No EVM key available.");
|
|
543
|
-
process.exit(1);
|
|
544
|
-
}
|
|
545
|
-
warn("Warning: private key will be printed to stdout.");
|
|
546
|
-
process.stdout.write(wallet.evmKey);
|
|
547
|
-
} else {
|
|
548
|
-
if (!wallet.solanaKey) {
|
|
549
|
-
error("No Solana key available.");
|
|
550
|
-
process.exit(1);
|
|
551
|
-
}
|
|
552
|
-
warn("Warning: private key will be printed to stdout.");
|
|
553
|
-
process.stdout.write(base58.encode(wallet.solanaKey.slice(0, 32)));
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
|
|
558
575
|
//#endregion
|
|
559
576
|
//#region src/app.ts
|
|
560
577
|
const routes = buildRouteMap({
|
|
@@ -575,11 +592,11 @@ const routes = buildRouteMap({
|
|
|
575
592
|
status: statusCommand
|
|
576
593
|
},
|
|
577
594
|
defaultCommand: "fetch",
|
|
578
|
-
docs: { brief: "
|
|
595
|
+
docs: { brief: "curl for x402 paid APIs" }
|
|
579
596
|
});
|
|
580
597
|
const app = buildApplication(routes, {
|
|
581
598
|
name: "x402-proxy",
|
|
582
|
-
versionInfo: { currentVersion: "0.
|
|
599
|
+
versionInfo: { currentVersion: "0.3.0" },
|
|
583
600
|
scanner: { caseStyle: "allow-kebab-for-camel" }
|
|
584
601
|
});
|
|
585
602
|
|
package/dist/index.d.ts
CHANGED
|
@@ -66,7 +66,9 @@ declare function calcSpend(records: TxRecord[]): {
|
|
|
66
66
|
count: number;
|
|
67
67
|
};
|
|
68
68
|
declare function explorerUrl(net: string, tx: string): string;
|
|
69
|
-
declare function formatTxLine(r: TxRecord
|
|
69
|
+
declare function formatTxLine(r: TxRecord, opts?: {
|
|
70
|
+
verbose?: boolean;
|
|
71
|
+
}): string;
|
|
70
72
|
//#endregion
|
|
71
73
|
//#region src/wallet.d.ts
|
|
72
74
|
/**
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,13 @@ function shortModel(model) {
|
|
|
131
131
|
const parts = model.split("/");
|
|
132
132
|
return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
|
|
133
133
|
}
|
|
134
|
-
function
|
|
134
|
+
function shortNetwork(net) {
|
|
135
|
+
if (net === "eip155:8453") return "base";
|
|
136
|
+
if (net.startsWith("eip155:")) return `evm:${net.split(":")[1]}`;
|
|
137
|
+
if (net.startsWith("solana:")) return "sol";
|
|
138
|
+
return net;
|
|
139
|
+
}
|
|
140
|
+
function formatTxLine(r, opts) {
|
|
135
141
|
const time = new Date(r.t).toLocaleTimeString("en-US", {
|
|
136
142
|
hour: "2-digit",
|
|
137
143
|
minute: "2-digit",
|
|
@@ -143,6 +149,11 @@ function formatTxLine(r) {
|
|
|
143
149
|
if (r.label) parts.push(r.label);
|
|
144
150
|
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
145
151
|
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
152
|
+
parts.push(shortNetwork(r.net));
|
|
153
|
+
if (opts?.verbose && r.tx) {
|
|
154
|
+
const short = r.tx.length > 20 ? `${r.tx.slice(0, 10)}...${r.tx.slice(-6)}` : r.tx;
|
|
155
|
+
parts.push(short);
|
|
156
|
+
}
|
|
146
157
|
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
147
158
|
}
|
|
148
159
|
|
|
@@ -1,32 +1,150 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildCommand } from "@stricli/core";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
+
import { x402Client } from "@x402/fetch";
|
|
4
5
|
import fs, { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { parse, stringify } from "yaml";
|
|
9
|
+
import { base58 } from "@scure/base";
|
|
8
10
|
import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
|
|
9
|
-
import { x402Client } from "@x402/fetch";
|
|
10
11
|
import { ExactSvmScheme } from "@x402/svm/exact/client";
|
|
11
|
-
import { base58 } from "@scure/base";
|
|
12
12
|
import { createPublicClient, http } from "viem";
|
|
13
13
|
import { privateKeyToAccount } from "viem/accounts";
|
|
14
14
|
import { base } from "viem/chains";
|
|
15
15
|
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
16
16
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
17
17
|
import { hmac } from "@noble/hashes/hmac.js";
|
|
18
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
19
18
|
import { sha512 } from "@noble/hashes/sha2.js";
|
|
19
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
20
20
|
import { HDKey } from "@scure/bip32";
|
|
21
21
|
import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
|
|
22
22
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
23
23
|
|
|
24
|
+
//#region src/history.ts
|
|
25
|
+
const HISTORY_MAX_LINES = 1e3;
|
|
26
|
+
const HISTORY_KEEP_LINES = 500;
|
|
27
|
+
function appendHistory(historyPath, record) {
|
|
28
|
+
try {
|
|
29
|
+
appendFileSync(historyPath, `${JSON.stringify(record)}\n`);
|
|
30
|
+
if (existsSync(historyPath)) {
|
|
31
|
+
if (statSync(historyPath).size > HISTORY_MAX_LINES * 200) {
|
|
32
|
+
const lines = readFileSync(historyPath, "utf-8").trimEnd().split("\n");
|
|
33
|
+
if (lines.length > HISTORY_MAX_LINES) writeFileSync(historyPath, `${lines.slice(-HISTORY_KEEP_LINES).join("\n")}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
function readHistory(historyPath) {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(historyPath)) return [];
|
|
41
|
+
const content = readFileSync(historyPath, "utf-8").trimEnd();
|
|
42
|
+
if (!content) return [];
|
|
43
|
+
return content.split("\n").flatMap((line) => {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(line);
|
|
46
|
+
if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
|
|
47
|
+
return [parsed];
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function calcSpend(records) {
|
|
57
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
58
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
59
|
+
const todayMs = todayStart.getTime();
|
|
60
|
+
let today = 0;
|
|
61
|
+
let total = 0;
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const r of records) {
|
|
64
|
+
if (!r.ok || r.amount == null) continue;
|
|
65
|
+
if (r.token !== "USDC") continue;
|
|
66
|
+
total += r.amount;
|
|
67
|
+
count++;
|
|
68
|
+
if (r.t >= todayMs) today += r.amount;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
today,
|
|
72
|
+
total,
|
|
73
|
+
count
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function formatAmount(amount, token) {
|
|
77
|
+
if (token === "USDC") {
|
|
78
|
+
if (amount >= .01) return `${amount.toFixed(2)} USDC`;
|
|
79
|
+
if (amount >= .001) return `${amount.toFixed(3)} USDC`;
|
|
80
|
+
if (amount >= 1e-4) return `${amount.toFixed(4)} USDC`;
|
|
81
|
+
return `${amount.toFixed(6)} USDC`;
|
|
82
|
+
}
|
|
83
|
+
if (token === "SOL") return `${amount} SOL`;
|
|
84
|
+
return `${amount} ${token}`;
|
|
85
|
+
}
|
|
86
|
+
const KIND_LABELS = {
|
|
87
|
+
x402_inference: "inference",
|
|
88
|
+
x402_payment: "payment",
|
|
89
|
+
transfer: "transfer",
|
|
90
|
+
buy: "buy",
|
|
91
|
+
sell: "sell",
|
|
92
|
+
mint: "mint",
|
|
93
|
+
swap: "swap"
|
|
94
|
+
};
|
|
95
|
+
function explorerUrl(net, tx) {
|
|
96
|
+
if (net.startsWith("eip155:")) {
|
|
97
|
+
if (net.split(":")[1] === "8453") return `https://basescan.org/tx/${tx}`;
|
|
98
|
+
return `https://basescan.org/tx/${tx}`;
|
|
99
|
+
}
|
|
100
|
+
return `https://solscan.io/tx/${tx}`;
|
|
101
|
+
}
|
|
102
|
+
/** Strip provider prefix and OpenRouter date suffixes from model IDs.
|
|
103
|
+
* e.g. "minimax/minimax-m2.5-20260211" -> "minimax-m2.5"
|
|
104
|
+
* "moonshotai/kimi-k2.5-0127" -> "kimi-k2.5" */
|
|
105
|
+
function shortModel(model) {
|
|
106
|
+
const parts = model.split("/");
|
|
107
|
+
return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
|
|
108
|
+
}
|
|
109
|
+
function shortNetwork(net) {
|
|
110
|
+
if (net === "eip155:8453") return "base";
|
|
111
|
+
if (net.startsWith("eip155:")) return `evm:${net.split(":")[1]}`;
|
|
112
|
+
if (net.startsWith("solana:")) return "sol";
|
|
113
|
+
return net;
|
|
114
|
+
}
|
|
115
|
+
function formatTxLine(r, opts) {
|
|
116
|
+
const time = new Date(r.t).toLocaleTimeString("en-US", {
|
|
117
|
+
hour: "2-digit",
|
|
118
|
+
minute: "2-digit",
|
|
119
|
+
hour12: false,
|
|
120
|
+
timeZone: "UTC"
|
|
121
|
+
});
|
|
122
|
+
const timeStr = r.tx ? `[${time}](${explorerUrl(r.net, r.tx)})` : time;
|
|
123
|
+
const parts = [r.kind === "x402_inference" && r.model ? shortModel(r.model) : KIND_LABELS[r.kind] ?? r.kind];
|
|
124
|
+
if (r.label) parts.push(r.label);
|
|
125
|
+
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
126
|
+
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
127
|
+
parts.push(shortNetwork(r.net));
|
|
128
|
+
if (opts?.verbose && r.tx) {
|
|
129
|
+
const short = r.tx.length > 20 ? `${r.tx.slice(0, 10)}...${r.tx.slice(-6)}` : r.tx;
|
|
130
|
+
parts.push(short);
|
|
131
|
+
}
|
|
132
|
+
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
24
136
|
//#region src/lib/config.ts
|
|
25
137
|
const APP_NAME = "x402-proxy";
|
|
26
138
|
function getConfigDir() {
|
|
27
139
|
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
28
140
|
return path.join(xdg, APP_NAME);
|
|
29
141
|
}
|
|
142
|
+
/** Config dir with $HOME replaced by ~ for display */
|
|
143
|
+
function getConfigDirShort() {
|
|
144
|
+
const dir = getConfigDir();
|
|
145
|
+
const home = os.homedir();
|
|
146
|
+
return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
|
|
147
|
+
}
|
|
30
148
|
function getWalletPath() {
|
|
31
149
|
return path.join(getConfigDir(), "wallet.json");
|
|
32
150
|
}
|
|
@@ -72,9 +190,7 @@ function loadConfig() {
|
|
|
72
190
|
return JSON.parse(stripped);
|
|
73
191
|
}
|
|
74
192
|
return JSON.parse(raw);
|
|
75
|
-
} catch {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
193
|
+
} catch {}
|
|
78
194
|
}
|
|
79
195
|
return null;
|
|
80
196
|
}
|
|
@@ -90,6 +206,24 @@ function isConfigured() {
|
|
|
90
206
|
return loadWalletFile() !== null;
|
|
91
207
|
}
|
|
92
208
|
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/lib/output.ts
|
|
211
|
+
function isTTY() {
|
|
212
|
+
return !!process.stderr.isTTY;
|
|
213
|
+
}
|
|
214
|
+
function info(msg) {
|
|
215
|
+
process.stderr.write(`${isTTY() ? pc.cyan(msg) : msg}\n`);
|
|
216
|
+
}
|
|
217
|
+
function warn(msg) {
|
|
218
|
+
process.stderr.write(`${isTTY() ? pc.yellow(msg) : msg}\n`);
|
|
219
|
+
}
|
|
220
|
+
function error(msg) {
|
|
221
|
+
process.stderr.write(`${isTTY() ? pc.red(`✗ ${msg}`) : `✗ ${msg}`}\n`);
|
|
222
|
+
}
|
|
223
|
+
function dim(msg) {
|
|
224
|
+
process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
93
227
|
//#endregion
|
|
94
228
|
//#region src/lib/derive.ts
|
|
95
229
|
/**
|
|
@@ -221,11 +355,23 @@ function parsesolanaKey(input) {
|
|
|
221
355
|
}
|
|
222
356
|
return base58.decode(trimmed);
|
|
223
357
|
}
|
|
358
|
+
function networkToCaipPrefix(name) {
|
|
359
|
+
switch (name.toLowerCase()) {
|
|
360
|
+
case "base": return "eip155:8453";
|
|
361
|
+
case "solana": return "solana:";
|
|
362
|
+
default: return name;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
224
365
|
/**
|
|
225
366
|
* Build a configured x402Client from resolved wallet keys.
|
|
226
367
|
*/
|
|
227
|
-
async function buildX402Client(wallet) {
|
|
228
|
-
const client = new x402Client()
|
|
368
|
+
async function buildX402Client(wallet, opts) {
|
|
369
|
+
const client = new x402Client(opts?.preferredNetwork ? (() => {
|
|
370
|
+
const prefix = networkToCaipPrefix(opts.preferredNetwork);
|
|
371
|
+
return (_version, accepts) => {
|
|
372
|
+
return accepts.find((r) => r.network.startsWith(prefix)) || accepts[0];
|
|
373
|
+
};
|
|
374
|
+
})() : void 0);
|
|
229
375
|
if (wallet.evmKey) {
|
|
230
376
|
const hex = wallet.evmKey;
|
|
231
377
|
const signer = toClientEvmSigner(privateKeyToAccount(hex), createPublicClient({
|
|
@@ -240,169 +386,190 @@ async function buildX402Client(wallet) {
|
|
|
240
386
|
client.register("solana:mainnet", new ExactSvmScheme(signer));
|
|
241
387
|
client.register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme(signer));
|
|
242
388
|
}
|
|
389
|
+
const daily = opts?.spendLimitDaily;
|
|
390
|
+
const perTx = opts?.spendLimitPerTx;
|
|
391
|
+
if (daily || perTx) client.registerPolicy((_version, reqs) => {
|
|
392
|
+
if (daily) {
|
|
393
|
+
const spend = calcSpend(readHistory(getHistoryPath()));
|
|
394
|
+
if (spend.today >= daily) throw new Error(`Daily spend limit reached (${spend.today.toFixed(4)}/${daily} USDC)`);
|
|
395
|
+
const remaining = daily - spend.today;
|
|
396
|
+
reqs = reqs.filter((r) => Number(r.amount) / 1e6 <= remaining);
|
|
397
|
+
if (reqs.length === 0) throw new Error(`Daily spend limit of ${daily} USDC would be exceeded (${spend.today.toFixed(4)} spent today)`);
|
|
398
|
+
}
|
|
399
|
+
if (perTx) {
|
|
400
|
+
const before = reqs.length;
|
|
401
|
+
reqs = reqs.filter((r) => Number(r.amount) / 1e6 <= perTx);
|
|
402
|
+
if (reqs.length === 0 && before > 0) throw new Error(`Payment exceeds per-transaction limit of ${perTx} USDC`);
|
|
403
|
+
}
|
|
404
|
+
return reqs;
|
|
405
|
+
});
|
|
243
406
|
return client;
|
|
244
407
|
}
|
|
245
408
|
|
|
246
409
|
//#endregion
|
|
247
|
-
//#region src/
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
410
|
+
//#region src/commands/wallet.ts
|
|
411
|
+
const BASE_RPC = "https://mainnet.base.org";
|
|
412
|
+
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
|
413
|
+
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
414
|
+
const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
415
|
+
async function rpcCall(url, method, params) {
|
|
416
|
+
return (await fetch(url, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: { "Content-Type": "application/json" },
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
jsonrpc: "2.0",
|
|
421
|
+
method,
|
|
422
|
+
params,
|
|
423
|
+
id: 1
|
|
424
|
+
})
|
|
425
|
+
})).json();
|
|
253
426
|
}
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
function dim(msg) {
|
|
261
|
-
process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
//#endregion
|
|
265
|
-
//#region src/history.ts
|
|
266
|
-
const HISTORY_MAX_LINES = 1e3;
|
|
267
|
-
const HISTORY_KEEP_LINES = 500;
|
|
268
|
-
function appendHistory(historyPath, record) {
|
|
269
|
-
try {
|
|
270
|
-
appendFileSync(historyPath, `${JSON.stringify(record)}\n`);
|
|
271
|
-
if (existsSync(historyPath)) {
|
|
272
|
-
if (statSync(historyPath).size > HISTORY_MAX_LINES * 200) {
|
|
273
|
-
const lines = readFileSync(historyPath, "utf-8").trimEnd().split("\n");
|
|
274
|
-
if (lines.length > HISTORY_MAX_LINES) writeFileSync(historyPath, `${lines.slice(-HISTORY_KEEP_LINES).join("\n")}\n`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} catch {}
|
|
278
|
-
}
|
|
279
|
-
function readHistory(historyPath) {
|
|
280
|
-
try {
|
|
281
|
-
if (!existsSync(historyPath)) return [];
|
|
282
|
-
const content = readFileSync(historyPath, "utf-8").trimEnd();
|
|
283
|
-
if (!content) return [];
|
|
284
|
-
return content.split("\n").flatMap((line) => {
|
|
285
|
-
try {
|
|
286
|
-
const parsed = JSON.parse(line);
|
|
287
|
-
if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
|
|
288
|
-
return [parsed];
|
|
289
|
-
} catch {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
} catch {
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function calcSpend(records) {
|
|
298
|
-
const todayStart = /* @__PURE__ */ new Date();
|
|
299
|
-
todayStart.setUTCHours(0, 0, 0, 0);
|
|
300
|
-
const todayMs = todayStart.getTime();
|
|
301
|
-
let today = 0;
|
|
302
|
-
let total = 0;
|
|
303
|
-
let count = 0;
|
|
304
|
-
for (const r of records) {
|
|
305
|
-
if (!r.ok || r.amount == null) continue;
|
|
306
|
-
if (r.token !== "USDC") continue;
|
|
307
|
-
total += r.amount;
|
|
308
|
-
count++;
|
|
309
|
-
if (r.t >= todayMs) today += r.amount;
|
|
310
|
-
}
|
|
427
|
+
async function fetchEvmBalances(address) {
|
|
428
|
+
const usdcData = `0x70a08231${address.slice(2).padStart(64, "0")}`;
|
|
429
|
+
const [ethRes, usdcRes] = await Promise.all([rpcCall(BASE_RPC, "eth_getBalance", [address, "latest"]), rpcCall(BASE_RPC, "eth_call", [{
|
|
430
|
+
to: USDC_BASE,
|
|
431
|
+
data: usdcData
|
|
432
|
+
}, "latest"])]);
|
|
311
433
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
count
|
|
434
|
+
eth: ethRes.result ? (Number(BigInt(ethRes.result)) / 0xde0b6b3a7640000).toFixed(6) : "?",
|
|
435
|
+
usdc: usdcRes.result ? (Number(BigInt(usdcRes.result)) / 1e6).toFixed(2) : "?"
|
|
315
436
|
};
|
|
316
437
|
}
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
x402_payment: "payment",
|
|
330
|
-
transfer: "transfer",
|
|
331
|
-
buy: "buy",
|
|
332
|
-
sell: "sell",
|
|
333
|
-
mint: "mint",
|
|
334
|
-
swap: "swap"
|
|
335
|
-
};
|
|
336
|
-
function explorerUrl(net, tx) {
|
|
337
|
-
if (net.startsWith("eip155:")) {
|
|
338
|
-
if (net.split(":")[1] === "8453") return `https://basescan.org/tx/${tx}`;
|
|
339
|
-
return `https://basescan.org/tx/${tx}`;
|
|
340
|
-
}
|
|
341
|
-
return `https://solscan.io/tx/${tx}`;
|
|
438
|
+
async function fetchSolanaBalances(address) {
|
|
439
|
+
const [solRes, usdcRes] = await Promise.all([rpcCall(SOLANA_RPC, "getBalance", [address]), rpcCall(SOLANA_RPC, "getTokenAccountsByOwner", [
|
|
440
|
+
address,
|
|
441
|
+
{ mint: USDC_SOLANA_MINT },
|
|
442
|
+
{ encoding: "jsonParsed" }
|
|
443
|
+
])]);
|
|
444
|
+
const sol = solRes.result?.value != null ? (solRes.result.value / 1e9).toFixed(6) : "?";
|
|
445
|
+
const accounts = usdcRes.result?.value;
|
|
446
|
+
return {
|
|
447
|
+
sol,
|
|
448
|
+
usdc: accounts?.length ? Number(accounts[0].account.data.parsed.info.tokenAmount.uiAmountString).toFixed(2) : "0.00"
|
|
449
|
+
};
|
|
342
450
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
* "moonshotai/kimi-k2.5-0127" -> "kimi-k2.5" */
|
|
346
|
-
function shortModel(model) {
|
|
347
|
-
const parts = model.split("/");
|
|
348
|
-
return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
|
|
451
|
+
function balanceLine(usdc, native, nativeSymbol) {
|
|
452
|
+
return pc.dim(` (${usdc} USDC, ${native} ${nativeSymbol})`);
|
|
349
453
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
hour: "2-digit",
|
|
353
|
-
minute: "2-digit",
|
|
354
|
-
hour12: false,
|
|
355
|
-
timeZone: "UTC"
|
|
356
|
-
});
|
|
357
|
-
const timeStr = r.tx ? `[${time}](${explorerUrl(r.net, r.tx)})` : time;
|
|
358
|
-
const parts = [r.kind === "x402_inference" && r.model ? shortModel(r.model) : KIND_LABELS[r.kind] ?? r.kind];
|
|
359
|
-
if (r.label) parts.push(r.label);
|
|
360
|
-
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
361
|
-
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
362
|
-
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/commands/status.ts
|
|
367
|
-
const statusCommand = buildCommand({
|
|
368
|
-
docs: { brief: "Show configuration and wallet status" },
|
|
454
|
+
const walletInfoCommand = buildCommand({
|
|
455
|
+
docs: { brief: "Show wallet addresses and balances" },
|
|
369
456
|
parameters: {
|
|
370
|
-
flags: {
|
|
457
|
+
flags: { verbose: {
|
|
458
|
+
kind: "boolean",
|
|
459
|
+
brief: "Show transaction IDs",
|
|
460
|
+
default: false
|
|
461
|
+
} },
|
|
371
462
|
positional: {
|
|
372
463
|
kind: "tuple",
|
|
373
464
|
parameters: []
|
|
374
465
|
}
|
|
375
466
|
},
|
|
376
|
-
func() {
|
|
467
|
+
async func(flags) {
|
|
377
468
|
const wallet = resolveWallet();
|
|
378
|
-
|
|
469
|
+
if (wallet.source === "none") {
|
|
470
|
+
console.log(pc.yellow("No wallet configured."));
|
|
471
|
+
console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
472
|
+
console.log(pc.dim(`Or set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
379
475
|
console.log();
|
|
380
|
-
info("
|
|
476
|
+
info("Wallet");
|
|
381
477
|
console.log();
|
|
382
|
-
dim(`
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
478
|
+
console.log(pc.dim(` Source: ${wallet.source}`));
|
|
479
|
+
const [evmResult, solResult] = await Promise.allSettled([wallet.evmAddress ? fetchEvmBalances(wallet.evmAddress) : Promise.resolve(null), wallet.solanaAddress ? fetchSolanaBalances(wallet.solanaAddress) : Promise.resolve(null)]);
|
|
480
|
+
const evm = evmResult.status === "fulfilled" ? evmResult.value : null;
|
|
481
|
+
const sol = solResult.status === "fulfilled" ? solResult.value : null;
|
|
482
|
+
if (wallet.evmAddress) {
|
|
483
|
+
const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
|
|
484
|
+
console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
485
|
+
}
|
|
486
|
+
if (wallet.solanaAddress) {
|
|
487
|
+
const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
|
|
488
|
+
console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
|
|
386
489
|
}
|
|
387
490
|
console.log();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
dim(
|
|
393
|
-
|
|
394
|
-
|
|
491
|
+
const records = readHistory(getHistoryPath());
|
|
492
|
+
if (records.length > 0) {
|
|
493
|
+
const spend = calcSpend(records);
|
|
494
|
+
const recent = records.slice(-10);
|
|
495
|
+
dim(" Recent transactions:");
|
|
496
|
+
for (const r of recent) {
|
|
497
|
+
const line = formatTxLine(r, { verbose: flags.verbose }).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
498
|
+
console.log(line);
|
|
499
|
+
}
|
|
500
|
+
console.log();
|
|
501
|
+
console.log(pc.dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`));
|
|
502
|
+
} else dim(" No transactions yet.");
|
|
503
|
+
console.log();
|
|
504
|
+
console.log(pc.dim(" See also: wallet history, wallet fund, wallet export-key"));
|
|
505
|
+
console.log();
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/commands/status.ts
|
|
511
|
+
async function displayStatus() {
|
|
512
|
+
const wallet = resolveWallet();
|
|
513
|
+
const config = loadConfig();
|
|
514
|
+
const records = readHistory(getHistoryPath());
|
|
515
|
+
const spend = calcSpend(records);
|
|
516
|
+
console.log();
|
|
517
|
+
console.log(pc.cyan(pc.bold("x402-proxy")));
|
|
518
|
+
console.log(pc.dim("curl for x402 paid APIs"));
|
|
519
|
+
console.log();
|
|
520
|
+
if (wallet.source === "none") {
|
|
521
|
+
console.log(pc.yellow(" No wallet configured."));
|
|
522
|
+
console.log(pc.dim(` Run ${pc.cyan("$ npx x402-proxy setup")} to create one.`));
|
|
523
|
+
} else {
|
|
524
|
+
const [evmResult, solResult] = await Promise.allSettled([wallet.evmAddress ? fetchEvmBalances(wallet.evmAddress) : Promise.resolve(null), wallet.solanaAddress ? fetchSolanaBalances(wallet.solanaAddress) : Promise.resolve(null)]);
|
|
525
|
+
const evm = evmResult.status === "fulfilled" ? evmResult.value : null;
|
|
526
|
+
const sol = solResult.status === "fulfilled" ? solResult.value : null;
|
|
527
|
+
if (wallet.evmAddress) {
|
|
528
|
+
const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
|
|
529
|
+
console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
530
|
+
}
|
|
531
|
+
if (wallet.solanaAddress) {
|
|
532
|
+
const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
|
|
533
|
+
console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
|
|
534
|
+
}
|
|
535
|
+
if (config?.spendLimitDaily || config?.spendLimitPerTx) {
|
|
536
|
+
console.log();
|
|
537
|
+
if (config.spendLimitDaily) {
|
|
538
|
+
const pct = config.spendLimitDaily > 0 ? Math.round(spend.today / config.spendLimitDaily * 100) : 0;
|
|
539
|
+
dim(` Daily limit: ${spend.today.toFixed(4)} / ${config.spendLimitDaily} USDC (${pct}%)`);
|
|
540
|
+
}
|
|
541
|
+
if (config.spendLimitPerTx) dim(` Per-tx limit: ${config.spendLimitPerTx} USDC`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
console.log();
|
|
545
|
+
if (spend.count > 0) {
|
|
546
|
+
const recent = records.slice(-5);
|
|
547
|
+
dim(" Recent transactions:");
|
|
548
|
+
for (const r of recent) {
|
|
549
|
+
const line = formatTxLine(r).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
550
|
+
console.log(line);
|
|
395
551
|
}
|
|
396
552
|
console.log();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
553
|
+
dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} tx`);
|
|
554
|
+
} else dim(" No payment history yet.");
|
|
555
|
+
console.log();
|
|
556
|
+
if (config?.defaultNetwork) dim(` Network: ${config.defaultNetwork}`);
|
|
557
|
+
dim(` Config: ${getConfigDirShort()}`);
|
|
558
|
+
}
|
|
559
|
+
const statusCommand = buildCommand({
|
|
560
|
+
docs: { brief: "Show configuration and wallet status" },
|
|
561
|
+
parameters: {
|
|
562
|
+
flags: {},
|
|
563
|
+
positional: {
|
|
564
|
+
kind: "tuple",
|
|
565
|
+
parameters: []
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
async func() {
|
|
569
|
+
await displayStatus();
|
|
403
570
|
console.log();
|
|
404
571
|
}
|
|
405
572
|
});
|
|
406
573
|
|
|
407
574
|
//#endregion
|
|
408
|
-
export {
|
|
575
|
+
export { calcSpend as C, appendHistory as S, readHistory as T, getWalletPath as _, resolveWallet as a, saveConfig as b, generateMnemonic$1 as c, info as d, isTTY as f, getHistoryPath as g, getConfigDirShort as h, buildX402Client as i, dim as l, ensureConfigDir as m, statusCommand as n, deriveEvmKeypair as o, warn as p, walletInfoCommand as r, deriveSolanaKeypair as s, displayStatus as t, error as u, isConfigured as v, formatTxLine as w, saveWalletFile as x, loadConfig as y };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-proxy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base and Solana.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -26,26 +26,22 @@
|
|
|
26
26
|
"@scure/base": "^2.0.0",
|
|
27
27
|
"@scure/bip32": "^2.0.1",
|
|
28
28
|
"@scure/bip39": "^2.0.1",
|
|
29
|
+
"@solana/kit": "^6.0.0",
|
|
29
30
|
"@stricli/core": "^1.2.6",
|
|
30
31
|
"@x402/evm": "^2.6.0",
|
|
31
32
|
"@x402/fetch": "^2.6.0",
|
|
32
33
|
"@x402/mcp": "^2.6.0",
|
|
33
34
|
"@x402/svm": "^2.6.0",
|
|
35
|
+
"ethers": "^6.0.0",
|
|
34
36
|
"picocolors": "^1.1.1",
|
|
37
|
+
"viem": "^2.0.0",
|
|
35
38
|
"yaml": "^2.8.2"
|
|
36
39
|
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"@solana/kit": "^6.0.0",
|
|
39
|
-
"ethers": "^6.0.0",
|
|
40
|
-
"viem": "^2.0.0"
|
|
41
|
-
},
|
|
42
40
|
"devDependencies": {
|
|
43
|
-
"@solana/kit": "^6.0.0",
|
|
44
41
|
"@types/node": "^22.0.0",
|
|
45
42
|
"publint": "^0.3.17",
|
|
46
43
|
"tsdown": "^0.20.3",
|
|
47
|
-
"typescript": "^5.9.0"
|
|
48
|
-
"viem": "^2.0.0"
|
|
44
|
+
"typescript": "^5.9.0"
|
|
49
45
|
},
|
|
50
46
|
"files": [
|
|
51
47
|
"dist/**",
|
|
@@ -54,15 +50,16 @@
|
|
|
54
50
|
],
|
|
55
51
|
"keywords": [
|
|
56
52
|
"x402",
|
|
57
|
-
"
|
|
58
|
-
"proxy",
|
|
53
|
+
"http-402",
|
|
59
54
|
"mcp",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
55
|
+
"mcp-proxy",
|
|
56
|
+
"ai-agent",
|
|
57
|
+
"agentic-commerce",
|
|
62
58
|
"base",
|
|
59
|
+
"solana",
|
|
63
60
|
"usdc",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
61
|
+
"coinbase",
|
|
62
|
+
"cli"
|
|
66
63
|
],
|
|
67
64
|
"license": "Apache-2.0",
|
|
68
65
|
"engines": {
|
package/dist/status-CPBQ0UBZ.js
DELETED