Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XClawRouter

Smart LLM router for autonomous agents. 55+ models. Wallet-based auth. USDC micropayments via x402.
Smart LLM router for autonomous agents. 60+ models. Wallet-based auth. USDC micropayments via x402.

## Commands

Expand Down
139 changes: 74 additions & 65 deletions README.md

Large diffs are not rendered by default.

62 changes: 56 additions & 6 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -31852,19 +31852,19 @@ var init_onchainos_adapter = __esm({
*/
async signX402Payment(accepts) {
const acceptsJson = JSON.stringify(accepts);
const stdout = await runCli(this.bin, ["payment", "x402-pay", "--accepts", acceptsJson], {
const stdout = await runCli(this.bin, ["payment", "pay", "--accepts", acceptsJson], {
timeoutMs: PAYMENT_TIMEOUT_MS
});
const parsed = parseJson(stdout, "payment x402-pay");
const parsed = parseJson(stdout, "payment pay");
const result = unwrapData(parsed);
if (!result.signature || typeof result.signature !== "string") {
throw new OnchainOsCliError(
`onchainos payment x402-pay returned no signature: ${JSON.stringify(parsed).slice(0, 500)}`
`onchainos payment pay returned no signature: ${JSON.stringify(parsed).slice(0, 500)}`
);
}
if (!result.authorization || typeof result.authorization !== "object") {
throw new OnchainOsCliError(
`onchainos payment x402-pay returned no authorization: ${JSON.stringify(parsed).slice(0, 500)}`
`onchainos payment pay returned no authorization: ${JSON.stringify(parsed).slice(0, 500)}`
);
}
return {
Expand Down Expand Up @@ -80973,9 +80973,44 @@ data: [DONE]

// src/cli.ts
init_auth();
import { basename } from "path";

// src/cli-startup.ts
function enableBlockingStdout(stream = process.stdout) {
if (stream.isTTY) return false;
try {
if (!stream._handle) return false;
stream._handle.setBlocking(true);
return true;
} catch {
return false;
}
}
function resolveAutoBaseChain(opts) {
if (opts.walletSource !== "okx") return null;
if (opts.pinnedChain) return null;
return opts.currentChain === "base" ? null : "base";
}
function formatFundingHint(walletSource, address2) {
if (walletSource !== "okx") {
return [`Fund wallet for premium models: ${address2}`];
}
const heading = "Fund your OKX Agentic Wallet for premium models";
const instruction = "Send USDC on Base to your EVM address:";
const inner = Math.max(heading.length, instruction.length, address2.length) + 2;
const pad4 = (s3) => ` ${s3}${" ".repeat(inner - s3.length - 1)}`;
return [
`\u250C${"\u2500".repeat(inner)}\u2510`,
`\u2502${pad4(heading)}\u2502`,
`\u2502${pad4(instruction)}\u2502`,
`\u2502${pad4(address2)}\u2502`,
`\u2514${"\u2500".repeat(inner)}\u2518`
];
}

// src/cli.ts
init_onchainos_adapter();
init_wallet();
import { basename } from "path";
import { execSync, execFileSync as execFileSync2 } from "child_process";
import { homedir as homedir7 } from "os";
import { createInterface } from "readline/promises";
Expand Down Expand Up @@ -82367,6 +82402,7 @@ function parseArgs(args) {
return result;
}
async function main() {
enableBlockingStdout();
const invokedAs = process.argv[1] ? basename(process.argv[1]) : "";
if (invokedAs === "clawrouter" && !process.env.XCLAW_SUPPRESS_RENAME_NOTICE) {
console.warn(
Expand Down Expand Up @@ -82575,6 +82611,18 @@ ClawRouter Partner APIs (v${VERSION})
if (printedStatus) {
console.log(`[XClawRouter] (set XCLAW_QUIET=1 to suppress this block)`);
}
if (wallet.source === "okx") {
const pinnedChain = process.env.XCLAWROUTER_PAYMENT_CHAIN ?? process.env.CLAWROUTER_PAYMENT_CHAIN;
const autoChain = resolveAutoBaseChain({
walletSource: wallet.source,
currentChain: await loadPaymentChain(),
pinnedChain
});
if (autoChain) {
await savePaymentChain(autoChain);
console.log(`[XClawRouter] OKX wallet detected \u2014 payment chain \u2192 Base (EVM)`);
}
}
if (wallet.solanaPrivateKeyBytes) {
try {
const solAddr = await getSolanaAddress(wallet.solanaPrivateKeyBytes);
Expand Down Expand Up @@ -82613,7 +82661,9 @@ ClawRouter Partner APIs (v${VERSION})
const balance = await proxy.balanceMonitor.checkBalance();
if (balance.isEmpty) {
console.log(`[XClawRouter] Wallet balance: $0.00 (using FREE model)`);
console.log(`[XClawRouter] Fund wallet for premium models: ${displayAddress}`);
for (const line of formatFundingHint(wallet.source, displayAddress)) {
console.log(`[XClawRouter] ${line}`);
}
} else if (balance.isLow) {
console.log(`[XClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
} else {
Expand Down
2 changes: 1 addition & 1 deletion dist/cli.js.map

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ declare class SolanaBalanceMonitor {
* tolerantly by `addresses()`.
* onchainos wallet login <email> (interactive)
* onchainos wallet logout
* onchainos payment x402-pay --accepts <json>
* onchainos payment pay --accepts <json>
* → { data: { signature, authorization, sessionCert? } }
*
* Some onchainos builds omit `evmAddress` from `wallet status` even when the
Expand All @@ -799,7 +799,7 @@ declare class SolanaBalanceMonitor {
* onchainos wallet".
*
* Raw EIP-712 / typed-data signing is NOT exposed by onchainos, so we use
* `payment x402-pay` for the entire signing step rather than the @x402/fetch
* `payment pay` for the entire signing step rather than the @x402/fetch
* signer plumbing. See proxy.ts for the call site.
*/
interface OnchainOsStatus {
Expand Down Expand Up @@ -860,7 +860,7 @@ declare class OnchainOsAdapter {
* Wallet identity is resolved in this order:
* 1. OKX onchainos CLI (if installed AND user is logged in) — preferred.
* Private keys never enter this process; signing happens via
* `onchainos payment x402-pay`. See onchainos-adapter.ts.
* `onchainos payment pay`. See onchainos-adapter.ts.
* 2. Saved wallet.key file (legacy BIP-39 path — preserved for existing users)
* 3. BLOCKRUN_WALLET_KEY env var (legacy)
* 4. Auto-generated BIP-39 wallet — **opt-in only**, gated behind
Expand Down Expand Up @@ -1793,7 +1793,7 @@ declare function buildPartnerTools(proxyBaseUrl: string): PartnerToolDefinition[
/**
* @blockrun/xclawrouter
*
* Smart LLM router for OpenClaw — 55+ models, x402 micropayments, 78% cost savings.
* Smart LLM router for OpenClaw — 60+ models, x402 micropayments, 78% cost savings.
* Routes each request to the cheapest model that can handle it.
*
* Usage:
Expand Down
12 changes: 6 additions & 6 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75379,19 +75379,19 @@ var OnchainOsAdapter = class {
*/
async signX402Payment(accepts) {
const acceptsJson = JSON.stringify(accepts);
const stdout = await runCli(this.bin, ["payment", "x402-pay", "--accepts", acceptsJson], {
const stdout = await runCli(this.bin, ["payment", "pay", "--accepts", acceptsJson], {
timeoutMs: PAYMENT_TIMEOUT_MS
});
const parsed = parseJson(stdout, "payment x402-pay");
const parsed = parseJson(stdout, "payment pay");
const result = unwrapData(parsed);
if (!result.signature || typeof result.signature !== "string") {
throw new OnchainOsCliError(
`onchainos payment x402-pay returned no signature: ${JSON.stringify(parsed).slice(0, 500)}`
`onchainos payment pay returned no signature: ${JSON.stringify(parsed).slice(0, 500)}`
);
}
if (!result.authorization || typeof result.authorization !== "object") {
throw new OnchainOsCliError(
`onchainos payment x402-pay returned no authorization: ${JSON.stringify(parsed).slice(0, 500)}`
`onchainos payment pay returned no authorization: ${JSON.stringify(parsed).slice(0, 500)}`
);
}
return {
Expand Down Expand Up @@ -83443,7 +83443,7 @@ var plugin = {
// declares "xclawrouter", so this must too.
id: "xclawrouter",
name: "XClawRouter",
description: "Smart LLM router \u2014 55+ models, x402 micropayments, 78% cost savings",
description: "Smart LLM router \u2014 60+ models, x402 micropayments, 78% cost savings",
version: VERSION,
// OpenClaw 2026.5.7+ requires plugins to declare upfront every tool name
// they will register via `api.registerTool()`. The gateway's
Expand Down Expand Up @@ -83534,7 +83534,7 @@ var plugin = {
const shouldLogRegistration = !proc.__clawrouterRegistrationLogged;
proc.__clawrouterRegistrationLogged = true;
if (shouldLogRegistration) {
api.logger.info("BlockRun provider registered (55+ models via x402)");
api.logger.info("BlockRun provider registered (60+ models via x402)");
if (typeof api.registerWebSearchProvider === "function") {
api.logger.info(`Registered BlockRun web_search provider (${BLOCKRUN_EXA_PROVIDER_ID})`);
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "xclawrouter",
"name": "XClawRouter",
"description": "Smart LLM router for OKX — agentic wallet, 55+ models, x402 micropayments on Base + Solana",
"description": "Smart LLM router for OKX — agentic wallet, 60+ models, x402 micropayments on Base + Solana",
"skills": ["./skills"],
"contracts": {
"tools": [
Expand Down
2 changes: 1 addition & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Wallet identity is resolved in this order:
* 1. OKX onchainos CLI (if installed AND user is logged in) — preferred.
* Private keys never enter this process; signing happens via
* `onchainos payment x402-pay`. See onchainos-adapter.ts.
* `onchainos payment pay`. See onchainos-adapter.ts.
* 2. Saved wallet.key file (legacy BIP-39 path — preserved for existing users)
* 3. BLOCKRUN_WALLET_KEY env var (legacy)
* 4. Auto-generated BIP-39 wallet — **opt-in only**, gated behind
Expand Down
105 changes: 105 additions & 0 deletions src/cli-startup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, it, expect } from "vitest";
import { enableBlockingStdout, resolveAutoBaseChain, formatFundingHint } from "./cli-startup.js";

describe("enableBlockingStdout", () => {
it("is a no-op on a TTY (never touches the handle)", () => {
let called = false;
const stream = {
isTTY: true,
_handle: {
setBlocking() {
called = true;
},
},
};
expect(enableBlockingStdout(stream)).toBe(false);
expect(called).toBe(false);
});

it("switches the handle to blocking on a non-TTY stream", () => {
const calls: boolean[] = [];
const stream = {
isTTY: false,
_handle: {
setBlocking(b: boolean) {
calls.push(b);
},
},
};
expect(enableBlockingStdout(stream)).toBe(true);
expect(calls).toEqual([true]);
});

it("returns false (no throw) when a non-TTY stream has no handle", () => {
expect(enableBlockingStdout({ isTTY: false })).toBe(false);
});

it("swallows a throwing setBlocking and returns false", () => {
const stream = {
isTTY: false,
_handle: {
setBlocking() {
throw new Error("setBlocking not supported here");
},
},
};
expect(enableBlockingStdout(stream)).toBe(false);
});
});

describe("resolveAutoBaseChain", () => {
it("switches an OKX wallet off Solana to Base", () => {
expect(resolveAutoBaseChain({ walletSource: "okx", currentChain: "solana" })).toBe("base");
});

it("is a no-op when an OKX wallet is already on Base", () => {
expect(resolveAutoBaseChain({ walletSource: "okx", currentChain: "base" })).toBeNull();
});

it("honours an explicit env pin even on OKX + Solana", () => {
expect(
resolveAutoBaseChain({ walletSource: "okx", currentChain: "solana", pinnedChain: "solana" }),
).toBeNull();
expect(
resolveAutoBaseChain({ walletSource: "okx", currentChain: "solana", pinnedChain: "base" }),
).toBeNull();
});

it("never switches a non-OKX wallet, regardless of chain", () => {
expect(resolveAutoBaseChain({ walletSource: "saved", currentChain: "solana" })).toBeNull();
expect(resolveAutoBaseChain({ walletSource: "generated", currentChain: "solana" })).toBeNull();
});
});

describe("formatFundingHint", () => {
it("returns the one-line hint for non-OKX wallets", () => {
expect(formatFundingHint("saved", "0xABCDEF")).toEqual([
"Fund wallet for premium models: 0xABCDEF",
]);
});

it("returns a 5-line framed block for OKX wallets containing the address", () => {
const addr = "0xb4c0000000000000000000000000000000abad8b";
const lines = formatFundingHint("okx", addr);
expect(lines).toHaveLength(5);
expect(lines[0].startsWith("┌")).toBe(true);
expect(lines[4].startsWith("└")).toBe(true);
expect(lines.some((l) => l.includes("Send USDC on Base"))).toBe(true);
expect(lines.some((l) => l.includes(addr))).toBe(true);
});

it("draws a frame whose lines are all the same width (no overflow)", () => {
const lines = formatFundingHint("okx", "0xdeadbeef");
const widths = new Set(lines.map((l) => [...l].length));
expect(widths.size).toBe(1);
});

it("grows the frame to fit an address longer than the heading", () => {
const longAddr = "0x" + "a".repeat(80);
const lines = formatFundingHint("okx", longAddr);
const widths = new Set(lines.map((l) => [...l].length));
expect(widths.size).toBe(1);
// the address line still contains the full, untruncated address
expect(lines.some((l) => l.includes(longAddr))).toBe(true);
});
});
Loading
Loading