x402-proxy 0.10.5 → 0.10.6

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.6] - 2026-04-01
11
+
12
+ ### Fixed
13
+ - Client disconnect now cancels upstream requests via abort signal propagation (prevents resource leaks on dropped connections)
14
+ - Anthropic SSE streaming stops reading after `message_stop` event instead of waiting for connection close
15
+ - Non-LLM endpoint requests (tool discovery, resource listing) no longer write empty records to inference history
16
+ - Empty inference history records (no amount, model, tokens, or tx) filtered out when reading history
17
+ - `formatUsdcValue()` uses `Intl.NumberFormat` for full precision up to 12 decimals instead of truncating to fixed tiers
18
+ - Stream responses guarded against double `res.end()` calls
19
+
20
+ ### Changed
21
+ - Default model for `claude` command changed from `minimax/minimax-m2.7` to `stepfun/step-3.5-flash`
22
+
10
23
  ## [0.10.5] - 2026-04-01
11
24
 
12
25
  ### Fixed
@@ -374,7 +387,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
374
387
  - `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
375
388
  - Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
376
389
 
377
- [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.5...HEAD
390
+ [Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.6...HEAD
391
+ [0.10.6]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.5...v0.10.6
378
392
  [0.10.5]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.4...v0.10.5
379
393
  [0.10.4]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.3...v0.10.4
380
394
  [0.10.3]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.2...v0.10.3
package/dist/bin/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { _ as error, b as success, c as resolveWallet, d as displayNetwork, f as formatAmount, g as dim, h as readHistory, l as appendHistory, m as formatUsdcValue, n as fetchAllBalances, o as walletInfoCommand, p as formatTxLine, s as buildX402Client, u as calcSpend, v as info, x as warn, y as isTTY } from "../wallet-LUl2l8WM.js";
2
+ import { _ as error, b as success, c as resolveWallet, d as displayNetwork, f as formatAmount, g as dim, h as readHistory, l as appendHistory, m as formatUsdcValue, n as fetchAllBalances, o as walletInfoCommand, p as formatTxLine, s as buildX402Client, u as calcSpend, v as info, x as warn, y as isTTY } from "../wallet-B0S-rma9.js";
3
3
  import { a as getHistoryPath, c as isConfigured, d as saveConfig, i as getConfigDirShort, l as loadConfig, u as loadWalletFile } from "../config-D9wIR3xc.js";
4
4
  import { n as setupCommand, t as runSetup } from "../setup-CNyMLnM-.js";
5
- import { n as statusCommand } from "../status-Dc1dB0ZG.js";
5
+ import { n as statusCommand } from "../status-DihAcUSC.js";
6
6
  import { dirname, join, normalize, resolve } from "node:path";
7
7
  import { buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
8
8
  import { spawn } from "node:child_process";
@@ -328,10 +328,28 @@ function addressForNetwork(evmAddress, solanaAddress, network) {
328
328
  function dbg(msg) {
329
329
  if (process.env.X402_PROXY_DEBUG === "1") process.stderr.write(`[x402-proxy] ${msg}\n`);
330
330
  }
331
+ function createDownstreamAbort(req, res) {
332
+ const controller = new AbortController();
333
+ const abort = () => {
334
+ if (!controller.signal.aborted) controller.abort();
335
+ };
336
+ const onReqAborted = () => abort();
337
+ const onResClose = () => abort();
338
+ req.once("aborted", onReqAborted);
339
+ res.once("close", onResClose);
340
+ return {
341
+ signal: controller.signal,
342
+ cleanup() {
343
+ req.off("aborted", onReqAborted);
344
+ res.off("close", onResClose);
345
+ }
346
+ };
347
+ }
331
348
  function createInferenceProxyRouteHandler(opts) {
332
349
  const { providers, getX402Proxy, getMppHandler, getWalletAddress, getWalletAddressForNetwork, historyPath, allModels, logger } = opts;
333
350
  const sortedProviders = providers.slice().sort((left, right) => right.baseUrl.length - left.baseUrl.length);
334
351
  return async (req, res) => {
352
+ const downstream = createDownstreamAbort(req, res);
335
353
  const url = new URL(req.url ?? "/", "http://localhost");
336
354
  const provider = sortedProviders.find((entry) => entry.baseUrl === "/" || url.pathname.startsWith(entry.baseUrl));
337
355
  if (!provider) {
@@ -395,7 +413,8 @@ function createInferenceProxyRouteHandler(opts) {
395
413
  const requestInit = {
396
414
  method,
397
415
  headers,
398
- body: ["GET", "HEAD"].includes(method) ? void 0 : body
416
+ body: ["GET", "HEAD"].includes(method) ? void 0 : body,
417
+ signal: downstream.signal
399
418
  };
400
419
  const useMpp = provider.protocol === "mpp" || provider.protocol === "auto";
401
420
  const wantsStreaming = isLlmEndpoint && /"stream"\s*:\s*true/.test(body);
@@ -409,11 +428,14 @@ function createInferenceProxyRouteHandler(opts) {
409
428
  } }));
410
429
  return true;
411
430
  }
431
+ const mppWalletAddress = getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress;
412
432
  return await handleMppRequest({
413
433
  res,
414
434
  upstreamUrl,
415
435
  requestInit,
416
- walletAddress: getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress,
436
+ abortSignal: downstream.signal,
437
+ isLlmEndpoint,
438
+ walletAddress: mppWalletAddress,
417
439
  historyPath,
418
440
  logger,
419
441
  allModels,
@@ -441,7 +463,7 @@ function createInferenceProxyRouteHandler(opts) {
441
463
  const amount = paymentAmount(payment);
442
464
  const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
443
465
  const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
444
- appendHistory(historyPath, {
466
+ if (isLlmEndpoint) appendHistory(historyPath, {
445
467
  t: Date.now(),
446
468
  ok: false,
447
469
  kind: "x402_inference",
@@ -493,7 +515,10 @@ function createInferenceProxyRouteHandler(opts) {
493
515
  res.writeHead(response.status, resHeaders);
494
516
  if (!response.body) {
495
517
  res.end();
496
- appendHistory(historyPath, {
518
+ if (shouldAppendInferenceHistory({
519
+ isLlmEndpoint,
520
+ amount
521
+ })) appendHistory(historyPath, {
497
522
  t: Date.now(),
498
523
  ok: true,
499
524
  kind: "x402_inference",
@@ -513,16 +538,28 @@ function createInferenceProxyRouteHandler(opts) {
513
538
  const decoder = new TextDecoder();
514
539
  try {
515
540
  while (true) {
541
+ if (downstream.signal.aborted) {
542
+ await reader.cancel().catch(() => {});
543
+ break;
544
+ }
516
545
  const { done, value } = await reader.read();
517
546
  if (done) break;
518
547
  res.write(value);
519
548
  sse?.push(decoder.decode(value, { stream: true }));
549
+ if (isMessagesApi && sse?.sawAnthropicMessageStop) {
550
+ await reader.cancel().catch(() => {});
551
+ break;
552
+ }
520
553
  }
521
554
  } finally {
522
555
  reader.releaseLock();
523
556
  }
524
- res.end();
525
- appendInferenceHistory({
557
+ if (!res.writableEnded) res.end();
558
+ if (shouldAppendInferenceHistory({
559
+ isLlmEndpoint,
560
+ amount,
561
+ usage: sse?.result
562
+ })) appendInferenceHistory({
526
563
  historyPath,
527
564
  allModels,
528
565
  walletAddress: paymentFrom,
@@ -539,7 +576,7 @@ function createInferenceProxyRouteHandler(opts) {
539
576
  const msg = String(err);
540
577
  logger.error(`x402: fetch threw: ${msg}`);
541
578
  getX402Proxy()?.shiftPayment();
542
- appendHistory(historyPath, {
579
+ if (isLlmEndpoint) appendHistory(historyPath, {
543
580
  t: Date.now(),
544
581
  ok: false,
545
582
  kind: "x402_inference",
@@ -554,6 +591,8 @@ function createInferenceProxyRouteHandler(opts) {
554
591
  else userMessage = `x402 request failed: ${msg}`;
555
592
  if (!res.headersSent) writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
556
593
  return true;
594
+ } finally {
595
+ downstream.cleanup();
557
596
  }
558
597
  };
559
598
  }
@@ -572,6 +611,10 @@ function writeErrorResponse(res, status, message, type, code, isAnthropicFormat)
572
611
  code
573
612
  } }));
574
613
  }
614
+ function shouldAppendInferenceHistory(opts) {
615
+ if (!opts.isLlmEndpoint) return false;
616
+ return opts.amount != null || opts.usage != null;
617
+ }
575
618
  function createSseTracker() {
576
619
  let residual = "";
577
620
  let anthropicModel = "";
@@ -579,6 +622,7 @@ function createSseTracker() {
579
622
  let anthropicOutputTokens = 0;
580
623
  let anthropicCacheRead;
581
624
  let anthropicCacheWrite;
625
+ let anthropicMessageStop = false;
582
626
  let lastOpenAiData = "";
583
627
  let isAnthropic = false;
584
628
  function processJson(json) {
@@ -607,6 +651,11 @@ function createSseTracker() {
607
651
  if (u?.output_tokens != null) anthropicOutputTokens = u.output_tokens;
608
652
  return;
609
653
  }
654
+ if (type === "message_stop") {
655
+ isAnthropic = true;
656
+ anthropicMessageStop = true;
657
+ return;
658
+ }
610
659
  if (type === "message") {
611
660
  isAnthropic = true;
612
661
  anthropicModel = parsed.model ?? "";
@@ -630,6 +679,9 @@ function createSseTracker() {
630
679
  pushJson(text) {
631
680
  processJson(text);
632
681
  },
682
+ get sawAnthropicMessageStop() {
683
+ return anthropicMessageStop;
684
+ },
633
685
  get result() {
634
686
  if (isAnthropic) {
635
687
  if (!anthropicModel && !anthropicInputTokens && !anthropicOutputTokens) return void 0;
@@ -659,7 +711,7 @@ function createSseTracker() {
659
711
  };
660
712
  }
661
713
  async function handleMppRequest(opts) {
662
- const { res, upstreamUrl, requestInit, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
714
+ const { res, upstreamUrl, requestInit, abortSignal, isLlmEndpoint, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
663
715
  try {
664
716
  if (wantsStreaming) {
665
717
  res.writeHead(200, {
@@ -671,27 +723,49 @@ async function handleMppRequest(opts) {
671
723
  dbg(`mpp.sse() calling ${upstreamUrl}`);
672
724
  const stream = await mpp.sse(upstreamUrl, requestInit);
673
725
  dbg("mpp.sse() resolved, iterating stream");
674
- if (isMessagesApi) for await (const chunk of stream) {
675
- const text = String(chunk);
676
- let eventType = "unknown";
677
- try {
678
- eventType = JSON.parse(text).type ?? "unknown";
679
- } catch {}
680
- res.write(`event: ${eventType}\ndata: ${text}\n\n`);
681
- sse.pushJson(text);
682
- }
683
- else {
684
- for await (const chunk of stream) {
685
- const text = String(chunk);
686
- res.write(`data: ${text}\n\n`);
726
+ const iterator = stream[Symbol.asyncIterator]();
727
+ let semanticComplete = false;
728
+ try {
729
+ if (isMessagesApi) while (true) {
730
+ if (abortSignal.aborted) break;
731
+ const { done, value } = await iterator.next();
732
+ if (done) break;
733
+ const text = String(value);
734
+ let eventType = "unknown";
735
+ try {
736
+ eventType = JSON.parse(text).type ?? "unknown";
737
+ } catch {}
738
+ res.write(`event: ${eventType}\ndata: ${text}\n\n`);
687
739
  sse.pushJson(text);
740
+ if (sse.sawAnthropicMessageStop) {
741
+ semanticComplete = true;
742
+ break;
743
+ }
744
+ }
745
+ else {
746
+ while (true) {
747
+ if (abortSignal.aborted) break;
748
+ const { done, value } = await iterator.next();
749
+ if (done) break;
750
+ const text = String(value);
751
+ res.write(`data: ${text}\n\n`);
752
+ sse.pushJson(text);
753
+ }
754
+ if (!abortSignal.aborted) res.write("data: [DONE]\n\n");
688
755
  }
689
- res.write("data: [DONE]\n\n");
756
+ } catch (err) {
757
+ const msg = err instanceof Error ? err.message : String(err);
758
+ if (!(abortSignal.aborted || semanticComplete) || !msg.includes("terminated")) throw err;
759
+ } finally {
760
+ await iterator.return?.().catch(() => {});
690
761
  }
691
762
  dbg(`stream done, ${sse.result ? `${sse.result.model} ${sse.result.inputTokens}+${sse.result.outputTokens}t` : "no usage"}`);
692
- res.end();
763
+ if (!res.writableEnded) res.end();
693
764
  mpp.shiftPayment();
694
- appendInferenceHistory({
765
+ if (shouldAppendInferenceHistory({
766
+ isLlmEndpoint,
767
+ usage: sse.result
768
+ })) appendInferenceHistory({
695
769
  historyPath,
696
770
  allModels,
697
771
  walletAddress,
@@ -708,7 +782,7 @@ async function handleMppRequest(opts) {
708
782
  if (response.status === 402) {
709
783
  const responseBody = await response.text();
710
784
  logger.error(`mpp: payment failed, raw response: ${responseBody}`);
711
- appendHistory(historyPath, {
785
+ if (isLlmEndpoint) appendHistory(historyPath, {
712
786
  t: Date.now(),
713
787
  ok: false,
714
788
  kind: "x402_inference",
@@ -728,14 +802,19 @@ async function handleMppRequest(opts) {
728
802
  const usageTracker = createSseTracker();
729
803
  usageTracker.pushJson(responseBody);
730
804
  const payment = mpp.shiftPayment();
731
- appendInferenceHistory({
805
+ const amount = parseMppAmount(payment?.amount);
806
+ if (shouldAppendInferenceHistory({
807
+ isLlmEndpoint,
808
+ amount,
809
+ usage: usageTracker.result
810
+ })) appendInferenceHistory({
732
811
  historyPath,
733
812
  allModels,
734
813
  walletAddress,
735
814
  paymentNetwork: payment?.network ?? "eip155:4217",
736
815
  paymentTo: void 0,
737
816
  tx: tx ?? payment?.receipt?.reference,
738
- amount: parseMppAmount(payment?.amount),
817
+ amount,
739
818
  thinkingMode,
740
819
  usage: usageTracker.result,
741
820
  durationMs: Date.now() - startMs
@@ -744,7 +823,7 @@ async function handleMppRequest(opts) {
744
823
  } catch (err) {
745
824
  dbg(`mpp error: ${String(err)}`);
746
825
  logger.error(`mpp: fetch threw: ${String(err)}`);
747
- appendHistory(historyPath, {
826
+ if (isLlmEndpoint) appendHistory(historyPath, {
748
827
  t: Date.now(),
749
828
  ok: false,
750
829
  kind: "x402_inference",
@@ -1003,7 +1082,7 @@ Examples:
1003
1082
  });
1004
1083
  //#endregion
1005
1084
  //#region src/commands/claude.ts
1006
- const DEFAULT_MODEL = "minimax/minimax-m2.7";
1085
+ const DEFAULT_MODEL = "stepfun/step-3.5-flash";
1007
1086
  function normalizeClaudeArgs(args) {
1008
1087
  return args[0] === "--" ? args.slice(1) : args;
1009
1088
  }
@@ -1021,7 +1100,7 @@ Examples:
1021
1100
  flags: {
1022
1101
  model: {
1023
1102
  kind: "parsed",
1024
- brief: "Model to use (default: minimax/minimax-m2.7)",
1103
+ brief: "Model to use (default: stepfun/step-3.5-flash)",
1025
1104
  parse: String,
1026
1105
  default: DEFAULT_MODEL
1027
1106
  },
@@ -1368,7 +1447,7 @@ Examples:
1368
1447
  };
1369
1448
  if (!url) {
1370
1449
  if (isConfigured()) {
1371
- const { displayStatus } = await import("../status-wknbavnl.js");
1450
+ const { displayStatus } = await import("../status-DZlJ4pS7.js");
1372
1451
  await displayStatus();
1373
1452
  console.log();
1374
1453
  console.log(pc.dim(" Commands:"));
@@ -1433,7 +1512,7 @@ Examples:
1433
1512
  verbose(`protocol: ${resolvedProtocol ?? "auto-detect"}, maxDeposit: ${maxDeposit}`);
1434
1513
  let preferredNetwork = config?.defaultNetwork;
1435
1514
  if (!preferredNetwork && wallet.evmAddress && wallet.solanaAddress) {
1436
- const { fetchAllBalances } = await import("../wallet-C9UlV7qi.js");
1515
+ const { fetchAllBalances } = await import("../wallet-DBrVZJqe.js");
1437
1516
  const balances = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
1438
1517
  const evmUsdc = balances.evm ? Number(balances.evm.usdc) : 0;
1439
1518
  const solUsdc = balances.sol ? Number(balances.sol.usdc) : 0;
@@ -1600,7 +1679,7 @@ Examples:
1600
1679
  const hasSolana = accepts.some((a) => a.network.startsWith("solana:"));
1601
1680
  const hasMpp = detected.mpp;
1602
1681
  const hasOther = accepts.some((a) => !a.network.startsWith("eip155:") && !a.network.startsWith("solana:"));
1603
- const { fetchAllBalances } = await import("../wallet-C9UlV7qi.js");
1682
+ const { fetchAllBalances } = await import("../wallet-DBrVZJqe.js");
1604
1683
  const balances = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
1605
1684
  const evmUsdc = hasEvm && balances.evm ? Number(balances.evm.usdc) : 0;
1606
1685
  const solUsdc = hasSolana && balances.sol ? Number(balances.sol.usdc) : 0;
@@ -1820,7 +1899,7 @@ Wallet is auto-generated on first run. No env vars needed.`
1820
1899
  async function startX402Proxy() {
1821
1900
  let preferredNetwork = config?.defaultNetwork;
1822
1901
  if (!preferredNetwork && wallet.evmAddress && wallet.solanaAddress) {
1823
- const { fetchAllBalances } = await import("../wallet-C9UlV7qi.js");
1902
+ const { fetchAllBalances } = await import("../wallet-DBrVZJqe.js");
1824
1903
  const balances = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
1825
1904
  const evmUsdc = balances.evm ? Number(balances.evm.usdc) : 0;
1826
1905
  const solUsdc = balances.sol ? Number(balances.sol.usdc) : 0;
@@ -1840,7 +1919,7 @@ Wallet is auto-generated on first run. No env vars needed.`
1840
1919
  }
1841
1920
  const remoteClient = new Client({
1842
1921
  name: "x402-proxy",
1843
- version: "0.10.5"
1922
+ version: "0.10.6"
1844
1923
  });
1845
1924
  const x402Mcp = new x402MCPClient(remoteClient, x402PaymentClient, {
1846
1925
  autoPayment: true,
@@ -1878,7 +1957,7 @@ Wallet is auto-generated on first run. No env vars needed.`
1878
1957
  }
1879
1958
  const localServer = new Server({
1880
1959
  name: "x402-proxy",
1881
- version: "0.10.5"
1960
+ version: "0.10.6"
1882
1961
  }, { capabilities: {
1883
1962
  tools: tools.length > 0 ? {} : void 0,
1884
1963
  resources: remoteResources.length > 0 ? {} : void 0
@@ -1973,7 +2052,7 @@ Wallet is auto-generated on first run. No env vars needed.`
1973
2052
  }));
1974
2053
  const remoteClient = new Client({
1975
2054
  name: "x402-proxy",
1976
- version: "0.10.5"
2055
+ version: "0.10.6"
1977
2056
  });
1978
2057
  await connectTransport(remoteClient);
1979
2058
  const mppClient = McpClient.wrap(remoteClient, { methods: wrappedMethods });
@@ -1988,7 +2067,7 @@ Wallet is auto-generated on first run. No env vars needed.`
1988
2067
  }
1989
2068
  const localServer = new Server({
1990
2069
  name: "x402-proxy",
1991
- version: "0.10.5"
2070
+ version: "0.10.6"
1992
2071
  }, { capabilities: {
1993
2072
  tools: tools.length > 0 ? {} : void 0,
1994
2073
  resources: remoteResources.length > 0 ? {} : void 0
@@ -2387,7 +2466,7 @@ const app = buildApplication(buildRouteMap({
2387
2466
  docs: { brief: "curl for x402 paid APIs" }
2388
2467
  }), {
2389
2468
  name: "x402-proxy",
2390
- versionInfo: { currentVersion: "0.10.5" },
2469
+ versionInfo: { currentVersion: "0.10.6" },
2391
2470
  scanner: { caseStyle: "allow-kebab-for-camel" }
2392
2471
  });
2393
2472
  //#endregion
package/dist/index.js CHANGED
@@ -174,6 +174,11 @@ async function createMppProxyHandler(opts) {
174
174
  //#region src/history.ts
175
175
  const HISTORY_MAX_LINES = 1e3;
176
176
  const HISTORY_KEEP_LINES = 500;
177
+ function isMeaningfulInferenceRecord(record) {
178
+ if (record.kind !== "x402_inference") return true;
179
+ if (!record.ok) return true;
180
+ return record.amount != null || record.model != null || record.inputTokens != null || record.outputTokens != null || record.tx != null;
181
+ }
177
182
  function appendHistory(historyPath, record) {
178
183
  try {
179
184
  mkdirSync(dirname(historyPath), { recursive: true });
@@ -195,7 +200,8 @@ function readHistory(historyPath) {
195
200
  try {
196
201
  const parsed = JSON.parse(line);
197
202
  if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
198
- return [parsed];
203
+ const record = parsed;
204
+ return isMeaningfulInferenceRecord(record) ? [record] : [];
199
205
  } catch {
200
206
  return [];
201
207
  }
@@ -224,12 +230,12 @@ function calcSpend(records) {
224
230
  count
225
231
  };
226
232
  }
227
- /** Format a USDC value with adaptive precision (no token suffix). */
228
233
  function formatUsdcValue(amount) {
229
- if (amount >= .01) return amount.toFixed(2);
230
- if (amount >= .001) return amount.toFixed(3);
231
- if (amount >= 1e-4) return amount.toFixed(4);
232
- return amount.toFixed(6);
234
+ return new Intl.NumberFormat("en-US", {
235
+ useGrouping: false,
236
+ minimumFractionDigits: 0,
237
+ maximumFractionDigits: 12
238
+ }).format(amount);
233
239
  }
234
240
  function formatAmount(amount, token) {
235
241
  if (token === "USDC") return `${formatUsdcValue(amount)} USDC`;
@@ -258,6 +258,11 @@ var OptimizedSvmScheme = class {
258
258
  };
259
259
  }
260
260
  };
261
+ function isMeaningfulInferenceRecord(record) {
262
+ if (record.kind !== "x402_inference") return true;
263
+ if (!record.ok) return true;
264
+ return record.amount != null || record.model != null || record.inputTokens != null || record.outputTokens != null || record.tx != null;
265
+ }
261
266
  function appendHistory(historyPath, record) {
262
267
  try {
263
268
  mkdirSync(dirname(historyPath), { recursive: true });
@@ -279,7 +284,8 @@ function readHistory(historyPath) {
279
284
  try {
280
285
  const parsed = JSON.parse(line);
281
286
  if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
282
- return [parsed];
287
+ const record = parsed;
288
+ return isMeaningfulInferenceRecord(record) ? [record] : [];
283
289
  } catch {
284
290
  return [];
285
291
  }
@@ -308,12 +314,12 @@ function calcSpend(records) {
308
314
  count
309
315
  };
310
316
  }
311
- /** Format a USDC value with adaptive precision (no token suffix). */
312
317
  function formatUsdcValue(amount) {
313
- if (amount >= .01) return amount.toFixed(2);
314
- if (amount >= .001) return amount.toFixed(3);
315
- if (amount >= 1e-4) return amount.toFixed(4);
316
- return amount.toFixed(6);
318
+ return new Intl.NumberFormat("en-US", {
319
+ useGrouping: false,
320
+ minimumFractionDigits: 0,
321
+ maximumFractionDigits: 12
322
+ }).format(amount);
317
323
  }
318
324
  function formatAmount(amount, token) {
319
325
  if (token === "USDC") return `${formatUsdcValue(amount)} USDC`;
@@ -1153,7 +1159,7 @@ function createWalletCommand(ctx) {
1153
1159
  if (parts[0]?.toLowerCase() === "send") return { text: "Use `/x_send <amount|all> <address>` for transfers." };
1154
1160
  try {
1155
1161
  const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
1156
- const lines = [`x402-proxy v0.10.5`];
1162
+ const lines = [`x402-proxy v0.10.6`];
1157
1163
  const defaultModel = ctx.allModels[0];
1158
1164
  if (defaultModel) lines.push("", `**Model** - ${defaultModel.name} (${defaultModel.provider})`);
1159
1165
  lines.push("", `**Protocol** - ${ctx.getDefaultRequestProtocol()}`);
@@ -1372,10 +1378,28 @@ function resolveMppSessionBudget(value, fallback = "0.5") {
1372
1378
  function dbg(msg) {
1373
1379
  if (process.env.X402_PROXY_DEBUG === "1") process.stderr.write(`[x402-proxy] ${msg}\n`);
1374
1380
  }
1381
+ function createDownstreamAbort(req, res) {
1382
+ const controller = new AbortController();
1383
+ const abort = () => {
1384
+ if (!controller.signal.aborted) controller.abort();
1385
+ };
1386
+ const onReqAborted = () => abort();
1387
+ const onResClose = () => abort();
1388
+ req.once("aborted", onReqAborted);
1389
+ res.once("close", onResClose);
1390
+ return {
1391
+ signal: controller.signal,
1392
+ cleanup() {
1393
+ req.off("aborted", onReqAborted);
1394
+ res.off("close", onResClose);
1395
+ }
1396
+ };
1397
+ }
1375
1398
  function createInferenceProxyRouteHandler(opts) {
1376
1399
  const { providers, getX402Proxy, getMppHandler, getWalletAddress, getWalletAddressForNetwork, historyPath, allModels, logger } = opts;
1377
1400
  const sortedProviders = providers.slice().sort((left, right) => right.baseUrl.length - left.baseUrl.length);
1378
1401
  return async (req, res) => {
1402
+ const downstream = createDownstreamAbort(req, res);
1379
1403
  const url = new URL(req.url ?? "/", "http://localhost");
1380
1404
  const provider = sortedProviders.find((entry) => entry.baseUrl === "/" || url.pathname.startsWith(entry.baseUrl));
1381
1405
  if (!provider) {
@@ -1439,7 +1463,8 @@ function createInferenceProxyRouteHandler(opts) {
1439
1463
  const requestInit = {
1440
1464
  method,
1441
1465
  headers,
1442
- body: ["GET", "HEAD"].includes(method) ? void 0 : body
1466
+ body: ["GET", "HEAD"].includes(method) ? void 0 : body,
1467
+ signal: downstream.signal
1443
1468
  };
1444
1469
  const useMpp = provider.protocol === "mpp" || provider.protocol === "auto";
1445
1470
  const wantsStreaming = isLlmEndpoint && /"stream"\s*:\s*true/.test(body);
@@ -1453,11 +1478,14 @@ function createInferenceProxyRouteHandler(opts) {
1453
1478
  } }));
1454
1479
  return true;
1455
1480
  }
1481
+ const mppWalletAddress = getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress;
1456
1482
  return await handleMppRequest({
1457
1483
  res,
1458
1484
  upstreamUrl,
1459
1485
  requestInit,
1460
- walletAddress: getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress,
1486
+ abortSignal: downstream.signal,
1487
+ isLlmEndpoint,
1488
+ walletAddress: mppWalletAddress,
1461
1489
  historyPath,
1462
1490
  logger,
1463
1491
  allModels,
@@ -1485,7 +1513,7 @@ function createInferenceProxyRouteHandler(opts) {
1485
1513
  const amount = paymentAmount(payment);
1486
1514
  const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
1487
1515
  const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
1488
- appendHistory(historyPath, {
1516
+ if (isLlmEndpoint) appendHistory(historyPath, {
1489
1517
  t: Date.now(),
1490
1518
  ok: false,
1491
1519
  kind: "x402_inference",
@@ -1537,7 +1565,10 @@ function createInferenceProxyRouteHandler(opts) {
1537
1565
  res.writeHead(response.status, resHeaders);
1538
1566
  if (!response.body) {
1539
1567
  res.end();
1540
- appendHistory(historyPath, {
1568
+ if (shouldAppendInferenceHistory({
1569
+ isLlmEndpoint,
1570
+ amount
1571
+ })) appendHistory(historyPath, {
1541
1572
  t: Date.now(),
1542
1573
  ok: true,
1543
1574
  kind: "x402_inference",
@@ -1557,16 +1588,28 @@ function createInferenceProxyRouteHandler(opts) {
1557
1588
  const decoder = new TextDecoder();
1558
1589
  try {
1559
1590
  while (true) {
1591
+ if (downstream.signal.aborted) {
1592
+ await reader.cancel().catch(() => {});
1593
+ break;
1594
+ }
1560
1595
  const { done, value } = await reader.read();
1561
1596
  if (done) break;
1562
1597
  res.write(value);
1563
1598
  sse?.push(decoder.decode(value, { stream: true }));
1599
+ if (isMessagesApi && sse?.sawAnthropicMessageStop) {
1600
+ await reader.cancel().catch(() => {});
1601
+ break;
1602
+ }
1564
1603
  }
1565
1604
  } finally {
1566
1605
  reader.releaseLock();
1567
1606
  }
1568
- res.end();
1569
- appendInferenceHistory({
1607
+ if (!res.writableEnded) res.end();
1608
+ if (shouldAppendInferenceHistory({
1609
+ isLlmEndpoint,
1610
+ amount,
1611
+ usage: sse?.result
1612
+ })) appendInferenceHistory({
1570
1613
  historyPath,
1571
1614
  allModels,
1572
1615
  walletAddress: paymentFrom,
@@ -1583,7 +1626,7 @@ function createInferenceProxyRouteHandler(opts) {
1583
1626
  const msg = String(err);
1584
1627
  logger.error(`x402: fetch threw: ${msg}`);
1585
1628
  getX402Proxy()?.shiftPayment();
1586
- appendHistory(historyPath, {
1629
+ if (isLlmEndpoint) appendHistory(historyPath, {
1587
1630
  t: Date.now(),
1588
1631
  ok: false,
1589
1632
  kind: "x402_inference",
@@ -1598,6 +1641,8 @@ function createInferenceProxyRouteHandler(opts) {
1598
1641
  else userMessage = `x402 request failed: ${msg}`;
1599
1642
  if (!res.headersSent) writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
1600
1643
  return true;
1644
+ } finally {
1645
+ downstream.cleanup();
1601
1646
  }
1602
1647
  };
1603
1648
  }
@@ -1616,6 +1661,10 @@ function writeErrorResponse(res, status, message, type, code, isAnthropicFormat)
1616
1661
  code
1617
1662
  } }));
1618
1663
  }
1664
+ function shouldAppendInferenceHistory(opts) {
1665
+ if (!opts.isLlmEndpoint) return false;
1666
+ return opts.amount != null || opts.usage != null;
1667
+ }
1619
1668
  function createSseTracker() {
1620
1669
  let residual = "";
1621
1670
  let anthropicModel = "";
@@ -1623,6 +1672,7 @@ function createSseTracker() {
1623
1672
  let anthropicOutputTokens = 0;
1624
1673
  let anthropicCacheRead;
1625
1674
  let anthropicCacheWrite;
1675
+ let anthropicMessageStop = false;
1626
1676
  let lastOpenAiData = "";
1627
1677
  let isAnthropic = false;
1628
1678
  function processJson(json) {
@@ -1651,6 +1701,11 @@ function createSseTracker() {
1651
1701
  if (u?.output_tokens != null) anthropicOutputTokens = u.output_tokens;
1652
1702
  return;
1653
1703
  }
1704
+ if (type === "message_stop") {
1705
+ isAnthropic = true;
1706
+ anthropicMessageStop = true;
1707
+ return;
1708
+ }
1654
1709
  if (type === "message") {
1655
1710
  isAnthropic = true;
1656
1711
  anthropicModel = parsed.model ?? "";
@@ -1674,6 +1729,9 @@ function createSseTracker() {
1674
1729
  pushJson(text) {
1675
1730
  processJson(text);
1676
1731
  },
1732
+ get sawAnthropicMessageStop() {
1733
+ return anthropicMessageStop;
1734
+ },
1677
1735
  get result() {
1678
1736
  if (isAnthropic) {
1679
1737
  if (!anthropicModel && !anthropicInputTokens && !anthropicOutputTokens) return void 0;
@@ -1703,7 +1761,7 @@ function createSseTracker() {
1703
1761
  };
1704
1762
  }
1705
1763
  async function handleMppRequest(opts) {
1706
- const { res, upstreamUrl, requestInit, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
1764
+ const { res, upstreamUrl, requestInit, abortSignal, isLlmEndpoint, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
1707
1765
  try {
1708
1766
  if (wantsStreaming) {
1709
1767
  res.writeHead(200, {
@@ -1715,27 +1773,49 @@ async function handleMppRequest(opts) {
1715
1773
  dbg(`mpp.sse() calling ${upstreamUrl}`);
1716
1774
  const stream = await mpp.sse(upstreamUrl, requestInit);
1717
1775
  dbg("mpp.sse() resolved, iterating stream");
1718
- if (isMessagesApi) for await (const chunk of stream) {
1719
- const text = String(chunk);
1720
- let eventType = "unknown";
1721
- try {
1722
- eventType = JSON.parse(text).type ?? "unknown";
1723
- } catch {}
1724
- res.write(`event: ${eventType}\ndata: ${text}\n\n`);
1725
- sse.pushJson(text);
1726
- }
1727
- else {
1728
- for await (const chunk of stream) {
1729
- const text = String(chunk);
1730
- res.write(`data: ${text}\n\n`);
1776
+ const iterator = stream[Symbol.asyncIterator]();
1777
+ let semanticComplete = false;
1778
+ try {
1779
+ if (isMessagesApi) while (true) {
1780
+ if (abortSignal.aborted) break;
1781
+ const { done, value } = await iterator.next();
1782
+ if (done) break;
1783
+ const text = String(value);
1784
+ let eventType = "unknown";
1785
+ try {
1786
+ eventType = JSON.parse(text).type ?? "unknown";
1787
+ } catch {}
1788
+ res.write(`event: ${eventType}\ndata: ${text}\n\n`);
1731
1789
  sse.pushJson(text);
1790
+ if (sse.sawAnthropicMessageStop) {
1791
+ semanticComplete = true;
1792
+ break;
1793
+ }
1794
+ }
1795
+ else {
1796
+ while (true) {
1797
+ if (abortSignal.aborted) break;
1798
+ const { done, value } = await iterator.next();
1799
+ if (done) break;
1800
+ const text = String(value);
1801
+ res.write(`data: ${text}\n\n`);
1802
+ sse.pushJson(text);
1803
+ }
1804
+ if (!abortSignal.aborted) res.write("data: [DONE]\n\n");
1732
1805
  }
1733
- res.write("data: [DONE]\n\n");
1806
+ } catch (err) {
1807
+ const msg = err instanceof Error ? err.message : String(err);
1808
+ if (!(abortSignal.aborted || semanticComplete) || !msg.includes("terminated")) throw err;
1809
+ } finally {
1810
+ await iterator.return?.().catch(() => {});
1734
1811
  }
1735
1812
  dbg(`stream done, ${sse.result ? `${sse.result.model} ${sse.result.inputTokens}+${sse.result.outputTokens}t` : "no usage"}`);
1736
- res.end();
1813
+ if (!res.writableEnded) res.end();
1737
1814
  mpp.shiftPayment();
1738
- appendInferenceHistory({
1815
+ if (shouldAppendInferenceHistory({
1816
+ isLlmEndpoint,
1817
+ usage: sse.result
1818
+ })) appendInferenceHistory({
1739
1819
  historyPath,
1740
1820
  allModels,
1741
1821
  walletAddress,
@@ -1752,7 +1832,7 @@ async function handleMppRequest(opts) {
1752
1832
  if (response.status === 402) {
1753
1833
  const responseBody = await response.text();
1754
1834
  logger.error(`mpp: payment failed, raw response: ${responseBody}`);
1755
- appendHistory(historyPath, {
1835
+ if (isLlmEndpoint) appendHistory(historyPath, {
1756
1836
  t: Date.now(),
1757
1837
  ok: false,
1758
1838
  kind: "x402_inference",
@@ -1772,14 +1852,19 @@ async function handleMppRequest(opts) {
1772
1852
  const usageTracker = createSseTracker();
1773
1853
  usageTracker.pushJson(responseBody);
1774
1854
  const payment = mpp.shiftPayment();
1775
- appendInferenceHistory({
1855
+ const amount = parseMppAmount(payment?.amount);
1856
+ if (shouldAppendInferenceHistory({
1857
+ isLlmEndpoint,
1858
+ amount,
1859
+ usage: usageTracker.result
1860
+ })) appendInferenceHistory({
1776
1861
  historyPath,
1777
1862
  allModels,
1778
1863
  walletAddress,
1779
1864
  paymentNetwork: payment?.network ?? "eip155:4217",
1780
1865
  paymentTo: void 0,
1781
1866
  tx: tx ?? payment?.receipt?.reference,
1782
- amount: parseMppAmount(payment?.amount),
1867
+ amount,
1783
1868
  thinkingMode,
1784
1869
  usage: usageTracker.result,
1785
1870
  durationMs: Date.now() - startMs
@@ -1788,7 +1873,7 @@ async function handleMppRequest(opts) {
1788
1873
  } catch (err) {
1789
1874
  dbg(`mpp error: ${String(err)}`);
1790
1875
  logger.error(`mpp: fetch threw: ${String(err)}`);
1791
- appendHistory(historyPath, {
1876
+ if (isLlmEndpoint) appendHistory(historyPath, {
1792
1877
  t: Date.now(),
1793
1878
  ok: false,
1794
1879
  kind: "x402_inference",
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { t as displayStatus } from "./status-DihAcUSC.js";
3
+ export { displayStatus };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as resolveWallet, f as formatAmount, g as dim, h as readHistory, m as formatUsdcValue, n as fetchAllBalances, p as formatTxLine, t as balanceLine, u as calcSpend } from "./wallet-LUl2l8WM.js";
2
+ import { c as resolveWallet, f as formatAmount, g as dim, h as readHistory, m as formatUsdcValue, n as fetchAllBalances, p as formatTxLine, t as balanceLine, u as calcSpend } from "./wallet-B0S-rma9.js";
3
3
  import { a as getHistoryPath, i as getConfigDirShort, l as loadConfig } from "./config-D9wIR3xc.js";
4
4
  import { buildCommand } from "@stricli/core";
5
5
  import pc from "picocolors";
@@ -34,6 +34,11 @@ function dim(msg) {
34
34
  function success(msg) {
35
35
  process.stderr.write(`${isTTY() ? pc.green(`✓ ${msg}`) : `✓ ${msg}`}\n`);
36
36
  }
37
+ function isMeaningfulInferenceRecord(record) {
38
+ if (record.kind !== "x402_inference") return true;
39
+ if (!record.ok) return true;
40
+ return record.amount != null || record.model != null || record.inputTokens != null || record.outputTokens != null || record.tx != null;
41
+ }
37
42
  function appendHistory(historyPath, record) {
38
43
  try {
39
44
  mkdirSync(dirname(historyPath), { recursive: true });
@@ -55,7 +60,8 @@ function readHistory(historyPath) {
55
60
  try {
56
61
  const parsed = JSON.parse(line);
57
62
  if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
58
- return [parsed];
63
+ const record = parsed;
64
+ return isMeaningfulInferenceRecord(record) ? [record] : [];
59
65
  } catch {
60
66
  return [];
61
67
  }
@@ -84,12 +90,12 @@ function calcSpend(records) {
84
90
  count
85
91
  };
86
92
  }
87
- /** Format a USDC value with adaptive precision (no token suffix). */
88
93
  function formatUsdcValue(amount) {
89
- if (amount >= .01) return amount.toFixed(2);
90
- if (amount >= .001) return amount.toFixed(3);
91
- if (amount >= 1e-4) return amount.toFixed(4);
92
- return amount.toFixed(6);
94
+ return new Intl.NumberFormat("en-US", {
95
+ useGrouping: false,
96
+ minimumFractionDigits: 0,
97
+ maximumFractionDigits: 12
98
+ }).format(amount);
93
99
  }
94
100
  function formatAmount(amount, token) {
95
101
  if (token === "USDC") return `${formatUsdcValue(amount)} USDC`;
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { n as fetchAllBalances } from "./wallet-B0S-rma9.js";
3
+ export { fetchAllBalances };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-proxy",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo. Also works as an OpenClaw plugin.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as displayStatus } from "./status-Dc1dB0ZG.js";
3
- export { displayStatus };
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as fetchAllBalances } from "./wallet-LUl2l8WM.js";
3
- export { fetchAllBalances };