x402-proxy 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.1] - 2026-03-20
11
+
12
+ ### Fixed
13
+ - Non-402 server errors (500, 503, etc.) now indicate whether payment was attempted, helping users distinguish "server down" from "payment failed"
14
+
15
+ ## [0.7.0] - 2026-03-20
16
+
17
+ ### Added
18
+ - `--verbose` flag on `fetch` command - debug logging for protocol negotiation, headers, session lifecycle, and payment flow
19
+ - OpenClaw plugin integration (`x402-proxy/openclaw`) - registers `x_balance` tool, `x_payment` tool, `/x_wallet` command, and HTTP route proxy for upstream x402 endpoints
20
+ - `openclaw.plugin.json` manifest with config schema for providers, keypair path, RPC URL, and dashboard URL
21
+ - `./openclaw` subpath export in package.json
22
+
23
+ ### Fixed
24
+ - MPP SSE requests silently losing `Content-Type` and other headers when `Headers` instances are spread (workaround for mppx SDK bug, upstream fix: wevm/mppx#209)
25
+ - MPP session `close()` errors no longer crash the CLI - wrapped in try/catch with verbose error reporting
26
+ - MPP payment history now includes `amount` (converted from base units) and `channelId` in transaction records
27
+ - MPP streaming history records now use `channelId` as fallback for `tx` field when no receipt reference is available
28
+
10
29
  ## [0.6.0] - 2026-03-19
11
30
 
12
31
  ### Added
@@ -172,7 +191,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172
191
  - `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
173
192
  - Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
174
193
 
175
- [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.6.0...HEAD
194
+ [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.7.1...HEAD
195
+ [0.7.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.7.0...v0.7.1
196
+ [0.7.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.6.0...v0.7.0
176
197
  [0.6.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.2...v0.6.0
177
198
  [0.5.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.1...v0.5.2
178
199
  [0.5.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.0...v0.5.1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # x402-proxy
2
2
 
3
- `curl` for [x402](https://www.x402.org/) paid APIs. Auto-pays HTTP 402 responses with USDC on Base, Solana, and Tempo - zero crypto code on the buyer side. Supports both [x402](https://www.x402.org/) and [MPP](https://mpp.dev/) payment protocols.
3
+ `curl` for [x402](https://www.x402.org/) and [MPP](https://mpp.dev/) paid APIs. Auto-pays HTTP 402 responses with USDC on Base, Solana, and [Tempo](https://tempo.xyz/) - zero crypto code on the buyer side. Supports one-time payments (x402, MPP charge) and pay-per-token streaming (MPP sessions).
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -30,7 +30,7 @@ Let your AI agent consume any paid MCP server. Configure in Claude, Cursor, or a
30
30
  }
31
31
  ```
32
32
 
33
- The proxy sits between your agent and the remote server, intercepting 402 responses, paying automatically, and forwarding the result. Your agent never touches crypto.
33
+ The proxy sits between your agent and the remote server, intercepting 402 responses, paying automatically, and forwarding the result. Supports both x402 and MPP protocols. Your agent never touches crypto.
34
34
 
35
35
  ## HTTP Requests
36
36
 
@@ -49,6 +49,9 @@ $ npx x402-proxy --method POST \
49
49
  # Force a specific network
50
50
  $ npx x402-proxy --network base https://api.example.com/data
51
51
 
52
+ # Debug protocol negotiation and payment flow
53
+ $ npx x402-proxy --verbose https://api.example.com/data
54
+
52
55
  # Use MPP protocol for streaming payments
53
56
  $ npx x402-proxy --protocol mpp \
54
57
  --method POST \
@@ -118,6 +121,12 @@ import {
118
121
 
119
122
  See the [library API docs](https://github.com/cascade-protocol/x402-proxy/tree/main/packages/x402-proxy#library-api) for details.
120
123
 
124
+ ## OpenClaw Plugin
125
+
126
+ x402-proxy ships as an [OpenClaw](https://openclaw.dev) plugin, giving your gateway automatic x402 payment capabilities. Registers `x_balance` and `x_payment` tools, `/x_wallet` command, and an HTTP route proxy for upstream x402 endpoints.
127
+
128
+ Configure providers and models in OpenClaw plugin settings. Uses the standard wallet resolution (env vars or `wallet.json`).
129
+
121
130
  ## License
122
131
 
123
132
  Apache-2.0
package/dist/bin/cli.js CHANGED
@@ -115,20 +115,24 @@ async function createMppProxyHandler(opts) {
115
115
  async close() {
116
116
  if (session?.opened) {
117
117
  const receipt = await session.close();
118
- if (receipt) paymentQueue.push({
119
- protocol: "mpp",
120
- network: TEMPO_NETWORK,
121
- intent: "session",
122
- channelId: session.channelId ?? void 0,
123
- receipt: {
124
- method: receipt.method,
125
- reference: receipt.reference,
126
- status: receipt.status,
127
- timestamp: receipt.timestamp,
128
- acceptedCumulative: receipt.acceptedCumulative,
129
- txHash: receipt.txHash
130
- }
131
- });
118
+ if (receipt) {
119
+ const spentUsdc = receipt.spent ? (Number(receipt.spent) / 1e6).toString() : void 0;
120
+ paymentQueue.push({
121
+ protocol: "mpp",
122
+ network: TEMPO_NETWORK,
123
+ intent: "session",
124
+ amount: spentUsdc,
125
+ channelId: session.channelId ?? void 0,
126
+ receipt: {
127
+ method: receipt.method,
128
+ reference: receipt.reference,
129
+ status: receipt.status,
130
+ timestamp: receipt.timestamp,
131
+ acceptedCumulative: receipt.acceptedCumulative,
132
+ txHash: receipt.txHash
133
+ }
134
+ });
135
+ }
132
136
  }
133
137
  }
134
138
  };
@@ -195,6 +199,11 @@ Examples:
195
199
  kind: "boolean",
196
200
  brief: "Force JSON output",
197
201
  default: false
202
+ },
203
+ verbose: {
204
+ kind: "boolean",
205
+ brief: "Show debug details (protocol negotiation, headers, payment flow)",
206
+ default: false
198
207
  }
199
208
  },
200
209
  positional: {
@@ -207,6 +216,18 @@ Examples:
207
216
  }
208
217
  },
209
218
  async func(flags, url) {
219
+ const verbose = (msg) => {
220
+ if (flags.verbose) dim(` [verbose] ${msg}`);
221
+ };
222
+ const closeMppSession = async (handler) => {
223
+ verbose("closing MPP session...");
224
+ try {
225
+ await handler.close();
226
+ verbose("session closed successfully");
227
+ } catch (closeErr) {
228
+ verbose(`session close failed: ${closeErr instanceof Error ? closeErr.message : String(closeErr)}`);
229
+ }
230
+ };
210
231
  if (!url) {
211
232
  if (isConfigured()) {
212
233
  const { displayStatus } = await import("../status-w5y-fhhe.js");
@@ -270,6 +291,8 @@ Examples:
270
291
  const config = loadConfig();
271
292
  const resolvedProtocol = flags.protocol ?? config?.preferredProtocol;
272
293
  const maxDeposit = config?.mppSessionBudget ?? "1";
294
+ verbose(`wallet source: ${wallet.source}`);
295
+ verbose(`protocol: ${resolvedProtocol ?? "auto-detect"}, maxDeposit: ${maxDeposit}`);
273
296
  let preferredNetwork = config?.defaultNetwork;
274
297
  if (!preferredNetwork && wallet.evmAddress && wallet.solanaAddress) {
275
298
  const { fetchAllBalances } = await import("../wallet-DjixXCHy.js");
@@ -287,7 +310,7 @@ Examples:
287
310
  const method = flags.method || "GET";
288
311
  const init = {
289
312
  method,
290
- headers
313
+ headers: Object.fromEntries(headers.entries())
291
314
  };
292
315
  if (flags.body) init.body = flags.body;
293
316
  if (isTTY()) dim(` ${method} ${parsedUrl.toString()}`);
@@ -306,17 +329,30 @@ Examples:
306
329
  evmKey: wallet.evmKey,
307
330
  maxDeposit
308
331
  });
309
- if (flags.body != null && /"stream"\s*:\s*true/.test(flags.body)) {
332
+ const isStreamingRequest = flags.body != null && /"stream"\s*:\s*true/.test(flags.body);
333
+ verbose(`mpp handler created, streaming: ${isStreamingRequest}`);
334
+ if (isStreamingRequest) {
310
335
  try {
336
+ verbose("opening SSE session...");
311
337
  const tokens = await mppHandler.sse(parsedUrl.toString(), init);
338
+ verbose("SSE stream opened, reading tokens...");
312
339
  for await (const token of tokens) process.stdout.write(token);
340
+ verbose("SSE stream complete");
313
341
  } finally {
314
- await mppHandler.close();
342
+ await closeMppSession(mppHandler);
315
343
  }
316
- mppPayment = mppHandler.shiftPayment();
344
+ mppHandler.shiftPayment();
345
+ const closeReceipt = mppHandler.shiftPayment();
346
+ verbose(closeReceipt ? `close receipt: amount=${closeReceipt.amount ?? "none"}, channelId=${closeReceipt.channelId ?? "none"}, txHash=${closeReceipt.receipt?.txHash ?? "none"}` : "no close receipt (session close may have failed)");
347
+ mppPayment = closeReceipt ?? {
348
+ protocol: "mpp",
349
+ network: TEMPO_NETWORK,
350
+ intent: "session"
351
+ };
317
352
  usedProtocol = "mpp";
318
353
  const elapsedMs = Date.now() - startMs;
319
- if (mppPayment && isTTY()) info(` MPP session (${displayNetwork(mppPayment.network)})`);
354
+ const spentAmount = mppPayment.amount ? Number(mppPayment.amount) : void 0;
355
+ if (mppPayment && isTTY()) info(` MPP session: ${spentAmount != null ? `${spentAmount.toFixed(4)} USDC ` : ""}(${displayNetwork(mppPayment.network)})`);
320
356
  if (isTTY()) dim(` Streamed (${elapsedMs}ms)`);
321
357
  if (mppPayment) {
322
358
  const record = {
@@ -325,8 +361,8 @@ Examples:
325
361
  kind: "mpp_payment",
326
362
  net: mppPayment.network,
327
363
  from: wallet.evmAddress ?? "unknown",
328
- tx: mppPayment.receipt?.reference,
329
- amount: void 0,
364
+ tx: mppPayment.receipt?.reference ?? mppPayment.channelId,
365
+ amount: spentAmount,
330
366
  token: "USDC",
331
367
  ms: elapsedMs,
332
368
  label: parsedUrl.hostname
@@ -336,10 +372,12 @@ Examples:
336
372
  if (isTTY()) process.stdout.write("\n");
337
373
  return;
338
374
  }
375
+ verbose("sending non-streaming MPP request...");
339
376
  try {
340
377
  response = await mppHandler.fetch(parsedUrl.toString(), init);
378
+ verbose(`response: ${response.status} ${response.statusText}`);
341
379
  } finally {
342
- await mppHandler.close();
380
+ await closeMppSession(mppHandler);
343
381
  }
344
382
  mppPayment = mppHandler.shiftPayment();
345
383
  usedProtocol = "mpp";
@@ -364,15 +402,19 @@ Examples:
364
402
  x402Payment = handler.shiftPayment();
365
403
  usedProtocol = "x402";
366
404
  if (response.status === 402 && wallet.evmKey) {
367
- if (detectProtocols(response).mpp) {
405
+ const detected = detectProtocols(response);
406
+ verbose(`auto-detect: x402=${detected.x402}, mpp=${detected.mpp}`);
407
+ if (detected.mpp) {
408
+ verbose("falling through to MPP...");
368
409
  const mppHandler = await createMppProxyHandler({
369
410
  evmKey: wallet.evmKey,
370
411
  maxDeposit
371
412
  });
372
413
  try {
373
414
  response = await mppHandler.fetch(parsedUrl.toString(), init);
415
+ verbose(`MPP response: ${response.status} ${response.statusText}`);
374
416
  } finally {
375
- await mppHandler.close();
417
+ await closeMppSession(mppHandler);
376
418
  }
377
419
  mppPayment = mppHandler.shiftPayment();
378
420
  x402Payment = void 0;
@@ -387,6 +429,10 @@ Examples:
387
429
  const elapsedMs = Date.now() - startMs;
388
430
  const payment = x402Payment ?? mppPayment;
389
431
  const txSig = extractTxSignature(response);
432
+ verbose(`protocol used: ${usedProtocol ?? "none"}`);
433
+ for (const [k, v] of response.headers) if (/payment|auth|www|x-pay/i.test(k)) verbose(`header ${k}: ${v.slice(0, 200)}`);
434
+ if (!response.ok && response.status !== 402 && isTTY()) if (!payment) dim(" Server returned error before payment was attempted.");
435
+ else dim(" Payment was processed but server returned an error.");
390
436
  if (response.status === 402 && isTTY()) {
391
437
  const detected = detectProtocols(response);
392
438
  const prHeader = response.headers.get("PAYMENT-REQUIRED") ?? response.headers.get("X-PAYMENT-REQUIRED");
@@ -491,6 +537,7 @@ Examples:
491
537
  net: mppPayment.network,
492
538
  from: wallet.evmAddress ?? "unknown",
493
539
  tx: mppPayment.receipt?.reference ?? txSig,
540
+ amount: mppPayment.amount ? Number(mppPayment.amount) : void 0,
494
541
  token: "USDC",
495
542
  ms: elapsedMs,
496
543
  label: parsedUrl.hostname
@@ -617,7 +664,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
617
664
  const { x402MCPClient } = await import("@x402/mcp");
618
665
  const remoteClient = new Client({
619
666
  name: "x402-proxy",
620
- version: "0.6.0"
667
+ version: "0.7.1"
621
668
  });
622
669
  const x402Mcp = new x402MCPClient(remoteClient, x402PaymentClient, {
623
670
  autoPayment: true,
@@ -656,7 +703,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
656
703
  }
657
704
  const localServer = new Server({
658
705
  name: "x402-proxy",
659
- version: "0.6.0"
706
+ version: "0.7.1"
660
707
  }, { capabilities: {
661
708
  tools: tools.length > 0 ? {} : void 0,
662
709
  resources: remoteResources.length > 0 ? {} : void 0
@@ -722,7 +769,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
722
769
  const maxDeposit = config?.mppSessionBudget ?? "1";
723
770
  const remoteClient = new Client({
724
771
  name: "x402-proxy",
725
- version: "0.6.0"
772
+ version: "0.7.1"
726
773
  });
727
774
  await connectTransport(remoteClient);
728
775
  const mppClient = McpClient.wrap(remoteClient, { methods: [tempo({
@@ -740,7 +787,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
740
787
  }
741
788
  const localServer = new Server({
742
789
  name: "x402-proxy",
743
- version: "0.6.0"
790
+ version: "0.7.1"
744
791
  }, { capabilities: {
745
792
  tools: tools.length > 0 ? {} : void 0,
746
793
  resources: remoteResources.length > 0 ? {} : void 0
@@ -940,7 +987,7 @@ const routes = buildRouteMap({
940
987
  });
941
988
  const app = buildApplication(routes, {
942
989
  name: "x402-proxy",
943
- versionInfo: { currentVersion: "0.6.0" },
990
+ versionInfo: { currentVersion: "0.7.1" },
944
991
  scanner: { caseStyle: "allow-kebab-for-camel" }
945
992
  });
946
993
 
package/dist/index.js CHANGED
@@ -112,20 +112,24 @@ async function createMppProxyHandler(opts) {
112
112
  async close() {
113
113
  if (session?.opened) {
114
114
  const receipt = await session.close();
115
- if (receipt) paymentQueue.push({
116
- protocol: "mpp",
117
- network: TEMPO_NETWORK,
118
- intent: "session",
119
- channelId: session.channelId ?? void 0,
120
- receipt: {
121
- method: receipt.method,
122
- reference: receipt.reference,
123
- status: receipt.status,
124
- timestamp: receipt.timestamp,
125
- acceptedCumulative: receipt.acceptedCumulative,
126
- txHash: receipt.txHash
127
- }
128
- });
115
+ if (receipt) {
116
+ const spentUsdc = receipt.spent ? (Number(receipt.spent) / 1e6).toString() : void 0;
117
+ paymentQueue.push({
118
+ protocol: "mpp",
119
+ network: TEMPO_NETWORK,
120
+ intent: "session",
121
+ amount: spentUsdc,
122
+ channelId: session.channelId ?? void 0,
123
+ receipt: {
124
+ method: receipt.method,
125
+ reference: receipt.reference,
126
+ status: receipt.status,
127
+ timestamp: receipt.timestamp,
128
+ acceptedCumulative: receipt.acceptedCumulative,
129
+ txHash: receipt.txHash
130
+ }
131
+ });
132
+ }
129
133
  }
130
134
  }
131
135
  };
@@ -0,0 +1,55 @@
1
+ import { x402Client } from "@x402/fetch";
2
+ //#region src/openclaw/tools.d.ts
3
+ type ModelEntry = {
4
+ provider: string;
5
+ id: string;
6
+ name: string;
7
+ maxTokens: number;
8
+ reasoning: boolean;
9
+ input: string[];
10
+ cost: {
11
+ input: number;
12
+ output: number;
13
+ cacheRead: number;
14
+ cacheWrite: number;
15
+ };
16
+ contextWindow: number;
17
+ };
18
+ //#endregion
19
+ //#region src/openclaw/plugin.d.ts
20
+ type OpenClawPluginApi = {
21
+ pluginConfig?: Record<string, unknown>;
22
+ logger: {
23
+ info: (msg: string) => void;
24
+ error: (msg: string) => void;
25
+ };
26
+ registerProvider: (provider: {
27
+ id: string;
28
+ label: string;
29
+ auth: unknown[];
30
+ models: {
31
+ baseUrl: string;
32
+ api: string;
33
+ authHeader: boolean;
34
+ models: Array<Omit<ModelEntry, "provider"> & {
35
+ input: Array<"text" | "image">;
36
+ }>;
37
+ };
38
+ }) => void;
39
+ registerTool: (tool: unknown) => void;
40
+ registerCommand: (command: unknown) => void;
41
+ registerService: (service: {
42
+ id: string;
43
+ start: () => Promise<void>;
44
+ stop: () => Promise<void>;
45
+ }) => void;
46
+ registerHttpRoute: (params: {
47
+ path: string;
48
+ match: string;
49
+ auth: string;
50
+ handler: (req: unknown, res: unknown) => Promise<void>;
51
+ }) => void;
52
+ };
53
+ declare function register(api: OpenClawPluginApi): void;
54
+ //#endregion
55
+ export { register };