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
52 changes: 52 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { resolveOrGenerateWalletKey } from "./auth.js";
import { BalanceMonitor } from "./balance.js";
import { VERSION } from "./version.js";
import { runDoctor } from "./doctor.js";
import { PARTNER_SERVICES } from "./partners/index.js";

function printHelp(): void {
console.log(`
Expand All @@ -26,6 +27,7 @@ ClawRouter v${VERSION} - Smart LLM Router
Usage:
clawrouter [options]
clawrouter doctor [opus] [question]
clawrouter partners [test]

Options:
--version, -v Show version number
Expand All @@ -35,6 +37,8 @@ Options:
Commands:
doctor AI-powered diagnostics (default: Sonnet ~$0.003)
doctor opus Use Opus for deeper analysis (~$0.01)
partners List available partner APIs with pricing
partners test Test partner API endpoints (expect 402 = alive)

Examples:
# Start standalone proxy
Expand Down Expand Up @@ -64,12 +68,16 @@ function parseArgs(args: string[]): {
version: boolean;
help: boolean;
doctor: boolean;
partners: boolean;
partnersTest: boolean;
port?: number;
} {
const result = {
version: false,
help: false,
doctor: false,
partners: false,
partnersTest: false,
port: undefined as number | undefined,
};

Expand All @@ -81,6 +89,13 @@ function parseArgs(args: string[]): {
result.help = true;
} else if (arg === "doctor" || arg === "--doctor") {
result.doctor = true;
} else if (arg === "partners") {
result.partners = true;
// Check for "test" subcommand
if (args[i + 1] === "test") {
result.partnersTest = true;
i++;
}
} else if (arg === "--port" && args[i + 1]) {
result.port = parseInt(args[i + 1], 10);
i++; // Skip next arg
Expand Down Expand Up @@ -126,6 +141,43 @@ async function main(): Promise<void> {
process.exit(0);
}

if (args.partners) {
if (PARTNER_SERVICES.length === 0) {
console.log("No partner APIs available.");
process.exit(0);
}

console.log(`\nClawRouter Partner APIs (v${VERSION})\n`);

for (const svc of PARTNER_SERVICES) {
console.log(` ${svc.name} (${svc.partner})`);
console.log(` ${svc.description}`);
console.log(` Tool: blockrun_${svc.id}`);
console.log(` Method: ${svc.method} /v1${svc.proxyPath}`);
console.log(` Pricing: ${svc.pricing.perUnit} per ${svc.pricing.unit} (min ${svc.pricing.minimum}, max ${svc.pricing.maximum})`);
console.log();
}

if (args.partnersTest) {
console.log("Testing partner endpoints...\n");
const apiBase = "https://blockrun.ai/api";
for (const svc of PARTNER_SERVICES) {
const url = `${apiBase}/v1${svc.proxyPath}`;
try {
const response = await fetch(url, { method: "GET" });
const status = response.status;
const ok = status === 402 ? "alive (402 = payment required)" : `status ${status}`;
console.log(` ${svc.id}: ${ok}`);
} catch (err) {
console.log(` ${svc.id}: error - ${err instanceof Error ? err.message : String(err)}`);
}
}
console.log();
}

process.exit(0);
}

// Resolve wallet key
const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();

Expand Down
47 changes: 47 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,51 @@ const plugin: OpenClawPluginDefinition = {

api.logger.info("BlockRun provider registered (30+ models via x402)");

// Register partner API tools (Twitter/X lookup, etc.)
try {
const { buildPartnerTools, PARTNER_SERVICES } = await import("./partners/index.js");
const proxyBaseUrl = `http://127.0.0.1:${runtimePort}`;
const partnerTools = buildPartnerTools(proxyBaseUrl);
for (const tool of partnerTools) {
api.registerTool(tool);
}
if (partnerTools.length > 0) {
api.logger.info(`Registered ${partnerTools.length} partner tool(s): ${partnerTools.map((t) => t.name).join(", ")}`);
}

// Register /partners command
api.registerCommand({
name: "partners",
description: "List available partner APIs and pricing",
acceptsArgs: false,
requireAuth: false,
handler: async () => {
if (PARTNER_SERVICES.length === 0) {
return { text: "No partner APIs available." };
}

const lines = [
"**Partner APIs** (paid via your ClawRouter wallet)",
"",
];

for (const svc of PARTNER_SERVICES) {
lines.push(`**${svc.name}** (${svc.partner})`);
lines.push(` ${svc.description}`);
lines.push(` Tool: \`${`blockrun_${svc.id}`}\``);
lines.push(` Pricing: ${svc.pricing.perUnit} per ${svc.pricing.unit} (min ${svc.pricing.minimum}, max ${svc.pricing.maximum})`);
lines.push("");
}

return { text: lines.join("\n") };
},
});
} catch (err) {
api.logger.warn(
`Failed to register partner tools: ${err instanceof Error ? err.message : String(err)}`,
);
}

// Register /wallet command for wallet management
createWalletCommand()
.then((walletCommand) => {
Expand Down Expand Up @@ -807,3 +852,5 @@ export { SessionStore, getSessionId, DEFAULT_SESSION_CONFIG } from "./session.js
export type { SessionEntry, SessionConfig } from "./session.js";
export { ResponseCache } from "./response-cache.js";
export type { CachedLLMResponse, ResponseCacheConfig } from "./response-cache.js";
export { PARTNER_SERVICES, getPartnerService, buildPartnerTools } from "./partners/index.js";
export type { PartnerServiceDefinition, PartnerToolDefinition } from "./partners/index.js";
4 changes: 4 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type UsageEntry = {
baselineCost: number;
savings: number; // 0-1 percentage
latencyMs: number;
/** Partner service ID (e.g., "x_users_lookup") — only set for partner API calls */
partnerId?: string;
/** Partner service name (e.g., "AttentionVC") — only set for partner API calls */
service?: string;
};

const LOG_DIR = join(homedir(), ".openclaw", "blockrun", "logs");
Expand Down
8 changes: 8 additions & 0 deletions src/partners/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Partner API Integration — barrel export
*/

export { PARTNER_SERVICES, getPartnerService } from "./registry.js";
export type { PartnerServiceDefinition, PartnerServiceParam } from "./registry.js";
export { buildPartnerTools } from "./tools.js";
export type { PartnerToolDefinition } from "./tools.js";
85 changes: 85 additions & 0 deletions src/partners/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Partner Service Registry
*
* Defines available partner APIs that can be called through ClawRouter's proxy.
* Partners provide specialized data (Twitter/X, etc.) via x402 micropayments.
* The same wallet used for LLM calls pays for partner API calls — zero extra setup.
*/

export type PartnerServiceParam = {
name: string;
type: "string" | "string[]" | "number";
description: string;
required: boolean;
};

export type PartnerServiceDefinition = {
/** Unique service ID used in tool names: blockrun_{id} */
id: string;
/** Human-readable name */
name: string;
/** Partner providing this service */
partner: string;
/** Short description for tool listing */
description: string;
/** Proxy path (relative to /v1) */
proxyPath: string;
/** HTTP method */
method: "GET" | "POST";
/** Parameters for the tool's JSON Schema */
params: PartnerServiceParam[];
/** Pricing info for display */
pricing: {
perUnit: string;
unit: string;
minimum: string;
maximum: string;
};
/** Example usage for help text */
example: {
input: Record<string, unknown>;
description: string;
};
};

/**
* All registered partner services.
* New partners are added here — the rest of the system picks them up automatically.
*/
export const PARTNER_SERVICES: PartnerServiceDefinition[] = [
{
id: "x_users_lookup",
name: "Twitter/X User Lookup",
partner: "AttentionVC",
description:
"Look up Twitter/X user profiles by username. Returns follower counts, verification status, bio, and more. Accepts up to 100 usernames per request.",
proxyPath: "/x/users/lookup",
method: "POST",
params: [
{
name: "usernames",
type: "string[]",
description:
'Array of Twitter/X usernames to look up (without @ prefix). Example: ["elonmusk", "naval"]',
required: true,
},
],
pricing: {
perUnit: "$0.001",
unit: "user",
minimum: "$0.01 (10 users)",
maximum: "$0.10 (100 users)",
},
example: {
input: { usernames: ["elonmusk", "naval", "balaboris"] },
description: "Look up 3 Twitter/X user profiles",
},
},
];

/**
* Get a partner service by ID.
*/
export function getPartnerService(id: string): PartnerServiceDefinition | undefined {
return PARTNER_SERVICES.find((s) => s.id === id);
}
92 changes: 92 additions & 0 deletions src/partners/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Partner Tool Builder
*
* Converts partner service definitions into OpenClaw tool definitions.
* Each tool's execute() calls through the local proxy which handles
* x402 payment transparently using the same wallet.
*/

import { PARTNER_SERVICES, type PartnerServiceDefinition } from "./registry.js";

/** OpenClaw tool definition shape (duck-typed) */
export type PartnerToolDefinition = {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<string, unknown>;
required: string[];
};
execute: (args: Record<string, unknown>) => Promise<unknown>;
};

/**
* Build a single partner tool from a service definition.
*/
function buildTool(
service: PartnerServiceDefinition,
proxyBaseUrl: string,
): PartnerToolDefinition {
// Build JSON Schema properties from service params
const properties: Record<string, unknown> = {};
const required: string[] = [];

for (const param of service.params) {
const prop: Record<string, unknown> = {
description: param.description,
};

if (param.type === "string[]") {
prop.type = "array";
prop.items = { type: "string" };
} else {
prop.type = param.type;
}

properties[param.name] = prop;
if (param.required) {
required.push(param.name);
}
}

return {
name: `blockrun_${service.id}`,
description: [
service.description,
"",
`Partner: ${service.partner}`,
`Pricing: ${service.pricing.perUnit} per ${service.pricing.unit} (min: ${service.pricing.minimum}, max: ${service.pricing.maximum})`,
].join("\n"),
inputSchema: {
type: "object",
properties,
required,
},
execute: async (args: Record<string, unknown>) => {
const url = `${proxyBaseUrl}/v1${service.proxyPath}`;

const response = await fetch(url, {
method: service.method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(args),
});

if (!response.ok) {
const errText = await response.text().catch(() => "");
throw new Error(
`Partner API error (${response.status}): ${errText || response.statusText}`,
);
}

return response.json();
},
};
}

/**
* Build OpenClaw tool definitions for all registered partner services.
* @param proxyBaseUrl - Local proxy base URL (e.g., "http://127.0.0.1:8402")
*/
export function buildPartnerTools(proxyBaseUrl: string): PartnerToolDefinition[] {
return PARTNER_SERVICES.map((service) => buildTool(service, proxyBaseUrl));
}
Loading
Loading