x402-proxy 0.1.2 → 0.2.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.
- package/README.md +89 -59
- package/dist/bin/cli.js +596 -0
- package/dist/index.js +1 -4
- package/dist/status-DnxuVdOP.js +4 -0
- package/dist/status-J-RoszDZ.js +409 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,87 +1,117 @@
|
|
|
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
|
-
|
|
8
|
+
npx x402-proxy https://twitter.surf.cascade.fyi/search?q=cascade_fyi
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
That's it. The endpoint returns 402, x402-proxy pays and streams the response.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
First time? Set up a wallet:
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
extractTxSignature,
|
|
22
|
-
loadSvmWallet,
|
|
23
|
-
loadEvmWallet,
|
|
24
|
-
} from "x402-proxy";
|
|
15
|
+
```bash
|
|
16
|
+
npx x402-proxy setup # generate wallet from BIP-39 mnemonic
|
|
17
|
+
npx x402-proxy wallet fund # see where to send USDC
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
One mnemonic derives both EVM (Base) and Solana keypairs. Fund either chain and go.
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
const svmWallet = await loadSvmWallet("/path/to/keypair.json");
|
|
28
|
-
const client = x402Client([ExactSvmScheme(svmWallet)]);
|
|
22
|
+
## MCP Proxy
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
const { x402Fetch, shiftPayment } = createX402ProxyHandler({ client });
|
|
24
|
+
Let your AI agent consume any paid MCP server. Configure in Claude, Cursor, or any MCP client:
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"paid-service": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["x402-proxy", "mcp", "https://mcp.example.com/sse"],
|
|
32
|
+
"env": {
|
|
33
|
+
"X402_PROXY_WALLET_MNEMONIC": "your 24 words here"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
37
38
|
```
|
|
38
39
|
|
|
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.
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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/search?q=x402
|
|
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)
|
|
60
71
|
```
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
All commands support `--help` for details.
|
|
74
|
+
|
|
75
|
+
## Wallet
|
|
76
|
+
|
|
77
|
+
A single BIP-39 mnemonic derives both chains:
|
|
78
|
+
- **Solana:** SLIP-10 Ed25519 at `m/44'/501'/0'/0'`
|
|
79
|
+
- **EVM:** BIP-32 secp256k1 at `m/44'/60'/0'/0/0`
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
Config stored at `$XDG_CONFIG_HOME/x402-proxy/` (default `~/.config/x402-proxy/`).
|
|
82
|
+
|
|
83
|
+
### Export keys for other tools
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Pipe-safe - outputs bare key to stdout
|
|
87
|
+
MY_KEY=$(npx x402-proxy wallet export-key evm)
|
|
88
|
+
```
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
- `extractTxSignature(response)` - extracts on-chain TX signature from payment response headers
|
|
90
|
+
## Env Vars
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
Override wallet per-instance (useful for MCP configs):
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
```
|
|
95
|
+
X402_PROXY_WALLET_MNEMONIC # BIP-39 mnemonic (derives both chains)
|
|
96
|
+
X402_PROXY_WALLET_EVM_KEY # EVM private key (hex)
|
|
97
|
+
X402_PROXY_WALLET_SOLANA_KEY # Solana private key (base58)
|
|
98
|
+
```
|
|
73
99
|
|
|
74
|
-
|
|
100
|
+
Resolution order: flags > env vars > mnemonic env > `wallet.json` file.
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
- `readHistory(path)` - read all records from a JSONL history file
|
|
78
|
-
- `calcSpend(records)` - aggregate USDC spend (today/total/count)
|
|
79
|
-
- `explorerUrl(net, tx)` - generate block explorer URL (Solscan, Basescan, Etherscan)
|
|
80
|
-
- `formatTxLine(record)` - format a record as a markdown line with explorer link
|
|
102
|
+
## Library Usage
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
```ts
|
|
105
|
+
import {
|
|
106
|
+
createX402ProxyHandler,
|
|
107
|
+
extractTxSignature,
|
|
108
|
+
appendHistory,
|
|
109
|
+
readHistory,
|
|
110
|
+
calcSpend,
|
|
111
|
+
} from "x402-proxy";
|
|
112
|
+
```
|
|
83
113
|
|
|
84
|
-
|
|
114
|
+
See the [library API docs](https://github.com/cascade-protocol/x402-proxy/tree/main/packages/x402-proxy#library-api) for details.
|
|
85
115
|
|
|
86
116
|
## License
|
|
87
117
|
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { C as readHistory, S as formatTxLine, _ as isConfigured, a as deriveEvmKeypair, b as appendHistory, c as dim, d as isTTY, f as warn, g as getWalletPath, h as getHistoryPath, i as resolveWallet, l as error, m as getConfigDir, n as statusCommand, o as deriveSolanaKeypair, p as ensureConfigDir, r as buildX402Client, s as generateMnemonic, u as info, v as saveConfig, x as calcSpend, y as saveWalletFile } from "../status-J-RoszDZ.js";
|
|
3
|
+
import { buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { decodePaymentResponseHeader, wrapFetchWithPayment } from "@x402/fetch";
|
|
6
|
+
import { base58 } from "@scure/base";
|
|
7
|
+
import * as prompts from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
//#region src/handler.ts
|
|
10
|
+
/**
|
|
11
|
+
* Extract the on-chain transaction signature from an x402 payment response header.
|
|
12
|
+
*/
|
|
13
|
+
function extractTxSignature(response) {
|
|
14
|
+
const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
15
|
+
if (!header) return void 0;
|
|
16
|
+
try {
|
|
17
|
+
return decodePaymentResponseHeader(header).transaction ?? void 0;
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create an x402 proxy handler that wraps fetch with automatic payment.
|
|
24
|
+
*
|
|
25
|
+
* Chain-agnostic: accepts a pre-configured x402Client with any registered
|
|
26
|
+
* schemes (SVM, EVM, etc). The handler captures payment info via the
|
|
27
|
+
* onAfterPaymentCreation hook. Callers use `x402Fetch` for requests that
|
|
28
|
+
* may require x402 payment, and `shiftPayment` to retrieve captured
|
|
29
|
+
* payment info after each call.
|
|
30
|
+
*/
|
|
31
|
+
function createX402ProxyHandler(opts) {
|
|
32
|
+
const { client } = opts;
|
|
33
|
+
const paymentQueue = [];
|
|
34
|
+
client.onAfterPaymentCreation(async (hookCtx) => {
|
|
35
|
+
const raw = hookCtx.selectedRequirements.amount;
|
|
36
|
+
paymentQueue.push({
|
|
37
|
+
network: hookCtx.selectedRequirements.network,
|
|
38
|
+
payTo: hookCtx.selectedRequirements.payTo,
|
|
39
|
+
amount: raw?.startsWith("debug.") ? raw.slice(6) : raw,
|
|
40
|
+
asset: hookCtx.selectedRequirements.asset
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
x402Fetch: wrapFetchWithPayment(globalThis.fetch, client),
|
|
45
|
+
shiftPayment: () => paymentQueue.shift()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/commands/fetch.ts
|
|
51
|
+
const fetchCommand = buildCommand({
|
|
52
|
+
docs: { brief: "Make a paid HTTP request (default command)" },
|
|
53
|
+
parameters: {
|
|
54
|
+
flags: {
|
|
55
|
+
method: {
|
|
56
|
+
kind: "parsed",
|
|
57
|
+
brief: "HTTP method",
|
|
58
|
+
parse: String,
|
|
59
|
+
default: "GET"
|
|
60
|
+
},
|
|
61
|
+
body: {
|
|
62
|
+
kind: "parsed",
|
|
63
|
+
brief: "Request body",
|
|
64
|
+
parse: String,
|
|
65
|
+
optional: true
|
|
66
|
+
},
|
|
67
|
+
header: {
|
|
68
|
+
kind: "parsed",
|
|
69
|
+
brief: "HTTP header (Key: Value), repeatable",
|
|
70
|
+
parse: String,
|
|
71
|
+
variadic: true,
|
|
72
|
+
optional: true
|
|
73
|
+
},
|
|
74
|
+
evmKey: {
|
|
75
|
+
kind: "parsed",
|
|
76
|
+
brief: "EVM private key (hex)",
|
|
77
|
+
parse: String,
|
|
78
|
+
optional: true
|
|
79
|
+
},
|
|
80
|
+
solanaKey: {
|
|
81
|
+
kind: "parsed",
|
|
82
|
+
brief: "Solana private key (base58)",
|
|
83
|
+
parse: String,
|
|
84
|
+
optional: true
|
|
85
|
+
},
|
|
86
|
+
json: {
|
|
87
|
+
kind: "boolean",
|
|
88
|
+
brief: "Force JSON output",
|
|
89
|
+
default: false
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
positional: {
|
|
93
|
+
kind: "tuple",
|
|
94
|
+
parameters: [{
|
|
95
|
+
brief: "URL to request",
|
|
96
|
+
parse: String,
|
|
97
|
+
optional: true
|
|
98
|
+
}]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async func(flags, url) {
|
|
102
|
+
if (!url) {
|
|
103
|
+
if (isConfigured()) {
|
|
104
|
+
const { displayStatus } = await import("../status-DnxuVdOP.js");
|
|
105
|
+
displayStatus();
|
|
106
|
+
} else {
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(pc.cyan("x402-proxy") + pc.dim(" - pay for any x402 resource"));
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(pc.dim(" Get started:"));
|
|
111
|
+
console.log(` ${pc.cyan("x402-proxy setup")} Create a wallet`);
|
|
112
|
+
console.log(` ${pc.cyan("x402-proxy <url>")} Make a paid request`);
|
|
113
|
+
console.log(` ${pc.cyan("x402-proxy mcp <url>")} MCP proxy for agents`);
|
|
114
|
+
console.log(` ${pc.cyan("x402-proxy wallet")} Wallet info`);
|
|
115
|
+
console.log(` ${pc.cyan("x402-proxy --help")} All commands`);
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let parsedUrl;
|
|
121
|
+
try {
|
|
122
|
+
parsedUrl = new URL(url);
|
|
123
|
+
} catch {
|
|
124
|
+
error(`Invalid URL: ${url}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const wallet = resolveWallet({
|
|
128
|
+
evmKey: flags.evmKey,
|
|
129
|
+
solanaKey: flags.solanaKey
|
|
130
|
+
});
|
|
131
|
+
if (wallet.source === "none") {
|
|
132
|
+
error("No wallet configured.");
|
|
133
|
+
console.error(pc.dim(`Run ${pc.cyan("x402-proxy setup")} or set X402_PROXY_WALLET_MNEMONIC`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const { x402Fetch, shiftPayment } = createX402ProxyHandler({ client: await buildX402Client(wallet) });
|
|
137
|
+
const headers = new Headers();
|
|
138
|
+
if (flags.header) for (const h of flags.header) {
|
|
139
|
+
const idx = h.indexOf(":");
|
|
140
|
+
if (idx > 0) headers.set(h.slice(0, idx).trim(), h.slice(idx + 1).trim());
|
|
141
|
+
}
|
|
142
|
+
const method = flags.method || "GET";
|
|
143
|
+
const init = {
|
|
144
|
+
method,
|
|
145
|
+
headers
|
|
146
|
+
};
|
|
147
|
+
if (flags.body) init.body = flags.body;
|
|
148
|
+
if (isTTY()) dim(` ${method} ${parsedUrl.toString()}`);
|
|
149
|
+
const startMs = Date.now();
|
|
150
|
+
let response;
|
|
151
|
+
try {
|
|
152
|
+
response = await x402Fetch(parsedUrl.toString(), init);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
error(`Request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const elapsedMs = Date.now() - startMs;
|
|
158
|
+
const payment = shiftPayment();
|
|
159
|
+
const txSig = extractTxSignature(response);
|
|
160
|
+
if (payment && isTTY()) {
|
|
161
|
+
info(` Payment: ${payment.amount ?? "?"} (${payment.network ?? "unknown"})`);
|
|
162
|
+
if (txSig) dim(` Tx: ${txSig}`);
|
|
163
|
+
}
|
|
164
|
+
if (isTTY()) dim(` ${response.status} ${response.statusText} (${elapsedMs}ms)`);
|
|
165
|
+
if (payment) {
|
|
166
|
+
ensureConfigDir();
|
|
167
|
+
const record = {
|
|
168
|
+
t: Date.now(),
|
|
169
|
+
ok: response.ok,
|
|
170
|
+
kind: "x402_payment",
|
|
171
|
+
net: payment.network ?? "unknown",
|
|
172
|
+
from: wallet.evmAddress ?? wallet.solanaAddress ?? "unknown",
|
|
173
|
+
to: payment.payTo,
|
|
174
|
+
tx: txSig,
|
|
175
|
+
amount: payment.amount ? Number(payment.amount) / 1e6 : void 0,
|
|
176
|
+
token: "USDC",
|
|
177
|
+
ms: elapsedMs,
|
|
178
|
+
label: parsedUrl.hostname
|
|
179
|
+
};
|
|
180
|
+
appendHistory(getHistoryPath(), record);
|
|
181
|
+
}
|
|
182
|
+
if (response.body) {
|
|
183
|
+
const reader = response.body.getReader();
|
|
184
|
+
try {
|
|
185
|
+
while (true) {
|
|
186
|
+
const { done, value } = await reader.read();
|
|
187
|
+
if (done) break;
|
|
188
|
+
process.stdout.write(value);
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
reader.releaseLock();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (isTTY() && response.body) process.stdout.write("\n");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/commands/mcp.ts
|
|
200
|
+
const mcpCommand = buildCommand({
|
|
201
|
+
docs: { brief: "Start MCP stdio proxy with x402 payment (alpha)" },
|
|
202
|
+
parameters: {
|
|
203
|
+
flags: {
|
|
204
|
+
evmKey: {
|
|
205
|
+
kind: "parsed",
|
|
206
|
+
brief: "EVM private key (hex)",
|
|
207
|
+
parse: String,
|
|
208
|
+
optional: true
|
|
209
|
+
},
|
|
210
|
+
solanaKey: {
|
|
211
|
+
kind: "parsed",
|
|
212
|
+
brief: "Solana private key (base58)",
|
|
213
|
+
parse: String,
|
|
214
|
+
optional: true
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
positional: {
|
|
218
|
+
kind: "tuple",
|
|
219
|
+
parameters: [{
|
|
220
|
+
brief: "Remote MCP server URL",
|
|
221
|
+
parse: String
|
|
222
|
+
}]
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
async func(flags, remoteUrl) {
|
|
226
|
+
const wallet = resolveWallet({
|
|
227
|
+
evmKey: flags.evmKey,
|
|
228
|
+
solanaKey: flags.solanaKey
|
|
229
|
+
});
|
|
230
|
+
if (wallet.source === "none") {
|
|
231
|
+
error("No wallet configured. Set X402_PROXY_WALLET_MNEMONIC or run x402-proxy setup.");
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
warn("Note: MCP proxy is alpha - please report issues.");
|
|
235
|
+
dim(`x402-proxy MCP proxy -> ${remoteUrl}`);
|
|
236
|
+
if (wallet.evmAddress) dim(` EVM: ${wallet.evmAddress}`);
|
|
237
|
+
if (wallet.solanaAddress) dim(` Solana: ${wallet.solanaAddress}`);
|
|
238
|
+
const x402PaymentClient = await buildX402Client(wallet);
|
|
239
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
240
|
+
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
241
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
242
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
243
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
244
|
+
const { x402MCPClient } = await import("@x402/mcp");
|
|
245
|
+
const x402Mcp = new x402MCPClient(new Client({
|
|
246
|
+
name: "x402-proxy",
|
|
247
|
+
version: "0.2.0"
|
|
248
|
+
}), x402PaymentClient, {
|
|
249
|
+
autoPayment: true,
|
|
250
|
+
onPaymentRequested: (ctx) => {
|
|
251
|
+
const accept = ctx.paymentRequired.accepts?.[0];
|
|
252
|
+
if (accept) warn(` Payment: ${accept.amount} on ${accept.network} for tool "${ctx.toolName}"`);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
x402Mcp.onAfterPayment(async (ctx) => {
|
|
257
|
+
ensureConfigDir();
|
|
258
|
+
const tx = ctx.settleResponse?.transaction;
|
|
259
|
+
const accept = ctx.paymentPayload;
|
|
260
|
+
const record = {
|
|
261
|
+
t: Date.now(),
|
|
262
|
+
ok: true,
|
|
263
|
+
kind: "x402_payment",
|
|
264
|
+
net: accept.network ?? "unknown",
|
|
265
|
+
from: wallet.evmAddress ?? wallet.solanaAddress ?? "unknown",
|
|
266
|
+
tx: typeof tx === "string" ? tx : void 0,
|
|
267
|
+
token: "USDC",
|
|
268
|
+
label: `mcp:${ctx.toolName}`
|
|
269
|
+
};
|
|
270
|
+
appendHistory(getHistoryPath(), record);
|
|
271
|
+
});
|
|
272
|
+
let connected = false;
|
|
273
|
+
try {
|
|
274
|
+
const transport = new StreamableHTTPClientTransport(new URL(remoteUrl));
|
|
275
|
+
await x402Mcp.connect(transport);
|
|
276
|
+
connected = true;
|
|
277
|
+
dim(" Connected via StreamableHTTP");
|
|
278
|
+
} catch {}
|
|
279
|
+
if (!connected) try {
|
|
280
|
+
const transport = new SSEClientTransport(new URL(remoteUrl));
|
|
281
|
+
await x402Mcp.connect(transport);
|
|
282
|
+
connected = true;
|
|
283
|
+
dim(" Connected via SSE");
|
|
284
|
+
} catch (err) {
|
|
285
|
+
error(`Failed to connect to ${remoteUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const { tools } = await x402Mcp.listTools();
|
|
289
|
+
dim(` ${tools.length} tools available`);
|
|
290
|
+
const localServer = new McpServer({
|
|
291
|
+
name: "x402-proxy",
|
|
292
|
+
version: "0.2.0"
|
|
293
|
+
});
|
|
294
|
+
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) => {
|
|
295
|
+
const result = await x402Mcp.callTool(tool.name, args);
|
|
296
|
+
return {
|
|
297
|
+
content: result.content,
|
|
298
|
+
isError: result.isError
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
const { resources } = await x402Mcp.listResources();
|
|
303
|
+
if (resources.length > 0) {
|
|
304
|
+
dim(` ${resources.length} resources available`);
|
|
305
|
+
for (const resource of resources) localServer.resource(resource.name, resource.uri, resource.description ? { description: resource.description } : {}, async (uri) => {
|
|
306
|
+
return { contents: (await x402Mcp.readResource({ uri: uri.href })).contents.map((c) => ({
|
|
307
|
+
uri: c.uri,
|
|
308
|
+
text: "text" in c ? c.text : ""
|
|
309
|
+
})) };
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch {}
|
|
313
|
+
const stdioTransport = new StdioServerTransport();
|
|
314
|
+
await localServer.connect(stdioTransport);
|
|
315
|
+
dim(" MCP proxy running (stdio)");
|
|
316
|
+
process.stdin.on("end", async () => {
|
|
317
|
+
await x402Mcp.close();
|
|
318
|
+
process.exit(0);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/commands/setup.ts
|
|
325
|
+
const setupCommand = buildCommand({
|
|
326
|
+
docs: { brief: "Set up x402-proxy with a new wallet" },
|
|
327
|
+
parameters: {
|
|
328
|
+
flags: { force: {
|
|
329
|
+
kind: "boolean",
|
|
330
|
+
brief: "Overwrite existing configuration",
|
|
331
|
+
default: false
|
|
332
|
+
} },
|
|
333
|
+
positional: {
|
|
334
|
+
kind: "tuple",
|
|
335
|
+
parameters: []
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
async func(flags) {
|
|
339
|
+
if (isConfigured() && !flags.force) {
|
|
340
|
+
prompts.log.warn(`Already configured. Wallet at ${pc.dim(getWalletPath())}\nUse ${pc.cyan("x402-proxy setup --force")} to reconfigure.`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
prompts.intro(pc.cyan("x402-proxy setup"));
|
|
344
|
+
prompts.log.info("This will generate a single BIP-39 mnemonic that derives wallets for both Solana and EVM chains.");
|
|
345
|
+
const action = await prompts.select({
|
|
346
|
+
message: "How would you like to set up your wallet?",
|
|
347
|
+
options: [{
|
|
348
|
+
value: "generate",
|
|
349
|
+
label: "Generate a new mnemonic"
|
|
350
|
+
}, {
|
|
351
|
+
value: "import",
|
|
352
|
+
label: "Import an existing mnemonic"
|
|
353
|
+
}]
|
|
354
|
+
});
|
|
355
|
+
if (prompts.isCancel(action)) {
|
|
356
|
+
prompts.cancel("Setup cancelled.");
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
let mnemonic;
|
|
360
|
+
if (action === "generate") {
|
|
361
|
+
mnemonic = generateMnemonic();
|
|
362
|
+
prompts.log.warn("Write down your mnemonic and store it safely. It will NOT be shown again.");
|
|
363
|
+
prompts.log.message(pc.bold(mnemonic));
|
|
364
|
+
} else {
|
|
365
|
+
const input = await prompts.text({
|
|
366
|
+
message: "Enter your 24-word mnemonic:",
|
|
367
|
+
validate: (v = "") => {
|
|
368
|
+
const words = v.trim().split(/\s+/);
|
|
369
|
+
if (words.length !== 12 && words.length !== 24) return "Mnemonic must be 12 or 24 words";
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (prompts.isCancel(input)) {
|
|
373
|
+
prompts.cancel("Setup cancelled.");
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
mnemonic = input.trim();
|
|
377
|
+
}
|
|
378
|
+
const evm = deriveEvmKeypair(mnemonic);
|
|
379
|
+
const sol = deriveSolanaKeypair(mnemonic);
|
|
380
|
+
prompts.log.success(`EVM address: ${pc.green(evm.address)}`);
|
|
381
|
+
prompts.log.success(`Solana address: ${pc.green(sol.address)}`);
|
|
382
|
+
saveWalletFile({
|
|
383
|
+
version: 1,
|
|
384
|
+
mnemonic,
|
|
385
|
+
addresses: {
|
|
386
|
+
evm: evm.address,
|
|
387
|
+
solana: sol.address
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
saveConfig({});
|
|
391
|
+
prompts.log.info(`Config directory: ${pc.dim(getConfigDir())}`);
|
|
392
|
+
prompts.log.step("Fund your wallets to start using x402 resources:");
|
|
393
|
+
prompts.log.message(` Solana (USDC): Send USDC to ${pc.cyan(sol.address)}`);
|
|
394
|
+
prompts.log.message(` EVM (USDC): Send USDC to ${pc.cyan(evm.address)} on Base`);
|
|
395
|
+
prompts.outro(pc.green("Setup complete!"));
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/commands/wallet.ts
|
|
401
|
+
const walletInfoCommand = buildCommand({
|
|
402
|
+
docs: { brief: "Show wallet addresses" },
|
|
403
|
+
parameters: {
|
|
404
|
+
flags: {},
|
|
405
|
+
positional: {
|
|
406
|
+
kind: "tuple",
|
|
407
|
+
parameters: []
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
func() {
|
|
411
|
+
const wallet = resolveWallet();
|
|
412
|
+
if (wallet.source === "none") {
|
|
413
|
+
console.log(pc.yellow("No wallet configured."));
|
|
414
|
+
console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
415
|
+
console.log(pc.dim(`Or set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
console.log();
|
|
419
|
+
info("Wallet");
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(pc.dim(` Source: ${wallet.source}`));
|
|
422
|
+
if (wallet.evmAddress) console.log(` EVM: ${pc.green(wallet.evmAddress)}`);
|
|
423
|
+
if (wallet.solanaAddress) console.log(` Solana: ${pc.green(wallet.solanaAddress)}`);
|
|
424
|
+
console.log();
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/commands/wallet-export.ts
|
|
430
|
+
const walletExportCommand = buildCommand({
|
|
431
|
+
docs: { brief: "Export private key to stdout (pipe-safe)" },
|
|
432
|
+
parameters: {
|
|
433
|
+
flags: {},
|
|
434
|
+
positional: {
|
|
435
|
+
kind: "tuple",
|
|
436
|
+
parameters: [{
|
|
437
|
+
brief: "Chain to export: evm or solana",
|
|
438
|
+
parse: (input) => {
|
|
439
|
+
const v = input.toLowerCase();
|
|
440
|
+
if (v !== "evm" && v !== "solana") throw new Error("Must be 'evm' or 'solana'");
|
|
441
|
+
return v;
|
|
442
|
+
}
|
|
443
|
+
}]
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
func(flags, chain) {
|
|
447
|
+
const wallet = resolveWallet();
|
|
448
|
+
if (wallet.source === "none") {
|
|
449
|
+
error("No wallet configured.");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
if (chain === "evm") {
|
|
453
|
+
if (!wallet.evmKey) {
|
|
454
|
+
error("No EVM key available.");
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
warn("Warning: private key will be printed to stdout.");
|
|
458
|
+
process.stdout.write(wallet.evmKey);
|
|
459
|
+
} else {
|
|
460
|
+
if (!wallet.solanaKey) {
|
|
461
|
+
error("No Solana key available.");
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
warn("Warning: private key will be printed to stdout.");
|
|
465
|
+
process.stdout.write(base58.encode(wallet.solanaKey.slice(0, 32)));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/commands/wallet-fund.ts
|
|
472
|
+
const walletFundCommand = buildCommand({
|
|
473
|
+
docs: { brief: "Show wallet funding instructions" },
|
|
474
|
+
parameters: {
|
|
475
|
+
flags: {},
|
|
476
|
+
positional: {
|
|
477
|
+
kind: "tuple",
|
|
478
|
+
parameters: []
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
func() {
|
|
482
|
+
const wallet = resolveWallet();
|
|
483
|
+
if (wallet.source === "none") {
|
|
484
|
+
console.log(pc.yellow("No wallet configured."));
|
|
485
|
+
console.log(pc.dim(`Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
console.log();
|
|
489
|
+
info("Funding Instructions");
|
|
490
|
+
console.log();
|
|
491
|
+
if (wallet.solanaAddress) {
|
|
492
|
+
console.log(pc.bold(" Solana (USDC):"));
|
|
493
|
+
console.log(` Send USDC to: ${pc.green(wallet.solanaAddress)}`);
|
|
494
|
+
console.log(pc.dim(" Network: Solana Mainnet"));
|
|
495
|
+
console.log();
|
|
496
|
+
}
|
|
497
|
+
if (wallet.evmAddress) {
|
|
498
|
+
console.log(pc.bold(" Base (USDC):"));
|
|
499
|
+
console.log(` Send USDC to: ${pc.green(wallet.evmAddress)}`);
|
|
500
|
+
console.log(pc.dim(" Network: Base (Chain ID 8453)"));
|
|
501
|
+
console.log();
|
|
502
|
+
}
|
|
503
|
+
console.log(pc.dim(" Tip: Most x402 services accept USDC on Base or Solana."));
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/commands/wallet-history.ts
|
|
510
|
+
const walletHistoryCommand = buildCommand({
|
|
511
|
+
docs: { brief: "Show payment history" },
|
|
512
|
+
parameters: {
|
|
513
|
+
flags: {
|
|
514
|
+
limit: {
|
|
515
|
+
kind: "parsed",
|
|
516
|
+
brief: "Number of entries to show",
|
|
517
|
+
parse: Number,
|
|
518
|
+
default: "20"
|
|
519
|
+
},
|
|
520
|
+
json: {
|
|
521
|
+
kind: "boolean",
|
|
522
|
+
brief: "Output raw JSONL",
|
|
523
|
+
default: false
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
positional: {
|
|
527
|
+
kind: "tuple",
|
|
528
|
+
parameters: []
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
func(flags) {
|
|
532
|
+
const records = readHistory(getHistoryPath());
|
|
533
|
+
if (records.length === 0) {
|
|
534
|
+
console.log(pc.dim("No payment history yet."));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (flags.json) {
|
|
538
|
+
const slice = records.slice(-flags.limit);
|
|
539
|
+
for (const r of slice) process.stdout.write(`${JSON.stringify(r)}\n`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const spend = calcSpend(records);
|
|
543
|
+
const slice = records.slice(-flags.limit);
|
|
544
|
+
console.log();
|
|
545
|
+
info("Payment History");
|
|
546
|
+
console.log();
|
|
547
|
+
for (const r of slice) {
|
|
548
|
+
const line = formatTxLine(r).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
549
|
+
console.log(line);
|
|
550
|
+
}
|
|
551
|
+
console.log();
|
|
552
|
+
console.log(pc.dim(` Today: ${spend.today.toFixed(4)} USDC | Total: ${spend.total.toFixed(4)} USDC | ${spend.count} transactions`));
|
|
553
|
+
console.log();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/app.ts
|
|
559
|
+
const routes = buildRouteMap({
|
|
560
|
+
routes: {
|
|
561
|
+
fetch: fetchCommand,
|
|
562
|
+
mcp: mcpCommand,
|
|
563
|
+
wallet: buildRouteMap({
|
|
564
|
+
routes: {
|
|
565
|
+
info: walletInfoCommand,
|
|
566
|
+
history: walletHistoryCommand,
|
|
567
|
+
fund: walletFundCommand,
|
|
568
|
+
"export-key": walletExportCommand
|
|
569
|
+
},
|
|
570
|
+
defaultCommand: "info",
|
|
571
|
+
docs: { brief: "Wallet management" }
|
|
572
|
+
}),
|
|
573
|
+
setup: setupCommand,
|
|
574
|
+
status: statusCommand
|
|
575
|
+
},
|
|
576
|
+
defaultCommand: "fetch",
|
|
577
|
+
docs: { brief: "curl for x402 paid APIs" }
|
|
578
|
+
});
|
|
579
|
+
const app = buildApplication(routes, {
|
|
580
|
+
name: "x402-proxy",
|
|
581
|
+
versionInfo: { currentVersion: "0.2.1" },
|
|
582
|
+
scanner: { caseStyle: "allow-kebab-for-camel" }
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/context.ts
|
|
587
|
+
function buildContext(process) {
|
|
588
|
+
return { process };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/bin/cli.ts
|
|
593
|
+
await run(app, process.argv.slice(2), buildContext(process));
|
|
594
|
+
|
|
595
|
+
//#endregion
|
|
596
|
+
export { };
|
package/dist/index.js
CHANGED
|
@@ -119,10 +119,7 @@ const KIND_LABELS = {
|
|
|
119
119
|
};
|
|
120
120
|
function explorerUrl(net, tx) {
|
|
121
121
|
if (net.startsWith("eip155:")) {
|
|
122
|
-
|
|
123
|
-
if (chainId === "8453") return `https://basescan.org/tx/${tx}`;
|
|
124
|
-
if (chainId === "84532") return `https://sepolia.basescan.org/tx/${tx}`;
|
|
125
|
-
if (chainId === "1") return `https://etherscan.io/tx/${tx}`;
|
|
122
|
+
if (net.split(":")[1] === "8453") return `https://basescan.org/tx/${tx}`;
|
|
126
123
|
return `https://basescan.org/tx/${tx}`;
|
|
127
124
|
}
|
|
128
125
|
return `https://solscan.io/tx/${tx}`;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildCommand } from "@stricli/core";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { x402Client } from "@x402/fetch";
|
|
5
|
+
import fs, { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { parse, stringify } from "yaml";
|
|
9
|
+
import { base58 } from "@scure/base";
|
|
10
|
+
import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
|
|
11
|
+
import { ExactSvmScheme } from "@x402/svm/exact/client";
|
|
12
|
+
import { createPublicClient, http } from "viem";
|
|
13
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
import { base } from "viem/chains";
|
|
15
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
16
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
17
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
18
|
+
import { sha512 } from "@noble/hashes/sha2.js";
|
|
19
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
20
|
+
import { HDKey } from "@scure/bip32";
|
|
21
|
+
import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
|
|
22
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
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 formatTxLine(r) {
|
|
110
|
+
const time = new Date(r.t).toLocaleTimeString("en-US", {
|
|
111
|
+
hour: "2-digit",
|
|
112
|
+
minute: "2-digit",
|
|
113
|
+
hour12: false,
|
|
114
|
+
timeZone: "UTC"
|
|
115
|
+
});
|
|
116
|
+
const timeStr = r.tx ? `[${time}](${explorerUrl(r.net, r.tx)})` : time;
|
|
117
|
+
const parts = [r.kind === "x402_inference" && r.model ? shortModel(r.model) : KIND_LABELS[r.kind] ?? r.kind];
|
|
118
|
+
if (r.label) parts.push(r.label);
|
|
119
|
+
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
120
|
+
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
121
|
+
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/lib/config.ts
|
|
126
|
+
const APP_NAME = "x402-proxy";
|
|
127
|
+
function getConfigDir() {
|
|
128
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
129
|
+
return path.join(xdg, APP_NAME);
|
|
130
|
+
}
|
|
131
|
+
function getWalletPath() {
|
|
132
|
+
return path.join(getConfigDir(), "wallet.json");
|
|
133
|
+
}
|
|
134
|
+
function getHistoryPath() {
|
|
135
|
+
return path.join(getConfigDir(), "history.jsonl");
|
|
136
|
+
}
|
|
137
|
+
function ensureConfigDir() {
|
|
138
|
+
fs.mkdirSync(getConfigDir(), { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
function loadWalletFile() {
|
|
141
|
+
const p = getWalletPath();
|
|
142
|
+
if (!fs.existsSync(p)) return null;
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
145
|
+
if (data.version === 1 && typeof data.mnemonic === "string") return data;
|
|
146
|
+
return null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function saveWalletFile(wallet) {
|
|
152
|
+
ensureConfigDir();
|
|
153
|
+
fs.writeFileSync(getWalletPath(), JSON.stringify(wallet, null, 2), {
|
|
154
|
+
mode: 384,
|
|
155
|
+
encoding: "utf-8"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function loadConfig() {
|
|
159
|
+
const dir = getConfigDir();
|
|
160
|
+
for (const name of [
|
|
161
|
+
"config.yaml",
|
|
162
|
+
"config.yml",
|
|
163
|
+
"config.jsonc",
|
|
164
|
+
"config.json"
|
|
165
|
+
]) {
|
|
166
|
+
const p = path.join(dir, name);
|
|
167
|
+
if (!fs.existsSync(p)) continue;
|
|
168
|
+
try {
|
|
169
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
170
|
+
if (name.endsWith(".yaml") || name.endsWith(".yml")) return parse(raw);
|
|
171
|
+
if (name.endsWith(".jsonc")) {
|
|
172
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
173
|
+
return JSON.parse(stripped);
|
|
174
|
+
}
|
|
175
|
+
return JSON.parse(raw);
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function saveConfig(config) {
|
|
181
|
+
ensureConfigDir();
|
|
182
|
+
const p = path.join(getConfigDir(), "config.yaml");
|
|
183
|
+
fs.writeFileSync(p, stringify(config), "utf-8");
|
|
184
|
+
}
|
|
185
|
+
function isConfigured() {
|
|
186
|
+
if (process.env.X402_PROXY_WALLET_MNEMONIC) return true;
|
|
187
|
+
if (process.env.X402_PROXY_WALLET_EVM_KEY) return true;
|
|
188
|
+
if (process.env.X402_PROXY_WALLET_SOLANA_KEY) return true;
|
|
189
|
+
return loadWalletFile() !== null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/lib/output.ts
|
|
194
|
+
function isTTY() {
|
|
195
|
+
return !!process.stderr.isTTY;
|
|
196
|
+
}
|
|
197
|
+
function info(msg) {
|
|
198
|
+
process.stderr.write(`${isTTY() ? pc.cyan(msg) : msg}\n`);
|
|
199
|
+
}
|
|
200
|
+
function warn(msg) {
|
|
201
|
+
process.stderr.write(`${isTTY() ? pc.yellow(msg) : msg}\n`);
|
|
202
|
+
}
|
|
203
|
+
function error(msg) {
|
|
204
|
+
process.stderr.write(`${isTTY() ? pc.red(msg) : msg}\n`);
|
|
205
|
+
}
|
|
206
|
+
function dim(msg) {
|
|
207
|
+
process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/lib/derive.ts
|
|
212
|
+
/**
|
|
213
|
+
* Wallet derivation from BIP-39 mnemonic.
|
|
214
|
+
*
|
|
215
|
+
* A single 24-word mnemonic is the root secret. Both Solana and EVM keypairs
|
|
216
|
+
* are deterministically derived from it.
|
|
217
|
+
*
|
|
218
|
+
* Solana: SLIP-10 Ed25519 at m/44'/501'/0'/0'
|
|
219
|
+
* EVM: BIP-32 secp256k1 at m/44'/60'/0'/0/0
|
|
220
|
+
*
|
|
221
|
+
* Ported from agentbox/packages/openclaw-x402/src/wallet.ts
|
|
222
|
+
*/
|
|
223
|
+
function generateMnemonic$1() {
|
|
224
|
+
return generateMnemonic(wordlist, 256);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* SLIP-10 Ed25519 derivation at m/44'/501'/0'/0' (Phantom/Backpack compatible).
|
|
228
|
+
*/
|
|
229
|
+
const enc = new TextEncoder();
|
|
230
|
+
function deriveSolanaKeypair(mnemonic) {
|
|
231
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
232
|
+
let I = hmac(sha512, enc.encode("ed25519 seed"), seed);
|
|
233
|
+
let key = I.slice(0, 32);
|
|
234
|
+
let chainCode = I.slice(32);
|
|
235
|
+
for (const index of [
|
|
236
|
+
2147483692,
|
|
237
|
+
2147484149,
|
|
238
|
+
2147483648,
|
|
239
|
+
2147483648
|
|
240
|
+
]) {
|
|
241
|
+
const data = new Uint8Array(37);
|
|
242
|
+
data[0] = 0;
|
|
243
|
+
data.set(key, 1);
|
|
244
|
+
data[33] = index >>> 24 & 255;
|
|
245
|
+
data[34] = index >>> 16 & 255;
|
|
246
|
+
data[35] = index >>> 8 & 255;
|
|
247
|
+
data[36] = index & 255;
|
|
248
|
+
I = hmac(sha512, chainCode, data);
|
|
249
|
+
key = I.slice(0, 32);
|
|
250
|
+
chainCode = I.slice(32);
|
|
251
|
+
}
|
|
252
|
+
const secretKey = new Uint8Array(key);
|
|
253
|
+
const publicKey = ed25519.getPublicKey(secretKey);
|
|
254
|
+
return {
|
|
255
|
+
secretKey,
|
|
256
|
+
publicKey,
|
|
257
|
+
address: base58.encode(publicKey)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* BIP-32 secp256k1 derivation at m/44'/60'/0'/0/0.
|
|
262
|
+
*/
|
|
263
|
+
function deriveEvmKeypair(mnemonic) {
|
|
264
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
265
|
+
const derived = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
|
|
266
|
+
if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
|
|
267
|
+
const privateKey = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
|
|
268
|
+
const hash = keccak_256(secp256k1.getPublicKey(derived.privateKey, false).slice(1));
|
|
269
|
+
return {
|
|
270
|
+
privateKey,
|
|
271
|
+
address: checksumAddress(Buffer.from(hash.slice(-20)).toString("hex"))
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function checksumAddress(addr) {
|
|
275
|
+
const hash = Buffer.from(keccak_256(enc.encode(addr))).toString("hex");
|
|
276
|
+
let out = "0x";
|
|
277
|
+
for (let i = 0; i < 40; i++) out += Number.parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/lib/resolve-wallet.ts
|
|
283
|
+
/**
|
|
284
|
+
* Resolve wallet keys following the priority cascade:
|
|
285
|
+
* 1. Flags (--evm-key / --solana-key as raw key strings)
|
|
286
|
+
* 2. X402_PROXY_WALLET_EVM_KEY / X402_PROXY_WALLET_SOLANA_KEY env vars
|
|
287
|
+
* 3. X402_PROXY_WALLET_MNEMONIC env var (derives both)
|
|
288
|
+
* 4. ~/.config/x402-proxy/wallet.json (mnemonic file)
|
|
289
|
+
*/
|
|
290
|
+
function resolveWallet(opts) {
|
|
291
|
+
if (opts?.evmKey || opts?.solanaKey) {
|
|
292
|
+
const result = { source: "flag" };
|
|
293
|
+
if (opts.evmKey) {
|
|
294
|
+
const hex = opts.evmKey.startsWith("0x") ? opts.evmKey : `0x${opts.evmKey}`;
|
|
295
|
+
result.evmKey = hex;
|
|
296
|
+
result.evmAddress = privateKeyToAccount(hex).address;
|
|
297
|
+
}
|
|
298
|
+
if (opts.solanaKey) result.solanaKey = parsesolanaKey(opts.solanaKey);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
const envEvm = process.env.X402_PROXY_WALLET_EVM_KEY;
|
|
302
|
+
const envSol = process.env.X402_PROXY_WALLET_SOLANA_KEY;
|
|
303
|
+
if (envEvm || envSol) {
|
|
304
|
+
const result = { source: "env" };
|
|
305
|
+
if (envEvm) {
|
|
306
|
+
const hex = envEvm.startsWith("0x") ? envEvm : `0x${envEvm}`;
|
|
307
|
+
result.evmKey = hex;
|
|
308
|
+
result.evmAddress = privateKeyToAccount(hex).address;
|
|
309
|
+
}
|
|
310
|
+
if (envSol) result.solanaKey = parsesolanaKey(envSol);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
const envMnemonic = process.env.X402_PROXY_WALLET_MNEMONIC;
|
|
314
|
+
if (envMnemonic) return resolveFromMnemonic(envMnemonic, "mnemonic-env");
|
|
315
|
+
const walletFile = loadWalletFile();
|
|
316
|
+
if (walletFile) return resolveFromMnemonic(walletFile.mnemonic, "wallet-file");
|
|
317
|
+
return { source: "none" };
|
|
318
|
+
}
|
|
319
|
+
function resolveFromMnemonic(mnemonic, source) {
|
|
320
|
+
const evm = deriveEvmKeypair(mnemonic);
|
|
321
|
+
const sol = deriveSolanaKeypair(mnemonic);
|
|
322
|
+
const solanaKey = new Uint8Array(64);
|
|
323
|
+
solanaKey.set(sol.secretKey, 0);
|
|
324
|
+
solanaKey.set(sol.publicKey, 32);
|
|
325
|
+
return {
|
|
326
|
+
evmKey: evm.privateKey,
|
|
327
|
+
evmAddress: evm.address,
|
|
328
|
+
solanaKey,
|
|
329
|
+
solanaAddress: sol.address,
|
|
330
|
+
source
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function parsesolanaKey(input) {
|
|
334
|
+
const trimmed = input.trim();
|
|
335
|
+
if (trimmed.startsWith("[")) {
|
|
336
|
+
const arr = JSON.parse(trimmed);
|
|
337
|
+
return new Uint8Array(arr);
|
|
338
|
+
}
|
|
339
|
+
return base58.decode(trimmed);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Build a configured x402Client from resolved wallet keys.
|
|
343
|
+
*/
|
|
344
|
+
async function buildX402Client(wallet) {
|
|
345
|
+
const client = new x402Client();
|
|
346
|
+
if (wallet.evmKey) {
|
|
347
|
+
const hex = wallet.evmKey;
|
|
348
|
+
const signer = toClientEvmSigner(privateKeyToAccount(hex), createPublicClient({
|
|
349
|
+
chain: base,
|
|
350
|
+
transport: http()
|
|
351
|
+
}));
|
|
352
|
+
client.register("eip155:8453", new ExactEvmScheme(signer));
|
|
353
|
+
}
|
|
354
|
+
if (wallet.solanaKey) {
|
|
355
|
+
const { createKeyPairSignerFromBytes } = await import("@solana/kit");
|
|
356
|
+
const signer = await createKeyPairSignerFromBytes(wallet.solanaKey);
|
|
357
|
+
client.register("solana:mainnet", new ExactSvmScheme(signer));
|
|
358
|
+
client.register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme(signer));
|
|
359
|
+
}
|
|
360
|
+
return client;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/commands/status.ts
|
|
365
|
+
function displayStatus() {
|
|
366
|
+
const wallet = resolveWallet();
|
|
367
|
+
const config = loadConfig();
|
|
368
|
+
console.log();
|
|
369
|
+
info("x402-proxy status");
|
|
370
|
+
console.log();
|
|
371
|
+
dim(` Config directory: ${getConfigDir()}`);
|
|
372
|
+
if (config) {
|
|
373
|
+
if (config.spendLimit) dim(` Spend limit: ${config.spendLimit} USDC`);
|
|
374
|
+
if (config.defaultNetwork) dim(` Default network: ${config.defaultNetwork}`);
|
|
375
|
+
}
|
|
376
|
+
console.log();
|
|
377
|
+
if (wallet.source === "none") {
|
|
378
|
+
console.log(pc.yellow(" No wallet configured."));
|
|
379
|
+
console.log(pc.dim(` Run ${pc.cyan("x402-proxy setup")} to create one.`));
|
|
380
|
+
} else {
|
|
381
|
+
dim(` Wallet source: ${wallet.source}`);
|
|
382
|
+
if (wallet.evmAddress) console.log(` EVM: ${pc.green(wallet.evmAddress)}`);
|
|
383
|
+
if (wallet.solanaAddress) console.log(` Solana: ${pc.green(wallet.solanaAddress)}`);
|
|
384
|
+
}
|
|
385
|
+
console.log();
|
|
386
|
+
const spend = calcSpend(readHistory(getHistoryPath()));
|
|
387
|
+
if (spend.count > 0) {
|
|
388
|
+
dim(` Transactions: ${spend.count}`);
|
|
389
|
+
dim(` Today: ${spend.today.toFixed(4)} USDC`);
|
|
390
|
+
dim(` Total: ${spend.total.toFixed(4)} USDC`);
|
|
391
|
+
} else dim(" No payment history yet.");
|
|
392
|
+
console.log();
|
|
393
|
+
}
|
|
394
|
+
const statusCommand = buildCommand({
|
|
395
|
+
docs: { brief: "Show configuration and wallet status" },
|
|
396
|
+
parameters: {
|
|
397
|
+
flags: {},
|
|
398
|
+
positional: {
|
|
399
|
+
kind: "tuple",
|
|
400
|
+
parameters: []
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
func() {
|
|
404
|
+
displayStatus();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
//#endregion
|
|
409
|
+
export { readHistory as C, formatTxLine as S, isConfigured as _, deriveEvmKeypair as a, appendHistory as b, dim as c, isTTY as d, warn as f, getWalletPath as g, getHistoryPath as h, resolveWallet as i, error as l, getConfigDir as m, statusCommand as n, deriveSolanaKeypair as o, ensureConfigDir as p, buildX402Client as r, generateMnemonic$1 as s, displayStatus as t, info as u, saveConfig as v, calcSpend as x, saveWalletFile as y };
|
package/package.json
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-proxy",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
10
16
|
"./package.json": "./package.json"
|
|
11
17
|
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"x402-proxy": "./dist/bin/cli.js"
|
|
20
|
+
},
|
|
12
21
|
"dependencies": {
|
|
22
|
+
"@clack/prompts": "^1.1.0",
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
|
+
"@noble/curves": "^2.0.1",
|
|
25
|
+
"@noble/hashes": "^2.0.1",
|
|
26
|
+
"@scure/base": "^2.0.0",
|
|
27
|
+
"@scure/bip32": "^2.0.1",
|
|
28
|
+
"@scure/bip39": "^2.0.1",
|
|
29
|
+
"@stricli/core": "^1.2.6",
|
|
13
30
|
"@x402/evm": "^2.6.0",
|
|
14
31
|
"@x402/fetch": "^2.6.0",
|
|
15
|
-
"@x402/
|
|
32
|
+
"@x402/mcp": "^2.6.0",
|
|
33
|
+
"@x402/svm": "^2.6.0",
|
|
34
|
+
"picocolors": "^1.1.1",
|
|
35
|
+
"yaml": "^2.8.2"
|
|
16
36
|
},
|
|
17
37
|
"peerDependencies": {
|
|
18
38
|
"@solana/kit": "^6.0.0",
|
|
@@ -32,6 +52,19 @@
|
|
|
32
52
|
"README.md",
|
|
33
53
|
"LICENSE"
|
|
34
54
|
],
|
|
55
|
+
"keywords": [
|
|
56
|
+
"x402",
|
|
57
|
+
"http-402",
|
|
58
|
+
"mcp",
|
|
59
|
+
"mcp-proxy",
|
|
60
|
+
"ai-agent",
|
|
61
|
+
"agentic-commerce",
|
|
62
|
+
"base",
|
|
63
|
+
"solana",
|
|
64
|
+
"usdc",
|
|
65
|
+
"coinbase",
|
|
66
|
+
"cli"
|
|
67
|
+
],
|
|
35
68
|
"license": "Apache-2.0",
|
|
36
69
|
"engines": {
|
|
37
70
|
"node": ">=22.0.0"
|