x402-proxy 0.9.4 → 0.10.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.
@@ -1,12 +1,11 @@
1
1
  import os, { homedir } from "node:os";
2
2
  import path, { dirname, join } from "node:path";
3
- import { SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR, address, appendTransactionMessageInstructions, createDefaultRpcTransport, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcFromTransport, createTransactionMessage, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, isSolanaError, mainnet, partiallySignTransactionMessageWithSigners, pipe, prependTransactionMessageInstruction, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
3
+ import { SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR, address, appendTransactionMessageInstructions, createDefaultRpcTransport, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcFromTransport, createTransactionMessage, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, isSolanaError, mainnet, partiallySignTransactionMessageWithSigners, pipe, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
4
4
  import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from "@x402/fetch";
5
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
5
6
  import fs, { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
7
  import "yaml";
7
- import { getSetComputeUnitLimitInstruction, setTransactionMessageComputeUnitPrice } from "@solana-program/compute-budget";
8
- import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
9
- import { TOKEN_2022_PROGRAM_ADDRESS, fetchMint, findAssociatedTokenPda, getTransferCheckedInstruction } from "@solana-program/token-2022";
8
+ import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction } from "@solana-program/token";
10
9
  import { ed25519 } from "@noble/curves/ed25519.js";
11
10
  import { base58 } from "@scure/base";
12
11
  import "@x402/evm";
@@ -22,6 +21,8 @@ import { HDKey } from "@scure/bip32";
22
21
  import { mnemonicToSeedSync } from "@scure/bip39";
23
22
  import "@scure/bip39/wordlists/english.js";
24
23
  import { Type } from "@sinclair/typebox";
24
+ import { buildCommand } from "@stricli/core";
25
+ import pc from "picocolors";
25
26
  //#region src/handler.ts
26
27
  /**
27
28
  * Extract the on-chain transaction signature from an x402 payment response header.
@@ -59,6 +60,94 @@ function createX402ProxyHandler(opts) {
59
60
  shiftPayment: () => paymentQueue.shift()
60
61
  };
61
62
  }
63
+ const TEMPO_NETWORK = "eip155:4217";
64
+ /**
65
+ * Create an MPP proxy handler using mppx client.
66
+ * Dynamically imports mppx/client to keep startup fast.
67
+ */
68
+ async function createMppProxyHandler(opts) {
69
+ const { Mppx, tempo } = await import("mppx/client");
70
+ const { privateKeyToAccount } = await import("viem/accounts");
71
+ const account = privateKeyToAccount(opts.evmKey);
72
+ const maxDeposit = opts.maxDeposit ?? "1";
73
+ const paymentQueue = [];
74
+ let lastChallengeAmount;
75
+ const mppx = Mppx.create({
76
+ methods: [tempo({
77
+ account,
78
+ maxDeposit
79
+ })],
80
+ polyfill: false,
81
+ onChallenge: async (challenge) => {
82
+ const req = challenge.request;
83
+ if (req.amount) lastChallengeAmount = (Number(req.amount) / 10 ** (req.decimals ?? 6)).toString();
84
+ }
85
+ });
86
+ let session;
87
+ return {
88
+ async fetch(input, init) {
89
+ const response = await mppx.fetch(typeof input === "string" ? input : input.toString(), init);
90
+ const receiptHeader = response.headers.get("Payment-Receipt");
91
+ if (receiptHeader) {
92
+ try {
93
+ const receipt = JSON.parse(Buffer.from(receiptHeader, "base64url").toString());
94
+ paymentQueue.push({
95
+ protocol: "mpp",
96
+ network: TEMPO_NETWORK,
97
+ amount: lastChallengeAmount,
98
+ receipt
99
+ });
100
+ } catch {
101
+ paymentQueue.push({
102
+ protocol: "mpp",
103
+ network: TEMPO_NETWORK,
104
+ amount: lastChallengeAmount
105
+ });
106
+ }
107
+ lastChallengeAmount = void 0;
108
+ }
109
+ return response;
110
+ },
111
+ async sse(input, init) {
112
+ session ??= tempo.session({
113
+ account,
114
+ maxDeposit
115
+ });
116
+ const url = typeof input === "string" ? input : input.toString();
117
+ const iterable = await session.sse(url, init);
118
+ paymentQueue.push({
119
+ protocol: "mpp",
120
+ network: TEMPO_NETWORK,
121
+ intent: "session"
122
+ });
123
+ return iterable;
124
+ },
125
+ shiftPayment: () => paymentQueue.shift(),
126
+ async close() {
127
+ if (session?.opened) {
128
+ const receipt = await session.close();
129
+ if (receipt) {
130
+ const spentUsdc = receipt.spent ? (Number(receipt.spent) / 1e6).toString() : void 0;
131
+ paymentQueue.push({
132
+ protocol: "mpp",
133
+ network: TEMPO_NETWORK,
134
+ intent: "session",
135
+ amount: spentUsdc,
136
+ channelId: session.channelId ?? void 0,
137
+ receipt: {
138
+ method: receipt.method,
139
+ reference: receipt.reference,
140
+ status: receipt.status,
141
+ timestamp: receipt.timestamp,
142
+ acceptedCumulative: receipt.acceptedCumulative,
143
+ txHash: receipt.txHash
144
+ }
145
+ });
146
+ }
147
+ }
148
+ }
149
+ };
150
+ }
62
151
  //#endregion
63
152
  //#region src/lib/config.ts
64
153
  const APP_NAME = "x402-proxy";
@@ -87,7 +176,7 @@ function loadWalletFile() {
87
176
  //#region src/lib/optimized-svm-scheme.ts
88
177
  const MEMO_PROGRAM_ADDRESS = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
89
178
  const COMPUTE_UNIT_LIMIT = 2e4;
90
- const COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
179
+ const COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1n;
91
180
  const USDC_MINT$1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
92
181
  const USDC_DECIMALS$1 = 6;
93
182
  const MAINNET_RPC_URLS = ["https://api.mainnet.solana.com", "https://public.rpc.solanavibestation.com"];
@@ -133,35 +222,24 @@ var OptimizedSvmScheme = class {
133
222
  async createPaymentPayload(x402Version, paymentRequirements) {
134
223
  const rpc = this.rpc;
135
224
  const asset = paymentRequirements.asset;
136
- let tokenProgramAddress;
137
- let decimals;
138
- if (asset === USDC_MINT$1) {
139
- tokenProgramAddress = TOKEN_PROGRAM_ADDRESS;
140
- decimals = USDC_DECIMALS$1;
141
- } else {
142
- const tokenMint = await fetchMint(rpc, asset);
143
- tokenProgramAddress = tokenMint.programAddress;
144
- if (tokenProgramAddress !== TOKEN_PROGRAM_ADDRESS && tokenProgramAddress !== TOKEN_2022_PROGRAM_ADDRESS) throw new Error("Asset was not created by a known token program");
145
- decimals = tokenMint.data.decimals;
146
- }
147
- const [sourceATA] = await findAssociatedTokenPda({
225
+ if (asset !== USDC_MINT$1) throw new Error(`Unsupported asset: ${asset}. Only USDC is supported.`);
226
+ const [[sourceATA], [destinationATA]] = await Promise.all([findAssociatedTokenPda({
148
227
  mint: asset,
149
228
  owner: this.signer.address,
150
- tokenProgram: tokenProgramAddress
151
- });
152
- const [destinationATA] = await findAssociatedTokenPda({
229
+ tokenProgram: TOKEN_PROGRAM_ADDRESS
230
+ }), findAssociatedTokenPda({
153
231
  mint: asset,
154
232
  owner: paymentRequirements.payTo,
155
- tokenProgram: tokenProgramAddress
156
- });
233
+ tokenProgram: TOKEN_PROGRAM_ADDRESS
234
+ })]);
157
235
  const transferIx = getTransferCheckedInstruction({
158
236
  source: sourceATA,
159
237
  mint: asset,
160
238
  destination: destinationATA,
161
239
  authority: this.signer,
162
240
  amount: BigInt(paymentRequirements.amount),
163
- decimals
164
- }, { programAddress: tokenProgramAddress });
241
+ decimals: USDC_DECIMALS$1
242
+ });
165
243
  const feePayer = paymentRequirements.extra?.feePayer;
166
244
  if (!feePayer) throw new Error("feePayer is required in paymentRequirements.extra for SVM transactions");
167
245
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
@@ -173,7 +251,7 @@ var OptimizedSvmScheme = class {
173
251
  };
174
252
  return {
175
253
  x402Version,
176
- payload: { transaction: getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageComputeUnitPrice(COMPUTE_UNIT_PRICE_MICROLAMPORTS, tx), (tx) => setTransactionMessageFeePayer(feePayer, tx), (tx) => prependTransactionMessageInstruction(getSetComputeUnitLimitInstruction({ units: COMPUTE_UNIT_LIMIT }), tx), (tx) => appendTransactionMessageInstructions([transferIx, memoIx], tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)))) }
254
+ payload: { transaction: getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageComputeUnitPrice(COMPUTE_UNIT_PRICE_MICROLAMPORTS, tx), (tx) => setTransactionMessageComputeUnitLimit(COMPUTE_UNIT_LIMIT, tx), (tx) => setTransactionMessageFeePayer(feePayer, tx), (tx) => appendTransactionMessageInstructions([transferIx, memoIx], tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)))) }
177
255
  };
178
256
  }
179
257
  };
@@ -441,7 +519,7 @@ async function loadSvmWallet(keypairPath) {
441
519
  //#endregion
442
520
  //#region src/openclaw/solana.ts
443
521
  const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
444
- const TOKEN_PROGRAM = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
522
+ const TOKEN_PROGRAM$1 = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
445
523
  const ASSOCIATED_TOKEN_PROGRAM = address("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
446
524
  async function findAta(mint, owner) {
447
525
  const encoder = getAddressEncoder();
@@ -449,7 +527,7 @@ async function findAta(mint, owner) {
449
527
  programAddress: ASSOCIATED_TOKEN_PROGRAM,
450
528
  seeds: [
451
529
  encoder.encode(owner),
452
- encoder.encode(TOKEN_PROGRAM),
530
+ encoder.encode(TOKEN_PROGRAM$1),
453
531
  encoder.encode(mint)
454
532
  ]
455
533
  });
@@ -461,7 +539,7 @@ function transferCheckedIx(source, mint, destination, authority, amount, decimal
461
539
  new DataView(data.buffer).setBigUint64(1, amount, true);
462
540
  data[9] = decimals;
463
541
  return {
464
- programAddress: TOKEN_PROGRAM,
542
+ programAddress: TOKEN_PROGRAM$1,
465
543
  accounts: [
466
544
  {
467
545
  address: source,
@@ -484,7 +562,7 @@ function transferCheckedIx(source, mint, destination, authority, amount, decimal
484
562
  };
485
563
  }
486
564
  async function getTokenAccounts(rpcUrl, owner) {
487
- const { value } = await createSolanaRpc(rpcUrl).getTokenAccountsByOwner(address(owner), { programId: TOKEN_PROGRAM }, { encoding: "jsonParsed" }).send();
565
+ const { value } = await createSolanaRpc(rpcUrl).getTokenAccountsByOwner(address(owner), { programId: TOKEN_PROGRAM$1 }, { encoding: "jsonParsed" }).send();
488
566
  return value.map((v) => {
489
567
  const info = v.account.data.parsed.info;
490
568
  return {
@@ -530,6 +608,158 @@ async function transferUsdc(signer, rpcUrl, dest, amountRaw) {
530
608
  return await rpc.sendTransaction(encoded, { encoding: "base64" }).send();
531
609
  }
532
610
  //#endregion
611
+ //#region src/lib/output.ts
612
+ function isTTY() {
613
+ return !!process.stderr.isTTY;
614
+ }
615
+ function info(msg) {
616
+ process.stderr.write(`${isTTY() ? pc.cyan(msg) : msg}\n`);
617
+ }
618
+ function dim(msg) {
619
+ process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
620
+ }
621
+ //#endregion
622
+ //#region src/commands/wallet.ts
623
+ const BASE_RPC = "https://mainnet.base.org";
624
+ const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
625
+ const TEMPO_RPC = "https://rpc.presto.tempo.xyz";
626
+ const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
627
+ const USDC_TEMPO = "0x20C000000000000000000000b9537d11c60E8b50";
628
+ const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
629
+ const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
630
+ const ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
631
+ async function rpcCall(url, method, params) {
632
+ const res = await fetch(url, {
633
+ method: "POST",
634
+ headers: { "Content-Type": "application/json" },
635
+ body: JSON.stringify({
636
+ jsonrpc: "2.0",
637
+ method,
638
+ params,
639
+ id: 1
640
+ })
641
+ });
642
+ if (!res.ok) throw new Error(`RPC ${method} failed: ${res.status} ${res.statusText}`);
643
+ return res.json();
644
+ }
645
+ async function fetchEvmBalances(address) {
646
+ const usdcData = `0x70a08231${address.slice(2).padStart(64, "0")}`;
647
+ const [ethRes, usdcRes] = await Promise.all([rpcCall(BASE_RPC, "eth_getBalance", [address, "latest"]), rpcCall(BASE_RPC, "eth_call", [{
648
+ to: USDC_BASE,
649
+ data: usdcData
650
+ }, "latest"])]);
651
+ return {
652
+ eth: ethRes.result ? (Number(BigInt(ethRes.result)) / 0xde0b6b3a7640000).toFixed(6) : "?",
653
+ usdc: usdcRes.result ? formatUsdcValue(Number(BigInt(usdcRes.result)) / 1e6) : "?"
654
+ };
655
+ }
656
+ async function fetchTempoBalances(address) {
657
+ const res = await rpcCall(TEMPO_RPC, "eth_call", [{
658
+ to: USDC_TEMPO,
659
+ data: `0x70a08231${address.slice(2).padStart(64, "0")}`
660
+ }, "latest"]);
661
+ return { usdc: res.result ? formatUsdcValue(Number(BigInt(res.result)) / 1e6) : "?" };
662
+ }
663
+ async function getUsdcAta(owner) {
664
+ const encoder = getAddressEncoder();
665
+ const [ata] = await getProgramDerivedAddress({
666
+ programAddress: address(ATA_PROGRAM),
667
+ seeds: [
668
+ encoder.encode(address(owner)),
669
+ encoder.encode(address(TOKEN_PROGRAM)),
670
+ encoder.encode(address(USDC_SOLANA_MINT))
671
+ ]
672
+ });
673
+ return ata;
674
+ }
675
+ async function fetchSolanaBalances(ownerAddress) {
676
+ const ata = await getUsdcAta(ownerAddress);
677
+ const [solRes, usdcRes] = await Promise.all([rpcCall(SOLANA_RPC, "getBalance", [ownerAddress]), rpcCall(SOLANA_RPC, "getTokenAccountBalance", [ata])]);
678
+ const sol = solRes.result?.value != null ? (solRes.result.value / 1e9).toFixed(6) : "?";
679
+ const usdcVal = usdcRes.result?.value;
680
+ return {
681
+ sol,
682
+ usdc: usdcVal ? formatUsdcValue(Number(usdcVal.uiAmountString)) : "0"
683
+ };
684
+ }
685
+ function balanceLine(usdc, native, nativeSymbol) {
686
+ return pc.dim(` (${usdc} USDC, ${native} ${nativeSymbol})`);
687
+ }
688
+ async function fetchAllBalances(evmAddress, solanaAddress) {
689
+ const [evmResult, solResult, tempoResult] = await Promise.allSettled([
690
+ evmAddress ? fetchEvmBalances(evmAddress) : Promise.resolve(null),
691
+ solanaAddress ? fetchSolanaBalances(solanaAddress) : Promise.resolve(null),
692
+ evmAddress ? fetchTempoBalances(evmAddress) : Promise.resolve(null)
693
+ ]);
694
+ return {
695
+ evm: evmResult.status === "fulfilled" ? evmResult.value : null,
696
+ sol: solResult.status === "fulfilled" ? solResult.value : null,
697
+ tempo: tempoResult.status === "fulfilled" ? tempoResult.value : null
698
+ };
699
+ }
700
+ buildCommand({
701
+ docs: { brief: "Show wallet addresses and balances" },
702
+ parameters: {
703
+ flags: { verbose: {
704
+ kind: "boolean",
705
+ brief: "Show transaction IDs",
706
+ default: false
707
+ } },
708
+ positional: {
709
+ kind: "tuple",
710
+ parameters: []
711
+ }
712
+ },
713
+ async func(flags) {
714
+ const wallet = resolveWallet();
715
+ if (wallet.source === "none") {
716
+ console.log(pc.yellow("No wallet configured."));
717
+ console.log(pc.dim(`\nRun:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
718
+ process.exit(1);
719
+ }
720
+ console.log();
721
+ info("Wallet");
722
+ console.log();
723
+ console.log(pc.dim(` Source: ${wallet.source}`));
724
+ const { evm, sol, tempo } = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
725
+ if (wallet.evmAddress) {
726
+ const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
727
+ console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
728
+ }
729
+ if (wallet.evmAddress) {
730
+ const bal = tempo ? pc.dim(` (${tempo.usdc} USDC)`) : pc.dim(" (network error)");
731
+ console.log(` Tempo: ${pc.green(wallet.evmAddress)}${bal}`);
732
+ }
733
+ if (wallet.solanaAddress) {
734
+ const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
735
+ console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
736
+ }
737
+ const evmEmpty = !evm || Number(evm.usdc) === 0;
738
+ const solEmpty = !sol || Number(sol.usdc) === 0;
739
+ const tempoEmpty = !tempo || Number(tempo.usdc) === 0;
740
+ if (evmEmpty && solEmpty && tempoEmpty) {
741
+ console.log();
742
+ dim(" Send USDC to any address above to start using paid APIs.");
743
+ }
744
+ console.log();
745
+ const records = readHistory(getHistoryPath());
746
+ if (records.length > 0) {
747
+ const spend = calcSpend(records);
748
+ const recent = records.slice(-10);
749
+ dim(" Recent transactions:");
750
+ for (const r of recent) {
751
+ const line = formatTxLine(r, { verbose: flags.verbose }).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
752
+ console.log(line);
753
+ }
754
+ console.log();
755
+ console.log(pc.dim(` Today: ${formatAmount(spend.today, "USDC")} | Total: ${formatAmount(spend.total, "USDC")} | ${spend.count} tx`));
756
+ } else dim(" No transactions yet.");
757
+ console.log();
758
+ console.log(pc.dim(" See also: wallet history, wallet export-key"));
759
+ console.log();
760
+ }
761
+ });
762
+ //#endregion
533
763
  //#region src/openclaw/tools.ts
534
764
  const SOL_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
535
765
  const USDC_DECIMALS = 6;
@@ -540,6 +770,11 @@ function paymentAmount(payment) {
540
770
  const parsed = Number.parseFloat(payment.amount);
541
771
  return Number.isNaN(parsed) ? void 0 : parsed / 10 ** USDC_DECIMALS;
542
772
  }
773
+ function parseMppAmount(value) {
774
+ if (!value) return void 0;
775
+ const parsed = Number(value);
776
+ return Number.isFinite(parsed) ? parsed : void 0;
777
+ }
543
778
  function toolResult(text) {
544
779
  return {
545
780
  content: [{
@@ -549,11 +784,23 @@ function toolResult(text) {
549
784
  details: {}
550
785
  };
551
786
  }
552
- async function getWalletSnapshot(rpcUrl, wallet, historyPath) {
553
- const [{ ui, raw }, sol, tokens] = await Promise.all([
554
- getUsdcBalance(rpcUrl, wallet),
555
- getSolBalance(rpcUrl, wallet),
556
- getTokenAccounts(rpcUrl, wallet).catch(() => [])
787
+ function addressForNetwork(evmAddress, solanaAddress, network) {
788
+ if (network?.startsWith("eip155:")) return evmAddress ?? solanaAddress;
789
+ if (network?.startsWith("solana:")) return solanaAddress ?? evmAddress;
790
+ return solanaAddress ?? evmAddress;
791
+ }
792
+ function walletAddressForNetwork(ctx, network) {
793
+ return addressForNetwork(ctx.getEvmWalletAddress(), ctx.getSolanaWalletAddress(), network) ?? "unknown";
794
+ }
795
+ async function getWalletSnapshot(rpcUrl, solanaWallet, evmWallet, historyPath) {
796
+ const [{ ui, raw }, sol, tokens, balances] = await Promise.all([
797
+ solanaWallet ? getUsdcBalance(rpcUrl, solanaWallet) : Promise.resolve({
798
+ ui: "0",
799
+ raw: 0n
800
+ }),
801
+ solanaWallet ? getSolBalance(rpcUrl, solanaWallet) : Promise.resolve("0"),
802
+ solanaWallet ? getTokenAccounts(rpcUrl, solanaWallet).catch(() => []) : Promise.resolve([]),
803
+ fetchAllBalances(evmWallet ?? void 0, solanaWallet ?? void 0)
557
804
  ]);
558
805
  const records = readHistory(historyPath);
559
806
  return {
@@ -561,61 +808,80 @@ async function getWalletSnapshot(rpcUrl, wallet, historyPath) {
561
808
  raw,
562
809
  sol,
563
810
  tokens,
811
+ balances,
564
812
  records,
565
813
  spend: calcSpend(records)
566
814
  };
567
815
  }
568
- function createBalanceTool(ctx) {
816
+ function createWalletTool(ctx) {
569
817
  return {
570
- name: "x_balance",
571
- label: "Wallet Balance",
572
- description: "Check wallet SOL and USDC balances. Use before making payments to verify sufficient funds.",
818
+ name: "x_wallet",
819
+ label: "Wallet Status",
820
+ description: "Check wallet readiness, balances, and spend history for x402 and MPP payments before making paid requests.",
573
821
  parameters: Type.Object({}),
574
822
  async execute() {
575
- const walletAddress = ctx.getWalletAddress();
576
- if (!walletAddress) return toolResult("Wallet not loaded yet. Wait for gateway startup.");
823
+ await ctx.ensureReady();
824
+ const solanaWallet = ctx.getSolanaWalletAddress();
825
+ const evmWallet = ctx.getEvmWalletAddress();
826
+ if (!solanaWallet && !evmWallet) return toolResult("Wallet not configured yet. Run `x402-proxy setup` or set X402_PROXY_WALLET_MNEMONIC.");
577
827
  try {
578
- const snap = await getWalletSnapshot(ctx.rpcUrl, walletAddress, ctx.historyPath);
828
+ const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
579
829
  const total = Number.parseFloat(snap.ui);
580
830
  const available = Math.max(0, total - INFERENCE_RESERVE);
581
831
  const tokenLines = snap.tokens.slice(0, 5).map((t) => {
582
832
  const short = `${t.mint.slice(0, 4)}...${t.mint.slice(-4)}`;
583
833
  return `${Number.parseFloat(t.amount).toLocaleString("en-US", { maximumFractionDigits: 0 })} (${short})`;
584
834
  });
585
- return toolResult([
586
- `Wallet: ${walletAddress}`,
587
- `SOL: ${snap.sol} SOL`,
588
- `USDC: ${snap.ui} USDC`,
589
- `Available for tools: ${available.toFixed(2)} USDC`,
590
- `Reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC`,
591
- `Spent today: ${formatAmount(snap.spend.today, "USDC")}`,
592
- `Total spent: ${formatAmount(snap.spend.total, "USDC")} (${snap.spend.count} txs)`,
593
- ...tokenLines.length > 0 ? [`Tokens held: ${tokenLines.join(", ")}`] : []
594
- ].join("\n"));
835
+ const lines = [
836
+ `Default protocol: ${ctx.getDefaultRequestProtocol()}`,
837
+ `MPP budget: ${ctx.getDefaultMppSessionBudget()} USDC`,
838
+ `MPP ready: ${evmWallet ? "yes" : "no"}`,
839
+ `x402 ready: ${solanaWallet ? "yes" : "no"}`
840
+ ];
841
+ if (evmWallet) {
842
+ lines.push(`Base wallet: ${evmWallet}`);
843
+ lines.push(`Base balance: ${snap.balances.evm?.usdc ?? "?"} USDC`);
844
+ lines.push(`Tempo balance: ${snap.balances.tempo?.usdc ?? "?"} USDC`);
845
+ }
846
+ if (solanaWallet) {
847
+ lines.push(`Solana wallet: ${solanaWallet}`);
848
+ lines.push(`Solana: ${snap.sol} SOL`);
849
+ lines.push(`Solana USDC: ${snap.ui} USDC`);
850
+ lines.push(`Available for x402 tools: ${available.toFixed(2)} USDC`);
851
+ lines.push(`Reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC`);
852
+ }
853
+ lines.push(`Spent today: ${formatAmount(snap.spend.today, "USDC")}`);
854
+ lines.push(`Total spent: ${formatAmount(snap.spend.total, "USDC")} (${snap.spend.count} txs)`);
855
+ if (tokenLines.length > 0) lines.push(`Tokens held: ${tokenLines.join(", ")}`);
856
+ if (!evmWallet) lines.push("MPP setup hint: set X402_PROXY_WALLET_MNEMONIC or X402_PROXY_WALLET_EVM_KEY, or run x402-proxy setup.");
857
+ if (!solanaWallet) lines.push("x402 setup hint: add a Solana wallet or mnemonic if you want to pay Solana x402 endpoints.");
858
+ return toolResult(lines.join("\n"));
595
859
  } catch (err) {
596
- return toolResult(`Failed to check balance: ${String(err)}`);
860
+ return toolResult(`Failed to check wallet status: ${String(err)}`);
597
861
  }
598
862
  }
599
863
  };
600
864
  }
601
- function createPaymentTool(ctx) {
865
+ function createRequestTool(ctx) {
602
866
  return {
603
- name: "x_payment",
604
- label: "x402 Payment",
605
- description: `Call an x402-enabled paid API endpoint with automatic USDC payment on Solana. Use this when you need to call a paid service given by the user. Note: ${INFERENCE_RESERVE.toFixed(2)} USDC is reserved for LLM inference and cannot be spent by this tool.`,
867
+ name: "x_request",
868
+ label: "Paid Request",
869
+ description: "Call a paid HTTP endpoint with automatic x402 or MPP settlement. Defaults to the plugin protocol and supports explicit protocol override.",
606
870
  parameters: Type.Object({
607
- url: Type.String({ description: "The x402-enabled endpoint URL" }),
871
+ url: Type.String({ description: "The paid endpoint URL" }),
608
872
  method: Type.Optional(Type.String({ description: "HTTP method (default: GET)" })),
609
873
  params: Type.Optional(Type.String({ description: "For GET: query params as JSON object. For POST/PUT/PATCH: JSON request body." })),
610
- headers: Type.Optional(Type.String({ description: "Custom HTTP headers as JSON object" }))
874
+ headers: Type.Optional(Type.String({ description: "Custom HTTP headers as JSON object" })),
875
+ protocol: Type.Optional(Type.Union([
876
+ Type.Literal("x402"),
877
+ Type.Literal("mpp"),
878
+ Type.Literal("auto")
879
+ ], { description: "Override the default payment protocol for this request." }))
611
880
  }),
612
881
  async execute(_id, params) {
613
- const walletAddress = ctx.getWalletAddress();
614
- if (!walletAddress) return toolResult("Wallet not loaded yet. Wait for gateway startup.");
615
- try {
616
- const { ui } = await getUsdcBalance(ctx.rpcUrl, walletAddress);
617
- if (Number.parseFloat(ui) <= INFERENCE_RESERVE) return toolResult(`Insufficient funds. Balance: ${ui} USDC, reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC. Top up wallet: ${walletAddress}`);
618
- } catch {}
882
+ await ctx.ensureReady();
883
+ const solanaWallet = ctx.getSolanaWalletAddress();
884
+ const evmWallet = ctx.getEvmWalletAddress();
619
885
  const method = (params.method || "GET").toUpperCase();
620
886
  let url = params.url;
621
887
  const reqInit = { method };
@@ -638,35 +904,101 @@ function createPaymentTool(ctx) {
638
904
  ...reqInit.headers
639
905
  };
640
906
  }
907
+ const protocol = params.protocol && [
908
+ "x402",
909
+ "mpp",
910
+ "auto"
911
+ ].includes(params.protocol) ? params.protocol : ctx.getDefaultRequestProtocol();
641
912
  const toolStartMs = Date.now();
913
+ if ((protocol === "mpp" || protocol === "auto") && !ctx.getEvmKey()) return toolResult("MPP request failed: no EVM wallet configured. Run x402-proxy setup or set X402_PROXY_WALLET_EVM_KEY.");
914
+ if (protocol === "x402") {
915
+ if (!solanaWallet) return toolResult("x402 request failed: no Solana wallet configured. Add a mnemonic or Solana key first.");
916
+ try {
917
+ const { ui } = await getUsdcBalance(ctx.rpcUrl, solanaWallet);
918
+ if (Number.parseFloat(ui) <= INFERENCE_RESERVE) return toolResult(`Insufficient funds. Balance: ${ui} USDC, reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC. Top up wallet: ${solanaWallet}`);
919
+ } catch {}
920
+ const proxy = ctx.getX402Proxy();
921
+ if (!proxy) return toolResult("x402 wallet is not ready yet. Try again in a moment.");
922
+ try {
923
+ const response = await proxy.x402Fetch(url, reqInit);
924
+ const body = await response.text();
925
+ if (response.status === 402) {
926
+ const payment = proxy.shiftPayment();
927
+ appendHistory(ctx.historyPath, {
928
+ t: Date.now(),
929
+ ok: false,
930
+ kind: "x402_payment",
931
+ net: payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
932
+ from: walletAddressForNetwork(ctx, payment?.network),
933
+ label: url,
934
+ ms: Date.now() - toolStartMs,
935
+ error: "payment_required"
936
+ });
937
+ return toolResult(`Payment failed (402): ${body.substring(0, 500)}. Wallet: ${walletAddressForNetwork(ctx, payment?.network)}`);
938
+ }
939
+ const payment = proxy.shiftPayment();
940
+ const amount = paymentAmount(payment);
941
+ appendHistory(ctx.historyPath, {
942
+ t: Date.now(),
943
+ ok: true,
944
+ kind: "x402_payment",
945
+ net: payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
946
+ from: walletAddressForNetwork(ctx, payment?.network),
947
+ to: payment?.payTo,
948
+ tx: extractTxSignature(response),
949
+ amount,
950
+ token: "USDC",
951
+ label: url,
952
+ ms: Date.now() - toolStartMs
953
+ });
954
+ const truncated = body.length > MAX_RESPONSE_CHARS ? `${body.substring(0, MAX_RESPONSE_CHARS)}\n\n[Truncated - response was ${body.length} chars]` : body;
955
+ return toolResult(`HTTP ${response.status}\n\n${truncated}`);
956
+ } catch (err) {
957
+ ctx.getX402Proxy()?.shiftPayment();
958
+ appendHistory(ctx.historyPath, {
959
+ t: Date.now(),
960
+ ok: false,
961
+ kind: "x402_payment",
962
+ net: SOL_MAINNET,
963
+ from: solanaWallet,
964
+ label: params.url,
965
+ error: String(err).substring(0, 200)
966
+ });
967
+ const msg = String(err);
968
+ return toolResult(msg.includes("Simulation failed") || msg.includes("insufficient") ? `Payment failed - insufficient funds. Wallet: ${solanaWallet}. Error: ${msg}` : `Request failed: ${msg}`);
969
+ }
970
+ }
971
+ const evmKey = ctx.getEvmKey();
972
+ if (!evmKey) return toolResult("MPP request failed: no EVM wallet configured. Run x402-proxy setup or set X402_PROXY_WALLET_EVM_KEY.");
973
+ const mpp = await createMppProxyHandler({
974
+ evmKey,
975
+ maxDeposit: ctx.getDefaultMppSessionBudget()
976
+ });
642
977
  try {
643
- const response = await ctx.proxy.x402Fetch(url, reqInit);
978
+ const response = await mpp.fetch(url, reqInit);
644
979
  const body = await response.text();
980
+ const payment = mpp.shiftPayment();
645
981
  if (response.status === 402) {
646
- ctx.proxy.shiftPayment();
647
982
  appendHistory(ctx.historyPath, {
648
983
  t: Date.now(),
649
984
  ok: false,
650
- kind: "x402_payment",
651
- net: SOL_MAINNET,
652
- from: walletAddress,
985
+ kind: "mpp_payment",
986
+ net: payment?.network ?? "eip155:4217",
987
+ from: evmWallet ?? "unknown",
653
988
  label: url,
654
989
  ms: Date.now() - toolStartMs,
655
990
  error: "payment_required"
656
991
  });
657
- return toolResult(`Payment failed (402): ${body.substring(0, 500)}. Wallet: ${walletAddress}`);
992
+ return toolResult(`MPP payment failed (402): ${body.substring(0, 500)}`);
658
993
  }
659
- const payment = ctx.proxy.shiftPayment();
660
- const amount = paymentAmount(payment);
661
994
  appendHistory(ctx.historyPath, {
662
995
  t: Date.now(),
663
996
  ok: true,
664
- kind: "x402_payment",
665
- net: SOL_MAINNET,
666
- from: walletAddress,
667
- to: payment?.payTo,
668
- tx: extractTxSignature(response),
669
- amount,
997
+ kind: "mpp_payment",
998
+ net: payment?.network ?? "eip155:4217",
999
+ from: evmWallet ?? "unknown",
1000
+ tx: extractTxSignature(response) ?? payment?.receipt?.reference,
1001
+ amount: parseMppAmount(payment?.amount),
670
1002
  token: "USDC",
671
1003
  label: url,
672
1004
  ms: Date.now() - toolStartMs
@@ -674,18 +1006,18 @@ function createPaymentTool(ctx) {
674
1006
  const truncated = body.length > MAX_RESPONSE_CHARS ? `${body.substring(0, MAX_RESPONSE_CHARS)}\n\n[Truncated - response was ${body.length} chars]` : body;
675
1007
  return toolResult(`HTTP ${response.status}\n\n${truncated}`);
676
1008
  } catch (err) {
677
- ctx.proxy.shiftPayment();
678
1009
  appendHistory(ctx.historyPath, {
679
1010
  t: Date.now(),
680
1011
  ok: false,
681
- kind: "x402_payment",
682
- net: SOL_MAINNET,
683
- from: walletAddress,
1012
+ kind: "mpp_payment",
1013
+ net: TEMPO_NETWORK,
1014
+ from: evmWallet ?? "unknown",
684
1015
  label: params.url,
685
1016
  error: String(err).substring(0, 200)
686
1017
  });
687
- const msg = String(err);
688
- return toolResult(msg.includes("Simulation failed") || msg.includes("insufficient") ? `Payment failed - insufficient funds. Wallet: ${walletAddress}. Error: ${msg}` : `Request failed: ${msg}`);
1018
+ return toolResult(`MPP request failed: ${String(err)}`);
1019
+ } finally {
1020
+ await mpp.close().catch(() => {});
689
1021
  }
690
1022
  }
691
1023
  };
@@ -695,8 +1027,10 @@ function createPaymentTool(ctx) {
695
1027
  const HISTORY_PAGE_SIZE = 5;
696
1028
  const STATUS_HISTORY_COUNT = 3;
697
1029
  const INLINE_HISTORY_TOKEN_THRESHOLD = 3;
1030
+ const SEND_CONFIRM_TTL_MS = 300 * 1e3;
698
1031
  const TOKEN_SYMBOL_CACHE_MAX = 200;
699
1032
  const tokenSymbolCache = /* @__PURE__ */ new Map();
1033
+ const pendingSends = /* @__PURE__ */ new Map();
700
1034
  async function resolveTokenSymbols(mints) {
701
1035
  const result = /* @__PURE__ */ new Map();
702
1036
  const toResolve = [];
@@ -725,13 +1059,18 @@ async function resolveTokenSymbols(mints) {
725
1059
  } catch {}
726
1060
  return result;
727
1061
  }
728
- async function handleSend(parts, wallet, signer, rpc, histPath) {
1062
+ function senderKey(cmdCtx) {
1063
+ return [
1064
+ cmdCtx.channel,
1065
+ cmdCtx.accountId ?? "",
1066
+ cmdCtx.senderId ?? cmdCtx.from ?? "",
1067
+ String(cmdCtx.messageThreadId ?? "")
1068
+ ].join(":");
1069
+ }
1070
+ async function executeSend(amountStr, destination, wallet, signer, rpc, histPath) {
729
1071
  if (!signer) return { text: "Wallet not loaded yet. Please wait for the gateway to finish starting." };
730
- if (parts.length !== 2) return { text: "Usage: `/x_wallet send <amount|all> <address>`\n\n `/x_wallet send 0.5 7xKXtg...`\n `/x_wallet send all 7xKXtg...`" };
731
- const [amountStr, destAddr] = parts;
732
- if (destAddr.length < 32 || destAddr.length > 44) return { text: `Invalid Solana address: ${destAddr}` };
733
1072
  try {
734
- if (!await checkAtaExists(rpc, destAddr)) return { text: "Recipient does not have a USDC token account.\nThey need to receive USDC at least once to create one." };
1073
+ if (!await checkAtaExists(rpc, destination)) return { text: "Recipient does not have a USDC token account.\nThey need to receive USDC at least once to create one." };
735
1074
  let amountRaw;
736
1075
  let amountUi;
737
1076
  if (amountStr.toLowerCase() === "all") {
@@ -745,20 +1084,20 @@ async function handleSend(parts, wallet, signer, rpc, histPath) {
745
1084
  amountRaw = BigInt(Math.round(amount * 1e6));
746
1085
  amountUi = amount.toString();
747
1086
  }
748
- const sig = await transferUsdc(signer, rpc, destAddr, amountRaw);
1087
+ const sig = await transferUsdc(signer, rpc, destination, amountRaw);
749
1088
  appendHistory(histPath, {
750
1089
  t: Date.now(),
751
1090
  ok: true,
752
1091
  kind: "transfer",
753
1092
  net: SOL_MAINNET,
754
1093
  from: wallet,
755
- to: destAddr,
1094
+ to: destination,
756
1095
  tx: sig,
757
1096
  amount: Number.parseFloat(amountUi),
758
1097
  token: "USDC",
759
- label: `${destAddr.slice(0, 4)}...${destAddr.slice(-4)}`
1098
+ label: `${destination.slice(0, 4)}...${destination.slice(-4)}`
760
1099
  });
761
- return { text: `Sent ${amountUi} USDC to \`${destAddr}\`\n[View transaction](https://solscan.io/tx/${sig})` };
1100
+ return { text: `Sent ${amountUi} USDC to \`${destination}\`\n[View transaction](https://solscan.io/tx/${sig})` };
762
1101
  } catch (err) {
763
1102
  appendHistory(histPath, {
764
1103
  t: Date.now(),
@@ -766,7 +1105,7 @@ async function handleSend(parts, wallet, signer, rpc, histPath) {
766
1105
  kind: "transfer",
767
1106
  net: SOL_MAINNET,
768
1107
  from: wallet,
769
- to: destAddr,
1108
+ to: destination,
770
1109
  token: "USDC",
771
1110
  error: String(err).substring(0, 200)
772
1111
  });
@@ -794,31 +1133,46 @@ function handleHistory(histPath, page) {
794
1133
  function createWalletCommand(ctx) {
795
1134
  return {
796
1135
  name: "x_wallet",
797
- description: "Wallet status, balance, send USDC, transaction history",
1136
+ description: "Wallet status, balances, payment readiness, and transaction history",
798
1137
  acceptsArgs: true,
1138
+ requireAuth: true,
799
1139
  handler: async (cmdCtx) => {
800
- const walletAddress = ctx.getWalletAddress();
801
- if (!walletAddress) return { text: "Wallet not loaded yet. Please wait for the gateway to finish starting." };
1140
+ await ctx.ensureReady();
1141
+ const solanaWallet = ctx.getSolanaWalletAddress();
1142
+ const evmWallet = ctx.getEvmWalletAddress();
1143
+ if (!solanaWallet && !evmWallet) return { text: "Wallet not configured yet.\nRun `x402-proxy setup` or set `X402_PROXY_WALLET_MNEMONIC` on the gateway host." };
802
1144
  const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
803
- if (parts[0]?.toLowerCase() === "send") return handleSend(parts.slice(1), walletAddress, ctx.getSigner(), ctx.rpcUrl, ctx.historyPath);
804
1145
  if (parts[0]?.toLowerCase() === "history") {
805
1146
  const pageArg = parts[1];
806
1147
  const page = pageArg ? Math.max(1, Number.parseInt(pageArg, 10) || 1) : 1;
807
1148
  return handleHistory(ctx.historyPath, page);
808
1149
  }
1150
+ if (parts[0]?.toLowerCase() === "send") return { text: "Use `/x_send <amount|all> <address>` for transfers." };
809
1151
  try {
810
- const snap = await getWalletSnapshot(ctx.rpcUrl, walletAddress, ctx.historyPath);
811
- const solscanUrl = `https://solscan.io/account/${walletAddress}`;
812
- const lines = [`x402-proxy v0.9.4`];
1152
+ const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
1153
+ const lines = [`x402-proxy v0.10.1`];
813
1154
  const defaultModel = ctx.allModels[0];
814
1155
  if (defaultModel) lines.push("", `**Model** - ${defaultModel.name} (${defaultModel.provider})`);
815
- lines.push("", `**[Wallet](${solscanUrl})**`, `\`${walletAddress}\``);
816
- lines.push("", ` ${snap.sol} SOL`, ` ${snap.ui} USDC`);
817
- if (snap.spend.today > 0) lines.push(` -${snap.spend.today.toFixed(2)} USDC today`);
1156
+ lines.push("", `**Protocol** - ${ctx.getDefaultRequestProtocol()}`);
1157
+ lines.push(`MPP session budget: ${ctx.getDefaultMppSessionBudget()} USDC`);
1158
+ lines.push(`MPP ready: ${evmWallet ? "yes" : "no"}`);
1159
+ lines.push(`x402 ready: ${solanaWallet ? "yes" : "no"}`);
1160
+ if (evmWallet) {
1161
+ lines.push("", "**EVM / Tempo**");
1162
+ lines.push(`\`${evmWallet}\``);
1163
+ lines.push(` Base: ${snap.balances.evm?.usdc ?? "?"} USDC`);
1164
+ lines.push(` Tempo: ${snap.balances.tempo?.usdc ?? "?"} USDC`);
1165
+ }
1166
+ if (solanaWallet) {
1167
+ const solscanUrl = `https://solscan.io/account/${solanaWallet}`;
1168
+ lines.push("", `**[Solana Wallet](${solscanUrl})**`, `\`${solanaWallet}\``);
1169
+ lines.push(` ${snap.sol} SOL`, ` ${snap.ui} USDC`);
1170
+ if (snap.spend.today > 0) lines.push(` -${snap.spend.today.toFixed(2)} USDC today`);
1171
+ }
818
1172
  if (snap.tokens.length > 0) {
819
1173
  const displayTokens = snap.tokens.slice(0, 10);
820
1174
  const symbols = await resolveTokenSymbols(displayTokens.map((t) => t.mint));
821
- lines.push("", "**Tokens**");
1175
+ lines.push("", "**Solana Tokens**");
822
1176
  for (const t of displayTokens) {
823
1177
  const label = symbols.get(t.mint) ?? `${t.mint.slice(0, 4)}...${t.mint.slice(-4)}`;
824
1178
  const amt = Number.parseFloat(t.amount).toLocaleString("en-US", { maximumFractionDigits: 0 });
@@ -833,21 +1187,199 @@ function createWalletCommand(ctx) {
833
1187
  for (const r of recentRecords) lines.push(formatTxLine(r));
834
1188
  }
835
1189
  }
836
- lines.push("", "History: `/x_wallet history`");
1190
+ if (!evmWallet) lines.push("", "MPP setup: add an EVM wallet via `x402-proxy setup` or `X402_PROXY_WALLET_EVM_KEY`.");
1191
+ if (!solanaWallet) lines.push("", "x402 setup: add a Solana wallet or mnemonic if you need Solana x402.");
1192
+ lines.push("", "History: `/x_wallet history`", "Send: `/x_send <amount|all> <address>`");
837
1193
  if (ctx.dashboardUrl) lines.push(`[Dashboard](${ctx.dashboardUrl})`);
838
1194
  return { text: lines.join("\n") };
839
1195
  } catch (err) {
840
- return { text: `Failed to check balance: ${String(err)}` };
1196
+ return { text: `Failed to check wallet status: ${String(err)}` };
841
1197
  }
842
1198
  }
843
1199
  };
844
1200
  }
1201
+ function createSendCommand(ctx) {
1202
+ return {
1203
+ name: "x_send",
1204
+ description: "Send USDC from the plugin wallet with an explicit confirmation step",
1205
+ acceptsArgs: true,
1206
+ requireAuth: true,
1207
+ handler: async (cmdCtx) => {
1208
+ await ctx.ensureReady();
1209
+ const wallet = ctx.getSolanaWalletAddress();
1210
+ if (!wallet) return { text: "No Solana wallet configured. `/x_send` only works with the Solana USDC wallet." };
1211
+ const key = senderKey(cmdCtx);
1212
+ const now = Date.now();
1213
+ const pending = pendingSends.get(key);
1214
+ if (pending && now - pending.createdAt > SEND_CONFIRM_TTL_MS) pendingSends.delete(key);
1215
+ const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
1216
+ if (parts.length === 0) return { text: "Usage: `/x_send <amount|all> <address>`\nConfirm with `/x_send confirm`\nCancel with `/x_send cancel`" };
1217
+ if (parts[0]?.toLowerCase() === "confirm") {
1218
+ const next = pendingSends.get(key);
1219
+ if (!next) return { text: "No pending transfer to confirm." };
1220
+ pendingSends.delete(key);
1221
+ return executeSend(next.amount, next.destination, wallet, ctx.getSigner(), ctx.rpcUrl, ctx.historyPath);
1222
+ }
1223
+ if (parts[0]?.toLowerCase() === "cancel") {
1224
+ pendingSends.delete(key);
1225
+ return { text: "Pending transfer cleared." };
1226
+ }
1227
+ if (parts.length !== 2) return { text: "Usage: `/x_send <amount|all> <address>`\nExample: `/x_send 0.5 7xKXtg...`" };
1228
+ const [amount, destination] = parts;
1229
+ if (destination.length < 32 || destination.length > 44) return { text: `Invalid Solana address: ${destination}` };
1230
+ pendingSends.set(key, {
1231
+ amount,
1232
+ destination,
1233
+ createdAt: now
1234
+ });
1235
+ return { text: `Pending transfer: send ${amount} USDC to \`${destination}\`\nConfirm within 5 minutes with \`/x_send confirm\`\nCancel with \`/x_send cancel\`` };
1236
+ }
1237
+ };
1238
+ }
1239
+ //#endregion
1240
+ //#region src/openclaw/defaults.ts
1241
+ const DEFAULT_SURF_PROVIDER_ID = "surf";
1242
+ const DEFAULT_SURF_BASE_URL = "/x402-proxy/v1";
1243
+ const DEFAULT_SURF_UPSTREAM_URL = "https://surf.cascade.fyi/api/v1/inference";
1244
+ const DEFAULT_SURF_MODELS = [
1245
+ {
1246
+ id: "anthropic/claude-opus-4.6",
1247
+ name: "Claude Opus 4.6",
1248
+ maxTokens: 2e5,
1249
+ reasoning: true,
1250
+ input: ["text", "image"],
1251
+ cost: {
1252
+ input: .015,
1253
+ output: .075,
1254
+ cacheRead: .0015,
1255
+ cacheWrite: .01875
1256
+ },
1257
+ contextWindow: 2e5
1258
+ },
1259
+ {
1260
+ id: "anthropic/claude-sonnet-4.6",
1261
+ name: "Claude Sonnet 4.6",
1262
+ maxTokens: 2e5,
1263
+ reasoning: true,
1264
+ input: ["text", "image"],
1265
+ cost: {
1266
+ input: .003,
1267
+ output: .015,
1268
+ cacheRead: 3e-4,
1269
+ cacheWrite: .00375
1270
+ },
1271
+ contextWindow: 2e5
1272
+ },
1273
+ {
1274
+ id: "x-ai/grok-4.20-beta",
1275
+ name: "Grok 4.20 Beta",
1276
+ maxTokens: 131072,
1277
+ reasoning: true,
1278
+ input: ["text"],
1279
+ cost: {
1280
+ input: .003,
1281
+ output: .015,
1282
+ cacheRead: 0,
1283
+ cacheWrite: 0
1284
+ },
1285
+ contextWindow: 131072
1286
+ },
1287
+ {
1288
+ id: "minimax/minimax-m2.5",
1289
+ name: "MiniMax M2.5",
1290
+ maxTokens: 1e6,
1291
+ reasoning: false,
1292
+ input: ["text"],
1293
+ cost: {
1294
+ input: .001,
1295
+ output: .005,
1296
+ cacheRead: 0,
1297
+ cacheWrite: 0
1298
+ },
1299
+ contextWindow: 1e6
1300
+ },
1301
+ {
1302
+ id: "moonshotai/kimi-k2.5",
1303
+ name: "Kimi K2.5",
1304
+ maxTokens: 131072,
1305
+ reasoning: true,
1306
+ input: ["text"],
1307
+ cost: {
1308
+ input: .002,
1309
+ output: .008,
1310
+ cacheRead: 0,
1311
+ cacheWrite: 0
1312
+ },
1313
+ contextWindow: 131072
1314
+ },
1315
+ {
1316
+ id: "z-ai/glm-5",
1317
+ name: "GLM-5",
1318
+ maxTokens: 128e3,
1319
+ reasoning: false,
1320
+ input: ["text"],
1321
+ cost: {
1322
+ input: .001,
1323
+ output: .005,
1324
+ cacheRead: 0,
1325
+ cacheWrite: 0
1326
+ },
1327
+ contextWindow: 128e3
1328
+ }
1329
+ ];
1330
+ function resolveProviders(config) {
1331
+ const defaultProtocol = resolveProtocol(config.protocol);
1332
+ const defaultMppSessionBudget = resolveMppSessionBudget(config.mppSessionBudget);
1333
+ const raw = config.providers ?? {};
1334
+ const entries = Object.entries(raw).length > 0 ? Object.entries(raw).map(([id, provider]) => ({
1335
+ id,
1336
+ baseUrl: provider.baseUrl || "/x402-proxy/v1",
1337
+ upstreamUrl: provider.upstreamUrl || "https://surf.cascade.fyi/api/v1/inference",
1338
+ protocol: resolveProtocol(provider.protocol, defaultProtocol),
1339
+ mppSessionBudget: resolveMppSessionBudget(provider.mppSessionBudget, defaultMppSessionBudget),
1340
+ models: provider.models && provider.models.length > 0 ? provider.models : DEFAULT_SURF_MODELS
1341
+ })) : [{
1342
+ id: DEFAULT_SURF_PROVIDER_ID,
1343
+ baseUrl: DEFAULT_SURF_BASE_URL,
1344
+ upstreamUrl: DEFAULT_SURF_UPSTREAM_URL,
1345
+ protocol: defaultProtocol,
1346
+ mppSessionBudget: defaultMppSessionBudget,
1347
+ models: DEFAULT_SURF_MODELS
1348
+ }];
1349
+ return {
1350
+ providers: entries,
1351
+ models: entries.flatMap((provider) => provider.models.map((model) => ({
1352
+ ...model,
1353
+ provider: provider.id
1354
+ })))
1355
+ };
1356
+ }
1357
+ function routePrefixForBaseUrl(baseUrl) {
1358
+ const segments = baseUrl.split("/").filter(Boolean);
1359
+ return segments.length > 0 ? `/${segments[0]}` : "/";
1360
+ }
1361
+ function resolveProtocol(value, fallback = "mpp") {
1362
+ return value === "x402" || value === "mpp" || value === "auto" ? value : fallback;
1363
+ }
1364
+ function resolveMppSessionBudget(value, fallback = "0.5") {
1365
+ return typeof value === "string" && value.trim().length > 0 ? value : fallback;
1366
+ }
845
1367
  //#endregion
846
1368
  //#region src/openclaw/route.ts
847
- function createX402RouteHandler(opts) {
848
- const { upstreamOrigin, proxy, getWalletAddress, historyPath, allModels, logger } = opts;
1369
+ function createInferenceProxyRouteHandler(opts) {
1370
+ const { providers, getX402Proxy, getWalletAddress, getWalletAddressForNetwork, getEvmKey, historyPath, allModels, logger } = opts;
1371
+ const sortedProviders = providers.slice().sort((left, right) => right.baseUrl.length - left.baseUrl.length);
849
1372
  return async (req, res) => {
850
1373
  const url = new URL(req.url ?? "/", "http://localhost");
1374
+ const provider = sortedProviders.find((entry) => entry.baseUrl === "/" || url.pathname.startsWith(entry.baseUrl));
1375
+ if (!provider) {
1376
+ res.writeHead(404, { "Content-Type": "application/json" });
1377
+ res.end(JSON.stringify({ error: {
1378
+ message: "Unknown inference route",
1379
+ code: "not_found"
1380
+ } }));
1381
+ return true;
1382
+ }
851
1383
  const walletAddress = getWalletAddress();
852
1384
  if (!walletAddress) {
853
1385
  res.writeHead(503, { "Content-Type": "application/json" });
@@ -855,11 +1387,11 @@ function createX402RouteHandler(opts) {
855
1387
  message: "Wallet not loaded yet",
856
1388
  code: "not_ready"
857
1389
  } }));
858
- return;
1390
+ return true;
859
1391
  }
860
- const pathSuffix = url.pathname.slice(5);
861
- const upstreamUrl = upstreamOrigin + pathSuffix + url.search;
862
- logger.info(`x402: intercepting ${upstreamUrl.substring(0, 80)}`);
1392
+ const pathSuffix = provider.baseUrl === "/" ? url.pathname : url.pathname.slice(provider.baseUrl.length);
1393
+ const upstreamUrl = `${provider.upstreamUrl.replace(/\/+$/, "")}${pathSuffix.startsWith("/") ? pathSuffix : `/${pathSuffix}`}${url.search}`;
1394
+ logger.info(`proxy: intercepting ${upstreamUrl.substring(0, 80)}`);
863
1395
  const chunks = [];
864
1396
  for await (const chunk of req) chunks.push(Buffer.from(chunk));
865
1397
  let body = Buffer.concat(chunks).toString("utf-8");
@@ -891,22 +1423,62 @@ function createX402RouteHandler(opts) {
891
1423
  const method = req.method ?? "GET";
892
1424
  const startMs = Date.now();
893
1425
  try {
894
- const response = await proxy.x402Fetch(upstreamUrl, {
1426
+ const requestInit = {
895
1427
  method,
896
1428
  headers,
897
1429
  body: ["GET", "HEAD"].includes(method) ? void 0 : body
898
- });
1430
+ };
1431
+ const useMpp = provider.protocol === "mpp" || provider.protocol === "auto";
1432
+ const wantsStreaming = isChatCompletion && /"stream"\s*:\s*true/.test(body);
1433
+ if (useMpp) {
1434
+ const evmKey = getEvmKey();
1435
+ if (!evmKey) {
1436
+ res.writeHead(503, { "Content-Type": "application/json" });
1437
+ res.end(JSON.stringify({ error: {
1438
+ message: "MPP inference requires an EVM wallet. Configure X402_PROXY_WALLET_MNEMONIC or X402_PROXY_WALLET_EVM_KEY.",
1439
+ code: "mpp_wallet_missing"
1440
+ } }));
1441
+ return true;
1442
+ }
1443
+ return await handleMppRequest({
1444
+ req,
1445
+ res,
1446
+ upstreamUrl,
1447
+ requestInit,
1448
+ walletAddress,
1449
+ historyPath,
1450
+ logger,
1451
+ allModels,
1452
+ thinkingMode,
1453
+ wantsStreaming,
1454
+ startMs,
1455
+ evmKey,
1456
+ mppSessionBudget: provider.mppSessionBudget
1457
+ });
1458
+ }
1459
+ const proxy = getX402Proxy();
1460
+ if (!proxy) {
1461
+ res.writeHead(503, { "Content-Type": "application/json" });
1462
+ res.end(JSON.stringify({ error: {
1463
+ message: "x402 wallet not loaded yet. Configure a Solana wallet or switch the provider to mpp.",
1464
+ code: "x402_wallet_missing"
1465
+ } }));
1466
+ return true;
1467
+ }
1468
+ const response = await proxy.x402Fetch(upstreamUrl, requestInit);
899
1469
  if (response.status === 402) {
900
1470
  const responseBody = await response.text();
901
1471
  logger.error(`x402: payment failed, raw response: ${responseBody}`);
902
1472
  const payment = proxy.shiftPayment();
903
1473
  const amount = paymentAmount(payment);
1474
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
1475
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
904
1476
  appendHistory(historyPath, {
905
1477
  t: Date.now(),
906
1478
  ok: false,
907
1479
  kind: "x402_inference",
908
- net: SOL_MAINNET,
909
- from: walletAddress,
1480
+ net: paymentNetwork,
1481
+ from: paymentFrom,
910
1482
  to: payment?.payTo,
911
1483
  amount,
912
1484
  token: amount != null ? "USDC" : void 0,
@@ -923,19 +1495,21 @@ function createX402RouteHandler(opts) {
923
1495
  type: "x402_payment_error",
924
1496
  code: "payment_failed"
925
1497
  } }));
926
- return;
1498
+ return true;
927
1499
  }
928
1500
  if (!response.ok && isChatCompletion) {
929
1501
  const responseBody = await response.text();
930
1502
  logger.error(`x402: upstream error ${response.status}: ${responseBody.substring(0, 300)}`);
931
1503
  const payment = proxy.shiftPayment();
932
1504
  const amount = paymentAmount(payment);
1505
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
1506
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
933
1507
  appendHistory(historyPath, {
934
1508
  t: Date.now(),
935
1509
  ok: false,
936
1510
  kind: "x402_inference",
937
- net: SOL_MAINNET,
938
- from: walletAddress,
1511
+ net: paymentNetwork,
1512
+ from: paymentFrom,
939
1513
  to: payment?.payTo,
940
1514
  amount,
941
1515
  token: amount != null ? "USDC" : void 0,
@@ -948,12 +1522,14 @@ function createX402RouteHandler(opts) {
948
1522
  type: "x402_upstream_error",
949
1523
  code: "upstream_failed"
950
1524
  } }));
951
- return;
1525
+ return true;
952
1526
  }
953
1527
  logger.info(`x402: response ${response.status}`);
954
1528
  const txSig = extractTxSignature(response);
955
1529
  const payment = proxy.shiftPayment();
956
1530
  const amount = paymentAmount(payment);
1531
+ const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
1532
+ const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
957
1533
  const resHeaders = {};
958
1534
  for (const [key, val] of response.headers.entries()) resHeaders[key] = val;
959
1535
  res.writeHead(response.status, resHeaders);
@@ -963,20 +1539,18 @@ function createX402RouteHandler(opts) {
963
1539
  t: Date.now(),
964
1540
  ok: true,
965
1541
  kind: "x402_inference",
966
- net: SOL_MAINNET,
967
- from: walletAddress,
1542
+ net: paymentNetwork,
1543
+ from: paymentFrom,
968
1544
  to: payment?.payTo,
969
1545
  tx: txSig,
970
1546
  amount,
971
1547
  token: "USDC",
972
1548
  ms: Date.now() - startMs
973
1549
  });
974
- return;
1550
+ return true;
975
1551
  }
976
1552
  const ct = response.headers.get("content-type") || "";
977
- const isSSE = isChatCompletion && ct.includes("text/event-stream");
978
- let lastDataLine = "";
979
- let residual = "";
1553
+ const sse = isChatCompletion && ct.includes("text/event-stream") ? createSseTracker() : null;
980
1554
  const reader = response.body.getReader();
981
1555
  const decoder = new TextDecoder();
982
1556
  try {
@@ -984,27 +1558,23 @@ function createX402RouteHandler(opts) {
984
1558
  const { done, value } = await reader.read();
985
1559
  if (done) break;
986
1560
  res.write(value);
987
- if (isSSE) {
988
- const lines = (residual + decoder.decode(value, { stream: true })).split("\n");
989
- residual = lines.pop() ?? "";
990
- for (const line of lines) if (line.startsWith("data: ") && line !== "data: [DONE]") lastDataLine = line.slice(6);
991
- }
1561
+ sse?.push(decoder.decode(value, { stream: true }));
992
1562
  }
993
1563
  } finally {
994
1564
  reader.releaseLock();
995
1565
  }
996
1566
  res.end();
997
1567
  const durationMs = Date.now() - startMs;
998
- if (isSSE && lastDataLine) try {
999
- const parsed = JSON.parse(lastDataLine);
1568
+ if (sse?.lastData) try {
1569
+ const parsed = JSON.parse(sse.lastData);
1000
1570
  const usage = parsed.usage;
1001
1571
  const model = parsed.model ?? "";
1002
1572
  appendHistory(historyPath, {
1003
1573
  t: Date.now(),
1004
1574
  ok: true,
1005
1575
  kind: "x402_inference",
1006
- net: SOL_MAINNET,
1007
- from: walletAddress,
1576
+ net: paymentNetwork,
1577
+ from: paymentFrom,
1008
1578
  to: payment?.payTo,
1009
1579
  tx: txSig,
1010
1580
  amount,
@@ -1024,8 +1594,8 @@ function createX402RouteHandler(opts) {
1024
1594
  t: Date.now(),
1025
1595
  ok: true,
1026
1596
  kind: "x402_inference",
1027
- net: SOL_MAINNET,
1028
- from: walletAddress,
1597
+ net: paymentNetwork,
1598
+ from: paymentFrom,
1029
1599
  to: payment?.payTo,
1030
1600
  tx: txSig,
1031
1601
  amount,
@@ -1037,19 +1607,19 @@ function createX402RouteHandler(opts) {
1037
1607
  t: Date.now(),
1038
1608
  ok: true,
1039
1609
  kind: "x402_inference",
1040
- net: SOL_MAINNET,
1041
- from: walletAddress,
1610
+ net: paymentNetwork,
1611
+ from: paymentFrom,
1042
1612
  to: payment?.payTo,
1043
1613
  tx: txSig,
1044
1614
  amount,
1045
1615
  token: "USDC",
1046
1616
  ms: durationMs
1047
1617
  });
1048
- return;
1618
+ return true;
1049
1619
  } catch (err) {
1050
1620
  const msg = String(err);
1051
1621
  logger.error(`x402: fetch threw: ${msg}`);
1052
- proxy.shiftPayment();
1622
+ getX402Proxy()?.shiftPayment();
1053
1623
  appendHistory(historyPath, {
1054
1624
  t: Date.now(),
1055
1625
  ok: false,
@@ -1071,104 +1641,258 @@ function createX402RouteHandler(opts) {
1071
1641
  code: "payment_failed"
1072
1642
  } }));
1073
1643
  }
1644
+ return true;
1074
1645
  }
1075
1646
  };
1076
1647
  }
1077
- //#endregion
1078
- //#region src/openclaw/plugin.ts
1079
- function parseProviders(config) {
1080
- const raw = config.providers ?? {};
1081
- const models = [];
1082
- const upstreamOrigins = [];
1083
- for (const [name, prov] of Object.entries(raw)) {
1084
- if (prov.upstreamUrl) upstreamOrigins.push(prov.upstreamUrl);
1085
- for (const m of prov.models) models.push({
1086
- ...m,
1087
- provider: name
1088
- });
1089
- }
1648
+ function createSseTracker() {
1649
+ let residual = "";
1650
+ let lastDataLine = "";
1090
1651
  return {
1091
- models,
1092
- upstreamOrigins
1652
+ push(text) {
1653
+ const lines = (residual + text).split("\n");
1654
+ residual = lines.pop() ?? "";
1655
+ for (const line of lines) if (line.startsWith("data: ") && line !== "data: [DONE]") lastDataLine = line.slice(6);
1656
+ },
1657
+ get lastData() {
1658
+ return lastDataLine;
1659
+ }
1093
1660
  };
1094
1661
  }
1662
+ async function closeMppSession(handler) {
1663
+ try {
1664
+ await handler.close();
1665
+ } catch {}
1666
+ }
1667
+ async function handleMppRequest(opts) {
1668
+ const { req, res, upstreamUrl, requestInit, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, startMs, evmKey, mppSessionBudget } = opts;
1669
+ const mpp = await createMppProxyHandler({
1670
+ evmKey,
1671
+ maxDeposit: mppSessionBudget
1672
+ });
1673
+ try {
1674
+ if (wantsStreaming) {
1675
+ res.writeHead(200, {
1676
+ "Content-Type": "text/event-stream; charset=utf-8",
1677
+ "Cache-Control": "no-cache, no-transform",
1678
+ Connection: "keep-alive"
1679
+ });
1680
+ const sse = createSseTracker();
1681
+ const stream = await mpp.sse(upstreamUrl, requestInit);
1682
+ for await (const chunk of stream) {
1683
+ const text = String(chunk);
1684
+ res.write(text);
1685
+ sse.push(text);
1686
+ }
1687
+ res.end();
1688
+ mpp.shiftPayment();
1689
+ const payment = mpp.shiftPayment();
1690
+ appendInferenceHistory({
1691
+ historyPath,
1692
+ allModels,
1693
+ walletAddress,
1694
+ paymentNetwork: payment?.network ?? "eip155:4217",
1695
+ paymentTo: void 0,
1696
+ tx: payment?.receipt?.reference ?? payment?.channelId,
1697
+ amount: parseMppAmount(payment?.amount),
1698
+ thinkingMode,
1699
+ lastDataLine: sse.lastData,
1700
+ durationMs: Date.now() - startMs
1701
+ });
1702
+ return true;
1703
+ }
1704
+ const response = await mpp.fetch(upstreamUrl, requestInit);
1705
+ const tx = extractTxSignature(response);
1706
+ if (response.status === 402) {
1707
+ const responseBody = await response.text();
1708
+ logger.error(`mpp: payment failed, raw response: ${responseBody}`);
1709
+ appendHistory(historyPath, {
1710
+ t: Date.now(),
1711
+ ok: false,
1712
+ kind: "x402_inference",
1713
+ net: TEMPO_NETWORK,
1714
+ from: walletAddress,
1715
+ ms: Date.now() - startMs,
1716
+ error: "payment_required"
1717
+ });
1718
+ res.writeHead(402, { "Content-Type": "application/json" });
1719
+ res.end(JSON.stringify({ error: {
1720
+ message: `MPP payment failed: ${responseBody.substring(0, 200) || "unknown error"}. Wallet: ${walletAddress}`,
1721
+ type: "mpp_payment_error",
1722
+ code: "payment_failed"
1723
+ } }));
1724
+ return true;
1725
+ }
1726
+ const resHeaders = {};
1727
+ for (const [key, value] of response.headers.entries()) resHeaders[key] = value;
1728
+ res.writeHead(response.status, resHeaders);
1729
+ const responseBody = await response.text();
1730
+ res.end(responseBody);
1731
+ const payment = mpp.shiftPayment();
1732
+ appendInferenceHistory({
1733
+ historyPath,
1734
+ allModels,
1735
+ walletAddress,
1736
+ paymentNetwork: payment?.network ?? "eip155:4217",
1737
+ paymentTo: void 0,
1738
+ tx: tx ?? payment?.receipt?.reference,
1739
+ amount: parseMppAmount(payment?.amount),
1740
+ thinkingMode,
1741
+ lastDataLine: responseBody,
1742
+ durationMs: Date.now() - startMs
1743
+ });
1744
+ return true;
1745
+ } catch (err) {
1746
+ logger.error(`mpp: fetch threw: ${String(err)}`);
1747
+ appendHistory(historyPath, {
1748
+ t: Date.now(),
1749
+ ok: false,
1750
+ kind: "x402_inference",
1751
+ net: TEMPO_NETWORK,
1752
+ from: walletAddress,
1753
+ ms: Date.now() - startMs,
1754
+ error: String(err).substring(0, 200)
1755
+ });
1756
+ if (!res.headersSent) {
1757
+ res.writeHead(402, { "Content-Type": "application/json" });
1758
+ res.end(JSON.stringify({ error: {
1759
+ message: `MPP request failed: ${String(err)}`,
1760
+ type: "mpp_payment_error",
1761
+ code: "payment_failed"
1762
+ } }));
1763
+ }
1764
+ return true;
1765
+ } finally {
1766
+ await closeMppSession(mpp);
1767
+ if (!res.writableEnded) res.end();
1768
+ req.resume();
1769
+ }
1770
+ }
1771
+ function appendInferenceHistory(opts) {
1772
+ const { historyPath, allModels, walletAddress, paymentNetwork, paymentTo, tx, amount, thinkingMode, lastDataLine, durationMs } = opts;
1773
+ try {
1774
+ const parsed = JSON.parse(lastDataLine);
1775
+ const usage = parsed.usage;
1776
+ const model = parsed.model ?? "";
1777
+ appendHistory(historyPath, {
1778
+ t: Date.now(),
1779
+ ok: true,
1780
+ kind: "x402_inference",
1781
+ net: paymentNetwork,
1782
+ from: walletAddress,
1783
+ to: paymentTo,
1784
+ tx,
1785
+ amount,
1786
+ token: "USDC",
1787
+ provider: allModels.find((entry) => entry.id === model || `${entry.provider}/${entry.id}` === model)?.provider,
1788
+ model,
1789
+ inputTokens: usage?.prompt_tokens ?? 0,
1790
+ outputTokens: usage?.completion_tokens ?? 0,
1791
+ reasoningTokens: usage?.completion_tokens_details?.reasoning_tokens,
1792
+ cacheRead: usage?.prompt_tokens_details?.cached_tokens,
1793
+ cacheWrite: usage?.prompt_tokens_details?.cache_creation_input_tokens,
1794
+ thinking: thinkingMode,
1795
+ ms: durationMs
1796
+ });
1797
+ } catch {
1798
+ appendHistory(historyPath, {
1799
+ t: Date.now(),
1800
+ ok: true,
1801
+ kind: "x402_inference",
1802
+ net: paymentNetwork,
1803
+ from: walletAddress,
1804
+ to: paymentTo,
1805
+ tx,
1806
+ amount,
1807
+ token: "USDC",
1808
+ ms: durationMs
1809
+ });
1810
+ }
1811
+ }
1812
+ //#endregion
1813
+ //#region src/openclaw/plugin.ts
1095
1814
  function register(api) {
1096
1815
  const config = api.pluginConfig ?? {};
1097
1816
  const explicitKeypairPath = config.keypairPath;
1098
1817
  const rpcUrl = config.rpcUrl || "https://api.mainnet-beta.solana.com";
1099
1818
  const dashboardUrl = config.dashboardUrl || "";
1100
- const { models: allModels, upstreamOrigins } = parseProviders(config);
1101
- if (allModels.length === 0) {
1102
- api.logger.error("x402-proxy: no providers configured");
1103
- return;
1104
- }
1105
- const raw = config.providers ?? {};
1106
- for (const [name, prov] of Object.entries(raw)) api.registerProvider({
1107
- id: name,
1108
- label: `${name} (x402)`,
1819
+ const { providers, models: allModels } = resolveProviders(config);
1820
+ const defaultProvider = providers[0];
1821
+ for (const provider of providers) api.registerProvider({
1822
+ id: provider.id,
1823
+ label: provider.id,
1109
1824
  auth: [],
1110
1825
  catalog: {
1111
1826
  order: "simple",
1112
1827
  run: async () => ({ provider: {
1113
- baseUrl: prov.baseUrl,
1828
+ baseUrl: provider.baseUrl,
1114
1829
  api: "openai-completions",
1115
1830
  authHeader: false,
1116
- models: prov.models
1831
+ models: provider.models
1117
1832
  } })
1118
1833
  }
1119
1834
  });
1120
- api.logger.info(`x402-proxy: ${Object.keys(raw).join(", ")} - ${allModels.length} models, ${upstreamOrigins.length} x402 endpoints`);
1121
- let walletAddress = null;
1835
+ api.logger.info(`x402-proxy: ${providers.map((provider) => `${provider.id}:${provider.protocol}`).join(", ")} - ${allModels.length} models`);
1836
+ let solanaWalletAddress = null;
1837
+ let evmWalletAddress = null;
1122
1838
  let signerRef = null;
1123
1839
  let proxyRef = null;
1124
- let walletLoading = false;
1840
+ let evmKeyRef = null;
1841
+ let walletLoadPromise = null;
1125
1842
  const historyPath = getHistoryPath();
1843
+ const routePrefixes = [...new Set(providers.map((provider) => routePrefixForBaseUrl(provider.baseUrl)))];
1844
+ const handler = createInferenceProxyRouteHandler({
1845
+ providers,
1846
+ getX402Proxy: () => proxyRef,
1847
+ getWalletAddress: () => solanaWalletAddress ?? evmWalletAddress,
1848
+ getWalletAddressForNetwork: (network) => addressForNetwork(evmWalletAddress, solanaWalletAddress, network),
1849
+ getEvmKey: () => evmKeyRef,
1850
+ historyPath,
1851
+ allModels,
1852
+ logger: api.logger
1853
+ });
1854
+ for (const path of routePrefixes) api.registerHttpRoute({
1855
+ path,
1856
+ match: "prefix",
1857
+ auth: "plugin",
1858
+ handler
1859
+ });
1860
+ api.logger.info(`proxy: HTTP routes ${routePrefixes.join(", ")} registered for ${providers.map((provider) => provider.upstreamUrl).join(", ")}`);
1126
1861
  async function ensureWalletLoaded() {
1127
- if (walletLoading || walletAddress) return;
1128
- walletLoading = true;
1129
- let signer;
1130
- try {
1131
- if (explicitKeypairPath) {
1132
- signer = await loadSvmWallet(explicitKeypairPath.startsWith("~/") ? join(homedir(), explicitKeypairPath.slice(2)) : explicitKeypairPath);
1133
- walletAddress = signer.address;
1134
- } else {
1862
+ if (walletLoadPromise) {
1863
+ await walletLoadPromise;
1864
+ return;
1865
+ }
1866
+ walletLoadPromise = (async () => {
1867
+ try {
1135
1868
  const resolution = resolveWallet();
1136
- if (resolution.source === "none" || !resolution.solanaKey || !resolution.solanaAddress) {
1137
- walletLoading = false;
1138
- api.logger.error("x402-proxy: no wallet found. Run `x402-proxy setup` to create one, or set X402_PROXY_WALLET_MNEMONIC env var.");
1869
+ evmKeyRef = resolution.evmKey ?? null;
1870
+ evmWalletAddress = resolution.evmAddress ?? null;
1871
+ if (explicitKeypairPath) {
1872
+ signerRef = await loadSvmWallet(explicitKeypairPath.startsWith("~/") ? join(homedir(), explicitKeypairPath.slice(2)) : explicitKeypairPath);
1873
+ solanaWalletAddress = signerRef.address;
1874
+ } else if (resolution.solanaKey && resolution.solanaAddress) {
1875
+ signerRef = await createKeyPairSignerFromBytes(resolution.solanaKey);
1876
+ solanaWalletAddress = resolution.solanaAddress;
1877
+ } else {
1878
+ signerRef = null;
1879
+ solanaWalletAddress = null;
1880
+ }
1881
+ if (!solanaWalletAddress && !evmWalletAddress) {
1882
+ api.logger.error("x402-proxy: no wallet found. Run `x402-proxy setup` to create one, or set X402_PROXY_WALLET_MNEMONIC / X402_PROXY_WALLET_EVM_KEY.");
1139
1883
  return;
1140
1884
  }
1141
- signer = await createKeyPairSignerFromBytes(resolution.solanaKey);
1142
- walletAddress = resolution.solanaAddress;
1885
+ if (signerRef) {
1886
+ const client = new x402Client();
1887
+ client.register(SOL_MAINNET, new OptimizedSvmScheme(signerRef, { rpcUrl }));
1888
+ proxyRef = createX402ProxyHandler({ client });
1889
+ } else proxyRef = null;
1890
+ api.logger.info(`wallets: solana=${solanaWalletAddress ?? "missing"} evm=${evmWalletAddress ?? "missing"}`);
1891
+ } catch (err) {
1892
+ api.logger.error(`wallet load failed: ${err}`);
1143
1893
  }
1144
- signerRef = signer;
1145
- api.logger.info(`x402: wallet ${walletAddress}`);
1146
- } catch (err) {
1147
- walletLoading = false;
1148
- api.logger.error(`x402: failed to load wallet: ${err}`);
1149
- return;
1150
- }
1151
- const client = new x402Client();
1152
- client.register(SOL_MAINNET, new OptimizedSvmScheme(signer, { rpcUrl }));
1153
- proxyRef = createX402ProxyHandler({ client });
1154
- const upstreamOrigin = upstreamOrigins[0];
1155
- if (upstreamOrigin) {
1156
- const handler = createX402RouteHandler({
1157
- upstreamOrigin,
1158
- proxy: proxyRef,
1159
- getWalletAddress: () => walletAddress,
1160
- historyPath,
1161
- allModels,
1162
- logger: api.logger
1163
- });
1164
- api.registerHttpRoute({
1165
- path: "/x402",
1166
- match: "prefix",
1167
- auth: "plugin",
1168
- handler
1169
- });
1170
- api.logger.info(`x402: HTTP route registered for ${upstreamOrigin}`);
1171
- }
1894
+ })();
1895
+ await walletLoadPromise;
1172
1896
  }
1173
1897
  ensureWalletLoaded();
1174
1898
  api.registerService({
@@ -1179,27 +1903,40 @@ function register(api) {
1179
1903
  async stop() {}
1180
1904
  });
1181
1905
  const toolCtx = {
1182
- getWalletAddress: () => walletAddress,
1906
+ ensureReady: ensureWalletLoaded,
1907
+ getSolanaWalletAddress: () => solanaWalletAddress,
1908
+ getEvmWalletAddress: () => evmWalletAddress,
1183
1909
  getSigner: () => signerRef,
1910
+ getX402Proxy: () => proxyRef,
1911
+ getEvmKey: () => evmKeyRef,
1912
+ getDefaultRequestProtocol: () => defaultProvider?.protocol ?? "mpp",
1913
+ getDefaultMppSessionBudget: () => defaultProvider?.mppSessionBudget ?? "0.5",
1184
1914
  rpcUrl,
1185
1915
  historyPath,
1186
- get proxy() {
1187
- if (!proxyRef) throw new Error("x402 proxy not initialized yet");
1188
- return proxyRef;
1189
- },
1190
1916
  allModels
1191
1917
  };
1192
- api.registerTool(createBalanceTool(toolCtx));
1193
- api.registerTool(createPaymentTool(toolCtx));
1918
+ api.registerTool(createWalletTool(toolCtx), { names: ["x_balance"] });
1919
+ api.registerTool(createRequestTool(toolCtx), { names: ["x_payment"] });
1194
1920
  const cmdCtx = {
1195
- getWalletAddress: () => walletAddress,
1921
+ ensureReady: ensureWalletLoaded,
1922
+ getSolanaWalletAddress: () => solanaWalletAddress,
1923
+ getEvmWalletAddress: () => evmWalletAddress,
1196
1924
  getSigner: () => signerRef,
1925
+ getDefaultRequestProtocol: () => defaultProvider?.protocol ?? "mpp",
1926
+ getDefaultMppSessionBudget: () => defaultProvider?.mppSessionBudget ?? "0.5",
1197
1927
  rpcUrl,
1198
1928
  dashboardUrl,
1199
1929
  historyPath,
1200
1930
  allModels
1201
1931
  };
1202
1932
  api.registerCommand(createWalletCommand(cmdCtx));
1933
+ api.registerCommand(createSendCommand(cmdCtx));
1203
1934
  }
1935
+ var plugin_default = definePluginEntry({
1936
+ id: "x402-proxy",
1937
+ name: "mpp/x402 Payments Proxy",
1938
+ description: "x402 and MPP payments, wallet tools, and paid inference proxying",
1939
+ register
1940
+ });
1204
1941
  //#endregion
1205
- export { register };
1942
+ export { plugin_default as default, register };