x402-proxy-openclaw 0.10.9
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 +453 -0
- package/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/commands/wallet.js +149 -0
- package/dist/commands.js +218 -0
- package/dist/defaults.js +129 -0
- package/dist/handler.js +319 -0
- package/dist/history.js +124 -0
- package/dist/lib/config.js +47 -0
- package/dist/lib/debug-log.js +29 -0
- package/dist/lib/derive.js +77 -0
- package/dist/lib/env.js +9 -0
- package/dist/lib/optimized-svm-scheme.js +86 -0
- package/dist/lib/output.js +13 -0
- package/dist/lib/wallet-resolution.js +76 -0
- package/dist/openclaw/plugin.d.ts +15 -0
- package/dist/openclaw/plugin.js +156 -0
- package/dist/openclaw.plugin.json +112 -0
- package/dist/route.js +671 -0
- package/dist/solana.js +93 -0
- package/dist/tools.js +269 -0
- package/dist/wallet.js +15 -0
- package/openclaw.plugin.json +112 -0
- package/package.json +93 -0
- package/skills/SKILL.md +183 -0
- package/skills/references/library.md +85 -0
- package/skills/references/openclaw-plugin.md +145 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# x402-proxy-openclaw
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin for x402 and MPP payments, wallet tools, and paid inference proxying.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install x402-proxy-openclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What It Adds
|
|
12
|
+
|
|
13
|
+
- `x_wallet` and `x_request` tools, with `x_balance` and `x_payment` aliases
|
|
14
|
+
- `/x_wallet` and `/x_send` commands
|
|
15
|
+
- an HTTP route proxy for upstream inference endpoints
|
|
16
|
+
- a built-in `surf` provider at `/x402-proxy/v1`
|
|
17
|
+
|
|
18
|
+
By default, the plugin proxies `https://surf.cascade.fyi/api/v1/inference` and prefers MPP.
|
|
19
|
+
|
|
20
|
+
## Relationship To The CLI Package
|
|
21
|
+
|
|
22
|
+
The standalone CLI and library remain published as `x402-proxy`.
|
|
23
|
+
|
|
24
|
+
Use `x402-proxy` when you want:
|
|
25
|
+
- paid HTTP requests from the command line
|
|
26
|
+
- the MCP proxy
|
|
27
|
+
- local wallet setup and export flows
|
|
28
|
+
|
|
29
|
+
Use `x402-proxy-openclaw` when you want:
|
|
30
|
+
- OpenClaw gateway integration
|
|
31
|
+
- plugin-managed tools, commands, and routes
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
Apache-2.0
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getHistoryPath } from "../lib/config.js";
|
|
2
|
+
import { resolveWallet } from "../lib/wallet-resolution.js";
|
|
3
|
+
import { calcSpend, formatAmount, formatTxLine, formatUsdcValue, readHistory } from "../history.js";
|
|
4
|
+
import { dim, info } from "../lib/output.js";
|
|
5
|
+
import { address, getAddressEncoder, getProgramDerivedAddress } from "@solana/kit";
|
|
6
|
+
import { buildCommand } from "@stricli/core";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
//#region packages/x402-proxy/src/commands/wallet.ts
|
|
9
|
+
const BASE_RPC = "https://mainnet.base.org";
|
|
10
|
+
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
|
11
|
+
const TEMPO_RPC = "https://rpc.presto.tempo.xyz";
|
|
12
|
+
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
13
|
+
const USDC_TEMPO = "0x20C000000000000000000000b9537d11c60E8b50";
|
|
14
|
+
const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
15
|
+
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
16
|
+
const ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
17
|
+
async function rpcCall(url, method, params) {
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
method,
|
|
24
|
+
params,
|
|
25
|
+
id: 1
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) throw new Error(`RPC ${method} failed: ${res.status} ${res.statusText}`);
|
|
29
|
+
return res.json();
|
|
30
|
+
}
|
|
31
|
+
async function fetchEvmBalances(address) {
|
|
32
|
+
const usdcData = `0x70a08231${address.slice(2).padStart(64, "0")}`;
|
|
33
|
+
const [ethRes, usdcRes] = await Promise.all([rpcCall(BASE_RPC, "eth_getBalance", [address, "latest"]), rpcCall(BASE_RPC, "eth_call", [{
|
|
34
|
+
to: USDC_BASE,
|
|
35
|
+
data: usdcData
|
|
36
|
+
}, "latest"])]);
|
|
37
|
+
return {
|
|
38
|
+
eth: ethRes.result ? (Number(BigInt(ethRes.result)) / 0xde0b6b3a7640000).toFixed(6) : "?",
|
|
39
|
+
usdc: usdcRes.result ? formatUsdcValue(Number(BigInt(usdcRes.result)) / 1e6) : "?"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function fetchTempoBalances(address) {
|
|
43
|
+
const res = await rpcCall(TEMPO_RPC, "eth_call", [{
|
|
44
|
+
to: USDC_TEMPO,
|
|
45
|
+
data: `0x70a08231${address.slice(2).padStart(64, "0")}`
|
|
46
|
+
}, "latest"]);
|
|
47
|
+
return { usdc: res.result ? formatUsdcValue(Number(BigInt(res.result)) / 1e6) : "?" };
|
|
48
|
+
}
|
|
49
|
+
async function getUsdcAta(owner) {
|
|
50
|
+
const encoder = getAddressEncoder();
|
|
51
|
+
const [ata] = await getProgramDerivedAddress({
|
|
52
|
+
programAddress: address(ATA_PROGRAM),
|
|
53
|
+
seeds: [
|
|
54
|
+
encoder.encode(address(owner)),
|
|
55
|
+
encoder.encode(address(TOKEN_PROGRAM)),
|
|
56
|
+
encoder.encode(address(USDC_SOLANA_MINT))
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
return ata;
|
|
60
|
+
}
|
|
61
|
+
async function fetchSolanaBalances(ownerAddress) {
|
|
62
|
+
const ata = await getUsdcAta(ownerAddress);
|
|
63
|
+
const [solRes, usdcRes] = await Promise.all([rpcCall(SOLANA_RPC, "getBalance", [ownerAddress]), rpcCall(SOLANA_RPC, "getTokenAccountBalance", [ata])]);
|
|
64
|
+
const sol = solRes.result?.value != null ? (solRes.result.value / 1e9).toFixed(6) : "?";
|
|
65
|
+
const usdcVal = usdcRes.result?.value;
|
|
66
|
+
return {
|
|
67
|
+
sol,
|
|
68
|
+
usdc: usdcVal ? formatUsdcValue(Number(usdcVal.uiAmountString)) : "0"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function balanceLine(usdc, native, nativeSymbol) {
|
|
72
|
+
return pc.dim(` (${usdc} USDC, ${native} ${nativeSymbol})`);
|
|
73
|
+
}
|
|
74
|
+
async function fetchAllBalances(evmAddress, solanaAddress) {
|
|
75
|
+
const [evmResult, solResult, tempoResult] = await Promise.allSettled([
|
|
76
|
+
evmAddress ? fetchEvmBalances(evmAddress) : Promise.resolve(null),
|
|
77
|
+
solanaAddress ? fetchSolanaBalances(solanaAddress) : Promise.resolve(null),
|
|
78
|
+
evmAddress ? fetchTempoBalances(evmAddress) : Promise.resolve(null)
|
|
79
|
+
]);
|
|
80
|
+
return {
|
|
81
|
+
evm: evmResult.status === "fulfilled" ? evmResult.value : null,
|
|
82
|
+
sol: solResult.status === "fulfilled" ? solResult.value : null,
|
|
83
|
+
tempo: tempoResult.status === "fulfilled" ? tempoResult.value : null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
buildCommand({
|
|
87
|
+
docs: { brief: "Show wallet addresses and balances" },
|
|
88
|
+
parameters: {
|
|
89
|
+
flags: { verbose: {
|
|
90
|
+
kind: "boolean",
|
|
91
|
+
brief: "Show transaction IDs",
|
|
92
|
+
default: false
|
|
93
|
+
} },
|
|
94
|
+
positional: {
|
|
95
|
+
kind: "tuple",
|
|
96
|
+
parameters: []
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async func(flags) {
|
|
100
|
+
const wallet = resolveWallet();
|
|
101
|
+
if (wallet.source === "none") {
|
|
102
|
+
console.log(pc.yellow("No wallet configured."));
|
|
103
|
+
console.log(pc.dim(`\nRun:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
info("Wallet");
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(pc.dim(` Source: ${wallet.source}`));
|
|
110
|
+
const { evm, sol, tempo } = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
|
|
111
|
+
if (wallet.evmAddress) {
|
|
112
|
+
const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
|
|
113
|
+
console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
114
|
+
}
|
|
115
|
+
if (wallet.evmAddress) {
|
|
116
|
+
const bal = tempo ? pc.dim(` (${tempo.usdc} USDC)`) : pc.dim(" (network error)");
|
|
117
|
+
console.log(` Tempo: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
118
|
+
}
|
|
119
|
+
if (wallet.solanaAddress) {
|
|
120
|
+
const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
|
|
121
|
+
console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
|
|
122
|
+
}
|
|
123
|
+
const evmEmpty = !evm || Number(evm.usdc) === 0;
|
|
124
|
+
const solEmpty = !sol || Number(sol.usdc) === 0;
|
|
125
|
+
const tempoEmpty = !tempo || Number(tempo.usdc) === 0;
|
|
126
|
+
if (evmEmpty && solEmpty && tempoEmpty) {
|
|
127
|
+
console.log();
|
|
128
|
+
dim(" Send USDC to any address above to start using paid APIs.");
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
const records = readHistory(getHistoryPath());
|
|
132
|
+
if (records.length > 0) {
|
|
133
|
+
const spend = calcSpend(records);
|
|
134
|
+
const recent = records.slice(-10);
|
|
135
|
+
dim(" Recent transactions:");
|
|
136
|
+
for (const r of recent) {
|
|
137
|
+
const line = formatTxLine(r, { verbose: flags.verbose }).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
138
|
+
console.log(line);
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(pc.dim(` Today: ${formatAmount(spend.today, "USDC")} | Total: ${formatAmount(spend.total, "USDC")} | ${spend.count} tx`));
|
|
142
|
+
} else dim(" No transactions yet.");
|
|
143
|
+
console.log();
|
|
144
|
+
console.log(pc.dim(" See also: wallet history, wallet export-key"));
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
//#endregion
|
|
149
|
+
export { fetchAllBalances };
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { appendHistory, formatTxLine, readHistory } from "./history.js";
|
|
2
|
+
import { checkAtaExists, getUsdcBalance, transferUsdc } from "./solana.js";
|
|
3
|
+
import { SOL_MAINNET, getWalletSnapshot } from "./tools.js";
|
|
4
|
+
//#region packages/x402-proxy/src/openclaw/commands.ts
|
|
5
|
+
const HISTORY_PAGE_SIZE = 5;
|
|
6
|
+
const STATUS_HISTORY_COUNT = 3;
|
|
7
|
+
const INLINE_HISTORY_TOKEN_THRESHOLD = 3;
|
|
8
|
+
const SEND_CONFIRM_TTL_MS = 300 * 1e3;
|
|
9
|
+
const TOKEN_SYMBOL_CACHE_MAX = 200;
|
|
10
|
+
const tokenSymbolCache = /* @__PURE__ */ new Map();
|
|
11
|
+
const pendingSends = /* @__PURE__ */ new Map();
|
|
12
|
+
async function resolveTokenSymbols(mints) {
|
|
13
|
+
const result = /* @__PURE__ */ new Map();
|
|
14
|
+
const toResolve = [];
|
|
15
|
+
for (const m of mints) {
|
|
16
|
+
const cached = tokenSymbolCache.get(m);
|
|
17
|
+
if (cached) result.set(m, cached);
|
|
18
|
+
else toResolve.push(m);
|
|
19
|
+
}
|
|
20
|
+
if (toResolve.length === 0) return result;
|
|
21
|
+
try {
|
|
22
|
+
const res = await globalThis.fetch(`https://api.dexscreener.com/tokens/v1/solana/${toResolve.join(",")}`, { signal: AbortSignal.timeout(5e3) });
|
|
23
|
+
if (!res.ok) return result;
|
|
24
|
+
const pairs = await res.json();
|
|
25
|
+
for (const pair of pairs) {
|
|
26
|
+
const addr = pair.baseToken?.address;
|
|
27
|
+
const sym = pair.baseToken?.symbol;
|
|
28
|
+
if (addr && sym && toResolve.includes(addr)) {
|
|
29
|
+
if (tokenSymbolCache.size >= TOKEN_SYMBOL_CACHE_MAX) {
|
|
30
|
+
const oldest = tokenSymbolCache.keys().next();
|
|
31
|
+
if (!oldest.done) tokenSymbolCache.delete(oldest.value);
|
|
32
|
+
}
|
|
33
|
+
tokenSymbolCache.set(addr, sym);
|
|
34
|
+
result.set(addr, sym);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function senderKey(cmdCtx) {
|
|
41
|
+
return [
|
|
42
|
+
cmdCtx.channel,
|
|
43
|
+
cmdCtx.accountId ?? "",
|
|
44
|
+
cmdCtx.senderId ?? cmdCtx.from ?? "",
|
|
45
|
+
String(cmdCtx.messageThreadId ?? "")
|
|
46
|
+
].join(":");
|
|
47
|
+
}
|
|
48
|
+
async function executeSend(amountStr, destination, wallet, signer, rpc, histPath) {
|
|
49
|
+
if (!signer) return { text: "Wallet not loaded yet. Please wait for the gateway to finish starting." };
|
|
50
|
+
try {
|
|
51
|
+
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." };
|
|
52
|
+
let amountRaw;
|
|
53
|
+
let amountUi;
|
|
54
|
+
if (amountStr.toLowerCase() === "all") {
|
|
55
|
+
const balance = await getUsdcBalance(rpc, wallet);
|
|
56
|
+
if (balance.raw === 0n) return { text: "Wallet has no USDC to send." };
|
|
57
|
+
amountRaw = balance.raw;
|
|
58
|
+
amountUi = balance.ui;
|
|
59
|
+
} else {
|
|
60
|
+
const amount = Number.parseFloat(amountStr);
|
|
61
|
+
if (Number.isNaN(amount) || amount <= 0) return { text: `Invalid amount: ${amountStr}` };
|
|
62
|
+
amountRaw = BigInt(Math.round(amount * 1e6));
|
|
63
|
+
amountUi = amount.toString();
|
|
64
|
+
}
|
|
65
|
+
const sig = await transferUsdc(signer, rpc, destination, amountRaw);
|
|
66
|
+
appendHistory(histPath, {
|
|
67
|
+
t: Date.now(),
|
|
68
|
+
ok: true,
|
|
69
|
+
kind: "transfer",
|
|
70
|
+
net: SOL_MAINNET,
|
|
71
|
+
from: wallet,
|
|
72
|
+
to: destination,
|
|
73
|
+
tx: sig,
|
|
74
|
+
amount: Number.parseFloat(amountUi),
|
|
75
|
+
token: "USDC",
|
|
76
|
+
label: `${destination.slice(0, 4)}...${destination.slice(-4)}`
|
|
77
|
+
});
|
|
78
|
+
return { text: `Sent ${amountUi} USDC to \`${destination}\`\n[View transaction](https://solscan.io/tx/${sig})` };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
appendHistory(histPath, {
|
|
81
|
+
t: Date.now(),
|
|
82
|
+
ok: false,
|
|
83
|
+
kind: "transfer",
|
|
84
|
+
net: SOL_MAINNET,
|
|
85
|
+
from: wallet,
|
|
86
|
+
to: destination,
|
|
87
|
+
token: "USDC",
|
|
88
|
+
error: String(err).substring(0, 200)
|
|
89
|
+
});
|
|
90
|
+
const msg = String(err);
|
|
91
|
+
const detail = err.cause?.message || msg;
|
|
92
|
+
if (detail.includes("insufficient") || detail.includes("lamports")) return { text: "Send failed - insufficient SOL for fees\nBalance too low for transaction fees. Fund wallet with SOL." };
|
|
93
|
+
return { text: `Send failed: ${detail}` };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function handleHistory(histPath, page) {
|
|
97
|
+
const records = readHistory(histPath);
|
|
98
|
+
const totalTxs = records.length;
|
|
99
|
+
const start = (page - 1) * HISTORY_PAGE_SIZE;
|
|
100
|
+
const fromEnd = totalTxs - start;
|
|
101
|
+
const pageRecords = records.slice(Math.max(0, fromEnd - HISTORY_PAGE_SIZE), fromEnd).reverse();
|
|
102
|
+
if (pageRecords.length === 0) return { text: page === 1 ? "No transactions yet." : "No more transactions." };
|
|
103
|
+
const lines = [`**History** (${start + 1}-${start + pageRecords.length})`, ""];
|
|
104
|
+
for (const r of pageRecords) lines.push(formatTxLine(r));
|
|
105
|
+
const nav = [];
|
|
106
|
+
if (page > 1) nav.push(`Newer: \`/x_wallet history${page === 2 ? "" : ` ${page - 1}`}\``);
|
|
107
|
+
if (start + HISTORY_PAGE_SIZE < totalTxs) nav.push(`Older: \`/x_wallet history ${page + 1}\``);
|
|
108
|
+
if (nav.length > 0) lines.push("", nav.join(" · "));
|
|
109
|
+
return { text: lines.join("\n") };
|
|
110
|
+
}
|
|
111
|
+
function createWalletCommand(ctx) {
|
|
112
|
+
return {
|
|
113
|
+
name: "x_wallet",
|
|
114
|
+
description: "Wallet status, balances, payment readiness, and transaction history",
|
|
115
|
+
acceptsArgs: true,
|
|
116
|
+
requireAuth: true,
|
|
117
|
+
handler: async (cmdCtx) => {
|
|
118
|
+
await ctx.ensureReady();
|
|
119
|
+
const solanaWallet = ctx.getSolanaWalletAddress();
|
|
120
|
+
const evmWallet = ctx.getEvmWalletAddress();
|
|
121
|
+
if (!solanaWallet && !evmWallet) return { text: "Wallet not configured yet.\nRun `x402-proxy setup` or set `X402_PROXY_WALLET_MNEMONIC` on the gateway host." };
|
|
122
|
+
const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
|
|
123
|
+
if (parts[0]?.toLowerCase() === "history") {
|
|
124
|
+
const pageArg = parts[1];
|
|
125
|
+
const page = pageArg ? Math.max(1, Number.parseInt(pageArg, 10) || 1) : 1;
|
|
126
|
+
return handleHistory(ctx.historyPath, page);
|
|
127
|
+
}
|
|
128
|
+
if (parts[0]?.toLowerCase() === "send") return { text: "Use `/x_send <amount|all> <address>` for transfers." };
|
|
129
|
+
try {
|
|
130
|
+
const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
|
|
131
|
+
const lines = [`x402-proxy v0.10.9`];
|
|
132
|
+
const defaultModel = ctx.allModels[0];
|
|
133
|
+
if (defaultModel) lines.push("", `**Model** - ${defaultModel.name} (${defaultModel.provider})`);
|
|
134
|
+
lines.push("", `**Protocol** - ${ctx.getDefaultRequestProtocol()}`);
|
|
135
|
+
lines.push(`MPP session budget: ${ctx.getDefaultMppSessionBudget()} USDC`);
|
|
136
|
+
lines.push(`MPP ready: ${evmWallet ? "yes" : "no"}`);
|
|
137
|
+
lines.push(`x402 ready: ${solanaWallet ? "yes" : "no"}`);
|
|
138
|
+
if (evmWallet) {
|
|
139
|
+
lines.push("", "**EVM / Tempo**");
|
|
140
|
+
lines.push(`\`${evmWallet}\``);
|
|
141
|
+
lines.push(` Base: ${snap.balances.evm?.usdc ?? "?"} USDC`);
|
|
142
|
+
lines.push(` Tempo: ${snap.balances.tempo?.usdc ?? "?"} USDC`);
|
|
143
|
+
}
|
|
144
|
+
if (solanaWallet) {
|
|
145
|
+
const solscanUrl = `https://solscan.io/account/${solanaWallet}`;
|
|
146
|
+
lines.push("", `**[Solana Wallet](${solscanUrl})**`, `\`${solanaWallet}\``);
|
|
147
|
+
lines.push(` ${snap.sol} SOL`, ` ${snap.ui} USDC`);
|
|
148
|
+
if (snap.spend.today > 0) lines.push(` -${snap.spend.today.toFixed(2)} USDC today`);
|
|
149
|
+
}
|
|
150
|
+
if (snap.tokens.length > 0) {
|
|
151
|
+
const displayTokens = snap.tokens.slice(0, 10);
|
|
152
|
+
const symbols = await resolveTokenSymbols(displayTokens.map((t) => t.mint));
|
|
153
|
+
lines.push("", "**Solana Tokens**");
|
|
154
|
+
for (const t of displayTokens) {
|
|
155
|
+
const label = symbols.get(t.mint) ?? `${t.mint.slice(0, 4)}...${t.mint.slice(-4)}`;
|
|
156
|
+
const amt = Number.parseFloat(t.amount).toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
157
|
+
lines.push(` ${amt} ${label}`);
|
|
158
|
+
}
|
|
159
|
+
if (snap.tokens.length > 10) lines.push(` ...and ${snap.tokens.length - 10} more`);
|
|
160
|
+
}
|
|
161
|
+
if (snap.tokens.length <= INLINE_HISTORY_TOKEN_THRESHOLD) {
|
|
162
|
+
const recentRecords = snap.records.slice(-STATUS_HISTORY_COUNT).reverse();
|
|
163
|
+
if (recentRecords.length > 0) {
|
|
164
|
+
lines.push("", "**Recent**");
|
|
165
|
+
for (const r of recentRecords) lines.push(formatTxLine(r));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!evmWallet) lines.push("", "MPP setup: add an EVM wallet via `x402-proxy setup` or `X402_PROXY_WALLET_EVM_KEY`.");
|
|
169
|
+
if (!solanaWallet) lines.push("", "x402 setup: add a Solana wallet or mnemonic if you need Solana x402.");
|
|
170
|
+
lines.push("", "History: `/x_wallet history`", "Send: `/x_send <amount|all> <address>`");
|
|
171
|
+
if (ctx.dashboardUrl) lines.push(`[Dashboard](${ctx.dashboardUrl})`);
|
|
172
|
+
return { text: lines.join("\n") };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { text: `Failed to check wallet status: ${String(err)}` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function createSendCommand(ctx) {
|
|
180
|
+
return {
|
|
181
|
+
name: "x_send",
|
|
182
|
+
description: "Send USDC from the plugin wallet with an explicit confirmation step",
|
|
183
|
+
acceptsArgs: true,
|
|
184
|
+
requireAuth: true,
|
|
185
|
+
handler: async (cmdCtx) => {
|
|
186
|
+
await ctx.ensureReady();
|
|
187
|
+
const wallet = ctx.getSolanaWalletAddress();
|
|
188
|
+
if (!wallet) return { text: "No Solana wallet configured. `/x_send` only works with the Solana USDC wallet." };
|
|
189
|
+
const key = senderKey(cmdCtx);
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const pending = pendingSends.get(key);
|
|
192
|
+
if (pending && now - pending.createdAt > SEND_CONFIRM_TTL_MS) pendingSends.delete(key);
|
|
193
|
+
const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
|
|
194
|
+
if (parts.length === 0) return { text: "Usage: `/x_send <amount|all> <address>`\nConfirm with `/x_send confirm`\nCancel with `/x_send cancel`" };
|
|
195
|
+
if (parts[0]?.toLowerCase() === "confirm") {
|
|
196
|
+
const next = pendingSends.get(key);
|
|
197
|
+
if (!next) return { text: "No pending transfer to confirm." };
|
|
198
|
+
pendingSends.delete(key);
|
|
199
|
+
return executeSend(next.amount, next.destination, wallet, ctx.getSigner(), ctx.rpcUrl, ctx.historyPath);
|
|
200
|
+
}
|
|
201
|
+
if (parts[0]?.toLowerCase() === "cancel") {
|
|
202
|
+
pendingSends.delete(key);
|
|
203
|
+
return { text: "Pending transfer cleared." };
|
|
204
|
+
}
|
|
205
|
+
if (parts.length !== 2) return { text: "Usage: `/x_send <amount|all> <address>`\nExample: `/x_send 0.5 7xKXtg...`" };
|
|
206
|
+
const [amount, destination] = parts;
|
|
207
|
+
if (destination.length < 32 || destination.length > 44) return { text: `Invalid Solana address: ${destination}` };
|
|
208
|
+
pendingSends.set(key, {
|
|
209
|
+
amount,
|
|
210
|
+
destination,
|
|
211
|
+
createdAt: now
|
|
212
|
+
});
|
|
213
|
+
return { text: `Pending transfer: send ${amount} USDC to \`${destination}\`\nConfirm within 5 minutes with \`/x_send confirm\`\nCancel with \`/x_send cancel\`` };
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { createSendCommand, createWalletCommand };
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//#region packages/x402-proxy/src/openclaw/defaults.ts
|
|
2
|
+
const DEFAULT_SURF_PROVIDER_ID = "surf";
|
|
3
|
+
const DEFAULT_SURF_BASE_URL = "/x402-proxy/v1";
|
|
4
|
+
const DEFAULT_SURF_UPSTREAM_URL = "https://surf.cascade.fyi/api/v1/inference";
|
|
5
|
+
const DEFAULT_SURF_MODELS = [
|
|
6
|
+
{
|
|
7
|
+
id: "anthropic/claude-opus-4.6",
|
|
8
|
+
name: "Claude Opus 4.6",
|
|
9
|
+
maxTokens: 2e5,
|
|
10
|
+
reasoning: true,
|
|
11
|
+
input: ["text", "image"],
|
|
12
|
+
cost: {
|
|
13
|
+
input: .015,
|
|
14
|
+
output: .075,
|
|
15
|
+
cacheRead: .0015,
|
|
16
|
+
cacheWrite: .01875
|
|
17
|
+
},
|
|
18
|
+
contextWindow: 2e5
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "anthropic/claude-sonnet-4.6",
|
|
22
|
+
name: "Claude Sonnet 4.6",
|
|
23
|
+
maxTokens: 2e5,
|
|
24
|
+
reasoning: true,
|
|
25
|
+
input: ["text", "image"],
|
|
26
|
+
cost: {
|
|
27
|
+
input: .003,
|
|
28
|
+
output: .015,
|
|
29
|
+
cacheRead: 3e-4,
|
|
30
|
+
cacheWrite: .00375
|
|
31
|
+
},
|
|
32
|
+
contextWindow: 2e5
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "x-ai/grok-4.20-beta",
|
|
36
|
+
name: "Grok 4.20 Beta",
|
|
37
|
+
maxTokens: 131072,
|
|
38
|
+
reasoning: true,
|
|
39
|
+
input: ["text"],
|
|
40
|
+
cost: {
|
|
41
|
+
input: .003,
|
|
42
|
+
output: .015,
|
|
43
|
+
cacheRead: 0,
|
|
44
|
+
cacheWrite: 0
|
|
45
|
+
},
|
|
46
|
+
contextWindow: 131072
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "minimax/minimax-m2.5",
|
|
50
|
+
name: "MiniMax M2.5",
|
|
51
|
+
maxTokens: 1e6,
|
|
52
|
+
reasoning: false,
|
|
53
|
+
input: ["text"],
|
|
54
|
+
cost: {
|
|
55
|
+
input: .001,
|
|
56
|
+
output: .005,
|
|
57
|
+
cacheRead: 0,
|
|
58
|
+
cacheWrite: 0
|
|
59
|
+
},
|
|
60
|
+
contextWindow: 1e6
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "moonshotai/kimi-k2.5",
|
|
64
|
+
name: "Kimi K2.5",
|
|
65
|
+
maxTokens: 131072,
|
|
66
|
+
reasoning: true,
|
|
67
|
+
input: ["text"],
|
|
68
|
+
cost: {
|
|
69
|
+
input: .002,
|
|
70
|
+
output: .008,
|
|
71
|
+
cacheRead: 0,
|
|
72
|
+
cacheWrite: 0
|
|
73
|
+
},
|
|
74
|
+
contextWindow: 131072
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "z-ai/glm-5",
|
|
78
|
+
name: "GLM-5",
|
|
79
|
+
maxTokens: 128e3,
|
|
80
|
+
reasoning: false,
|
|
81
|
+
input: ["text"],
|
|
82
|
+
cost: {
|
|
83
|
+
input: .001,
|
|
84
|
+
output: .005,
|
|
85
|
+
cacheRead: 0,
|
|
86
|
+
cacheWrite: 0
|
|
87
|
+
},
|
|
88
|
+
contextWindow: 128e3
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
function resolveProviders(config) {
|
|
92
|
+
const defaultProtocol = resolveProtocol(config.protocol);
|
|
93
|
+
const defaultMppSessionBudget = resolveMppSessionBudget(config.mppSessionBudget);
|
|
94
|
+
const raw = config.providers ?? {};
|
|
95
|
+
const entries = Object.entries(raw).length > 0 ? Object.entries(raw).map(([id, provider]) => ({
|
|
96
|
+
id,
|
|
97
|
+
baseUrl: provider.baseUrl || "/x402-proxy/v1",
|
|
98
|
+
upstreamUrl: provider.upstreamUrl || "https://surf.cascade.fyi/api/v1/inference",
|
|
99
|
+
protocol: resolveProtocol(provider.protocol, defaultProtocol),
|
|
100
|
+
mppSessionBudget: resolveMppSessionBudget(provider.mppSessionBudget, defaultMppSessionBudget),
|
|
101
|
+
models: provider.models && provider.models.length > 0 ? provider.models : DEFAULT_SURF_MODELS
|
|
102
|
+
})) : [{
|
|
103
|
+
id: DEFAULT_SURF_PROVIDER_ID,
|
|
104
|
+
baseUrl: DEFAULT_SURF_BASE_URL,
|
|
105
|
+
upstreamUrl: DEFAULT_SURF_UPSTREAM_URL,
|
|
106
|
+
protocol: defaultProtocol,
|
|
107
|
+
mppSessionBudget: defaultMppSessionBudget,
|
|
108
|
+
models: DEFAULT_SURF_MODELS
|
|
109
|
+
}];
|
|
110
|
+
return {
|
|
111
|
+
providers: entries,
|
|
112
|
+
models: entries.flatMap((provider) => provider.models.map((model) => ({
|
|
113
|
+
...model,
|
|
114
|
+
provider: provider.id
|
|
115
|
+
})))
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function routePrefixForBaseUrl(baseUrl) {
|
|
119
|
+
const segments = baseUrl.split("/").filter(Boolean);
|
|
120
|
+
return segments.length > 0 ? `/${segments[0]}` : "/";
|
|
121
|
+
}
|
|
122
|
+
function resolveProtocol(value, fallback = "mpp") {
|
|
123
|
+
return value === "x402" || value === "mpp" || value === "auto" ? value : fallback;
|
|
124
|
+
}
|
|
125
|
+
function resolveMppSessionBudget(value, fallback = "0.5") {
|
|
126
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { resolveProviders, routePrefixForBaseUrl };
|