x402-proxy 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-03-20
11
+
12
+ ### Added
13
+ - `--verbose` flag on `fetch` command - debug logging for protocol negotiation, headers, session lifecycle, and payment flow
14
+ - OpenClaw plugin integration (`x402-proxy/openclaw`) - registers `x_balance` tool, `x_payment` tool, `/x_wallet` command, and HTTP route proxy for upstream x402 endpoints
15
+ - `openclaw.plugin.json` manifest with config schema for providers, keypair path, RPC URL, and dashboard URL
16
+ - `./openclaw` subpath export in package.json
17
+
18
+ ### Fixed
19
+ - 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)
20
+ - MPP session `close()` errors no longer crash the CLI - wrapped in try/catch with verbose error reporting
21
+ - MPP payment history now includes `amount` (converted from base units) and `channelId` in transaction records
22
+ - MPP streaming history records now use `channelId` as fallback for `tx` field when no receipt reference is available
23
+
10
24
  ## [0.6.0] - 2026-03-19
11
25
 
12
26
  ### Added
@@ -172,7 +186,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172
186
  - `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
173
187
  - Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
174
188
 
175
- [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.6.0...HEAD
189
+ [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.7.0...HEAD
190
+ [0.7.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.6.0...v0.7.0
176
191
  [0.6.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.2...v0.6.0
177
192
  [0.5.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.1...v0.5.2
178
193
  [0.5.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.0...v0.5.1
package/README.md CHANGED
@@ -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,8 @@ 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)}`);
390
434
  if (response.status === 402 && isTTY()) {
391
435
  const detected = detectProtocols(response);
392
436
  const prHeader = response.headers.get("PAYMENT-REQUIRED") ?? response.headers.get("X-PAYMENT-REQUIRED");
@@ -491,6 +535,7 @@ Examples:
491
535
  net: mppPayment.network,
492
536
  from: wallet.evmAddress ?? "unknown",
493
537
  tx: mppPayment.receipt?.reference ?? txSig,
538
+ amount: mppPayment.amount ? Number(mppPayment.amount) : void 0,
494
539
  token: "USDC",
495
540
  ms: elapsedMs,
496
541
  label: parsedUrl.hostname
@@ -617,7 +662,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
617
662
  const { x402MCPClient } = await import("@x402/mcp");
618
663
  const remoteClient = new Client({
619
664
  name: "x402-proxy",
620
- version: "0.6.0"
665
+ version: "0.7.0"
621
666
  });
622
667
  const x402Mcp = new x402MCPClient(remoteClient, x402PaymentClient, {
623
668
  autoPayment: true,
@@ -656,7 +701,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
656
701
  }
657
702
  const localServer = new Server({
658
703
  name: "x402-proxy",
659
- version: "0.6.0"
704
+ version: "0.7.0"
660
705
  }, { capabilities: {
661
706
  tools: tools.length > 0 ? {} : void 0,
662
707
  resources: remoteResources.length > 0 ? {} : void 0
@@ -722,7 +767,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
722
767
  const maxDeposit = config?.mppSessionBudget ?? "1";
723
768
  const remoteClient = new Client({
724
769
  name: "x402-proxy",
725
- version: "0.6.0"
770
+ version: "0.7.0"
726
771
  });
727
772
  await connectTransport(remoteClient);
728
773
  const mppClient = McpClient.wrap(remoteClient, { methods: [tempo({
@@ -740,7 +785,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
740
785
  }
741
786
  const localServer = new Server({
742
787
  name: "x402-proxy",
743
- version: "0.6.0"
788
+ version: "0.7.0"
744
789
  }, { capabilities: {
745
790
  tools: tools.length > 0 ? {} : void 0,
746
791
  resources: remoteResources.length > 0 ? {} : void 0
@@ -940,7 +985,7 @@ const routes = buildRouteMap({
940
985
  });
941
986
  const app = buildApplication(routes, {
942
987
  name: "x402-proxy",
943
- versionInfo: { currentVersion: "0.6.0" },
988
+ versionInfo: { currentVersion: "0.7.0" },
944
989
  scanner: { caseStyle: "allow-kebab-for-camel" }
945
990
  });
946
991
 
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 };