Skip to content

Commit a69f040

Browse files
committed
feat: add OpenClaw plugin, --verbose flag, and MPP session fixes
- OpenClaw plugin (src/openclaw/): x_balance tool, x_payment tool, /x_wallet command, HTTP route proxy for upstream x402 endpoints - --verbose flag on fetch command for debugging protocol negotiation, headers, session lifecycle, and payment flow - Fix Headers instances silently dropped in MPP SSE requests by converting to plain object before passing to mppx SDK - Wrap MPP session close() in try/catch to prevent CLI crashes - Include amount and channelId in MPP payment history records - Bump to v0.7.0
1 parent 0edaaf7 commit a69f040

16 files changed

Lines changed: 6120 additions & 204 deletions

File tree

packages/x402-proxy/CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

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+
1024
## [0.6.0] - 2026-03-19
1125

1226
### Added
@@ -172,7 +186,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172186
- `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
173187
- Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
174188

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
176191
[0.6.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.2...v0.6.0
177192
[0.5.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.1...v0.5.2
178193
[0.5.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.0...v0.5.1

packages/x402-proxy/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ $ npx x402-proxy --method POST \
4949
# Force a specific network
5050
$ npx x402-proxy --network base https://api.example.com/data
5151

52+
# Debug protocol negotiation and payment flow
53+
$ npx x402-proxy --verbose https://api.example.com/data
54+
5255
# Use MPP protocol for streaming payments
5356
$ npx x402-proxy --protocol mpp \
5457
--method POST \
@@ -118,6 +121,12 @@ import {
118121

119122
See the [library API docs](https://github.com/cascade-protocol/x402-proxy/tree/main/packages/x402-proxy#library-api) for details.
120123

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+
121130
## License
122131

123132
Apache-2.0
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"id": "x402-proxy",
3+
"name": "x402 Proxy",
4+
"description": "x402/MPP payments, Solana wallet, and inference proxying",
5+
"version": "1.0.0",
6+
"configSchema": {
7+
"type": "object",
8+
"properties": {
9+
"providers": {
10+
"type": "object",
11+
"description": "Provider catalog keyed by name, each with baseUrl and models array"
12+
},
13+
"keypairPath": {
14+
"type": "string",
15+
"description": "Optional path to Solana keypair JSON file (overrides x402-proxy wallet resolution)"
16+
},
17+
"rpcUrl": {
18+
"type": "string",
19+
"description": "Solana RPC URL"
20+
},
21+
"dashboardUrl": {
22+
"type": "string",
23+
"description": "URL to link from /x_wallet dashboard"
24+
}
25+
},
26+
"required": []
27+
}
28+
}

packages/x402-proxy/package.json

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "x402-proxy",
3-
"version": "0.6.0",
4-
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo.",
3+
"version": "0.7.0",
4+
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo. Also works as an OpenClaw plugin.",
55
"type": "module",
66
"sideEffects": false,
77
"main": "./dist/index.js",
@@ -11,13 +11,22 @@
1111
"types": "./dist/index.d.ts",
1212
"default": "./dist/index.js"
1313
},
14+
"./openclaw": {
15+
"types": "./dist/openclaw/plugin.d.ts",
16+
"default": "./dist/openclaw/plugin.js"
17+
},
1418
"./package.json": "./package.json"
1519
},
20+
"openclaw": {
21+
"extensions": [
22+
"./dist/openclaw/plugin.js"
23+
]
24+
},
1625
"bin": {
1726
"x402-proxy": "./dist/bin/cli.js"
1827
},
1928
"scripts": {
20-
"build": "rm -rf dist && tsdown --publint",
29+
"build": "rm -rf dist && tsdown --publint && cp openclaw.plugin.json dist/",
2130
"dev": "tsdown --watch",
2231
"type-check": "tsc --noEmit",
2332
"test": "vitest run",
@@ -33,6 +42,7 @@
3342
"@scure/bip32": "^2.0.1",
3443
"@scure/bip39": "^2.0.1",
3544
"@solana/kit": "^6.0.0",
45+
"@sinclair/typebox": "^0.34.48",
3646
"@stricli/core": "^1.2.6",
3747
"@x402/evm": "^2.6.0",
3848
"@x402/fetch": "^2.6.0",
@@ -44,15 +54,25 @@
4454
"viem": "^2.0.0",
4555
"yaml": "^2.8.2"
4656
},
57+
"peerDependencies": {
58+
"openclaw": ">=2026.3.8"
59+
},
60+
"peerDependenciesMeta": {
61+
"openclaw": {
62+
"optional": true
63+
}
64+
},
4765
"devDependencies": {
4866
"@types/node": "^22.0.0",
67+
"openclaw": ">=2026.3.8",
4968
"publint": "^0.3.17",
5069
"tsdown": "^0.20.3",
5170
"typescript": "^5.9.0",
5271
"vitest": "^4.0.18"
5372
},
5473
"files": [
5574
"dist/**",
75+
"openclaw.plugin.json",
5676
"README.md",
5777
"CHANGELOG.md",
5878
"skills/**",
@@ -71,7 +91,9 @@
7191
"tempo",
7292
"usdc",
7393
"coinbase",
74-
"cli"
94+
"cli",
95+
"openclaw",
96+
"openclaw-plugin"
7597
],
7698
"license": "Apache-2.0",
7799
"engines": {

packages/x402-proxy/skills/SKILL.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ x402-proxy wallet export-key mnemonic # bare mnemonic to stdout
6262
--body, -d <DATA> Request body (string or @file)
6363
--network <NETWORK> Force payment chain (base, solana, tempo)
6464
--protocol <PROTOCOL> Payment protocol (x402, mpp)
65+
--verbose Show debug details (protocol negotiation, headers, payment flow)
6566
```
6667

6768
## MCP proxy for AI agents
@@ -122,7 +123,7 @@ Also supports JSONC and JSON config files. Wallet stored in `wallet.json` (mode
122123

123124
```bash
124125
# Smoke test an endpoint
125-
npx x402-proxy https://your-service.com/paid-route
126+
npx x402-proxy --verbose https://your-service.com/paid-route
126127
127128
# Test both chains
128129
npx x402-proxy --network base https://your-service.com/route
@@ -142,6 +143,17 @@ npx x402-proxy status
142143

143144
stdout = response body, stderr = payment info. Pipes, redirects, and `jq` all work cleanly.
144145

146+
## OpenClaw Plugin
147+
148+
x402-proxy ships as an [OpenClaw](https://openclaw.dev) plugin. Install it to give your OpenClaw gateway automatic x402 payment capabilities:
149+
150+
- `x_balance` tool - check wallet SOL/USDC balances
151+
- `x_payment` tool - call any x402-enabled endpoint with automatic payment
152+
- `/x_wallet` command - wallet status, send USDC, transaction history
153+
- HTTP route proxy (`/x402/*`) - proxies requests to upstream x402 endpoints with payment
154+
155+
Configure in your OpenClaw plugin settings with `providers` (upstream x402 endpoints and models) and optionally `keypairPath` or use the standard `X402_PROXY_WALLET_MNEMONIC` env var.
156+
145157
## Library API
146158

147159
For programmatic use in Node.js apps, read `references/library.md`.

packages/x402-proxy/src/commands/fetch.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
extractTxSignature,
99
type MppPaymentInfo,
1010
type PaymentInfo,
11+
TEMPO_NETWORK,
1112
} from "../handler.js";
1213
import { appendHistory, displayNetwork, type TxRecord } from "../history.js";
1314
import { getHistoryPath, isConfigured, loadConfig } from "../lib/config.js";
@@ -23,6 +24,7 @@ type FetchFlags = {
2324
network: string | undefined;
2425
protocol: string | undefined;
2526
json: boolean;
27+
verbose: boolean;
2628
};
2729

2830
export const fetchCommand = buildCommand<FetchFlags, [url?: string], CommandContext>({
@@ -85,6 +87,11 @@ Examples:
8587
brief: "Force JSON output",
8688
default: false,
8789
},
90+
verbose: {
91+
kind: "boolean",
92+
brief: "Show debug details (protocol negotiation, headers, payment flow)",
93+
default: false,
94+
},
8895
},
8996
positional: {
9097
kind: "tuple",
@@ -98,6 +105,22 @@ Examples:
98105
},
99106
},
100107
async func(flags, url?: string) {
108+
const verbose = (msg: string) => {
109+
if (flags.verbose) dim(` [verbose] ${msg}`);
110+
};
111+
112+
const closeMppSession = async (handler: Awaited<ReturnType<typeof createMppProxyHandler>>) => {
113+
verbose("closing MPP session...");
114+
try {
115+
await handler.close();
116+
verbose("session closed successfully");
117+
} catch (closeErr) {
118+
verbose(
119+
`session close failed: ${closeErr instanceof Error ? closeErr.message : String(closeErr)}`,
120+
);
121+
}
122+
};
123+
101124
// No URL: show status or onboarding
102125
if (!url) {
103126
if (isConfigured()) {
@@ -181,6 +204,8 @@ Examples:
181204
const config = loadConfig();
182205
const resolvedProtocol = flags.protocol ?? config?.preferredProtocol;
183206
const maxDeposit = config?.mppSessionBudget ?? "1";
207+
verbose(`wallet source: ${wallet.source}`);
208+
verbose(`protocol: ${resolvedProtocol ?? "auto-detect"}, maxDeposit: ${maxDeposit}`);
184209

185210
// Auto-detect preferred network based on balance when not configured
186211
let preferredNetwork = config?.defaultNetwork;
@@ -206,7 +231,8 @@ Examples:
206231
}
207232

208233
const method = flags.method || "GET";
209-
const init: RequestInit = { method, headers };
234+
// Convert Headers to plain object so mppx SSE spread doesn't lose them
235+
const init: RequestInit = { method, headers: Object.fromEntries(headers.entries()) };
210236
if (flags.body) init.body = flags.body;
211237

212238
if (isTTY()) {
@@ -234,23 +260,41 @@ Examples:
234260

235261
// Detect SSE streaming requests - these need session.sse() for mid-stream voucher cycling
236262
const isStreamingRequest = flags.body != null && /"stream"\s*:\s*true/.test(flags.body);
263+
verbose(`mpp handler created, streaming: ${isStreamingRequest}`);
237264

238265
if (isStreamingRequest) {
239266
try {
267+
verbose("opening SSE session...");
240268
const tokens = await mppHandler.sse(parsedUrl.toString(), init);
269+
verbose("SSE stream opened, reading tokens...");
241270
for await (const token of tokens) {
242271
process.stdout.write(token);
243272
}
273+
verbose("SSE stream complete");
244274
} finally {
245-
await mppHandler.close();
275+
await closeMppSession(mppHandler);
246276
}
247277

248-
mppPayment = mppHandler.shiftPayment();
278+
// SSE open pushes an intent-only entry; close pushes the actual receipt
279+
mppHandler.shiftPayment();
280+
const closeReceipt = mppHandler.shiftPayment();
281+
verbose(
282+
closeReceipt
283+
? `close receipt: amount=${closeReceipt.amount ?? "none"}, channelId=${closeReceipt.channelId ?? "none"}, txHash=${closeReceipt.receipt?.txHash ?? "none"}`
284+
: "no close receipt (session close may have failed)",
285+
);
286+
mppPayment = closeReceipt ?? {
287+
protocol: "mpp",
288+
network: TEMPO_NETWORK,
289+
intent: "session",
290+
};
249291
usedProtocol = "mpp";
250292

251293
const elapsedMs = Date.now() - startMs;
294+
const spentAmount = mppPayment.amount ? Number(mppPayment.amount) : undefined;
252295
if (mppPayment && isTTY()) {
253-
info(` MPP session (${displayNetwork(mppPayment.network)})`);
296+
const spentStr = spentAmount != null ? `${spentAmount.toFixed(4)} USDC ` : "";
297+
info(` MPP session: ${spentStr}(${displayNetwork(mppPayment.network)})`);
254298
}
255299
if (isTTY()) {
256300
dim(` Streamed (${elapsedMs}ms)`);
@@ -263,8 +307,8 @@ Examples:
263307
kind: "mpp_payment",
264308
net: mppPayment.network,
265309
from: wallet.evmAddress ?? "unknown",
266-
tx: mppPayment.receipt?.reference,
267-
amount: undefined,
310+
tx: mppPayment.receipt?.reference ?? mppPayment.channelId,
311+
amount: spentAmount,
268312
token: "USDC",
269313
ms: elapsedMs,
270314
label: parsedUrl.hostname,
@@ -277,10 +321,12 @@ Examples:
277321
}
278322

279323
// Non-streaming MPP request
324+
verbose("sending non-streaming MPP request...");
280325
try {
281326
response = await mppHandler.fetch(parsedUrl.toString(), init);
327+
verbose(`response: ${response.status} ${response.statusText}`);
282328
} finally {
283-
await mppHandler.close();
329+
await closeMppSession(mppHandler);
284330
}
285331
mppPayment = mppHandler.shiftPayment();
286332
usedProtocol = "mpp";
@@ -312,15 +358,18 @@ Examples:
312358
// If x402 couldn't handle it and server advertises MPP, fall through
313359
if (response.status === 402 && wallet.evmKey) {
314360
const detected = detectProtocols(response);
361+
verbose(`auto-detect: x402=${detected.x402}, mpp=${detected.mpp}`);
315362
if (detected.mpp) {
363+
verbose("falling through to MPP...");
316364
const mppHandler = await createMppProxyHandler({
317365
evmKey: wallet.evmKey,
318366
maxDeposit,
319367
});
320368
try {
321369
response = await mppHandler.fetch(parsedUrl.toString(), init);
370+
verbose(`MPP response: ${response.status} ${response.statusText}`);
322371
} finally {
323-
await mppHandler.close();
372+
await closeMppSession(mppHandler);
324373
}
325374
mppPayment = mppHandler.shiftPayment();
326375
x402Payment = undefined;
@@ -337,6 +386,11 @@ Examples:
337386
const payment = x402Payment ?? mppPayment;
338387
const txSig = extractTxSignature(response);
339388

389+
verbose(`protocol used: ${usedProtocol ?? "none"}`);
390+
for (const [k, v] of response.headers) {
391+
if (/payment|auth|www|x-pay/i.test(k)) verbose(`header ${k}: ${v.slice(0, 200)}`);
392+
}
393+
340394
// Payment failed - check balances and show appropriate message
341395
if (response.status === 402 && isTTY()) {
342396
const detected = detectProtocols(response);
@@ -505,6 +559,7 @@ Examples:
505559
net: mppPayment.network,
506560
from: wallet.evmAddress ?? "unknown",
507561
tx: mppPayment.receipt?.reference ?? txSig,
562+
amount: mppPayment.amount ? Number(mppPayment.amount) : undefined,
508563
token: "USDC",
509564
ms: elapsedMs,
510565
label: parsedUrl.hostname,

packages/x402-proxy/src/handler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,15 @@ export async function createMppProxyHandler(opts: {
190190
if (session?.opened) {
191191
const receipt = await session.close();
192192
if (receipt) {
193+
// spent is in USDC base units (6 decimals)
194+
const spentUsdc = receipt.spent
195+
? (Number(receipt.spent) / 1_000_000).toString()
196+
: undefined;
193197
paymentQueue.push({
194198
protocol: "mpp",
195199
network: TEMPO_NETWORK,
196200
intent: "session",
201+
amount: spentUsdc,
197202
channelId: session.channelId ?? undefined,
198203
receipt: {
199204
method: receipt.method,

0 commit comments

Comments
 (0)