#!/usr/bin/env node /** * SpendOS Public MCP Server — x402 Paid AI Tools * * External agents (Claude Code, Cursor, any MCP client) connect to this * server and get access to x402-paid AI tools on Base mainnet. * Each call pays real USDC to the SpendOS agent wallet. * * Track 3 #5: "Build MCP tool servers that charge per invocation via x402." * * Setup: * export EVM_PRIVATE_KEY=0x... # wallet with USDC on Base * npx tsx src/mcp-public.ts * * Claude Desktop config: * { * "mcpServers": { * "spendos": { * "command": "npx", * "args": ["tsx", "/path/to/spendos/src/mcp-public.ts"], * "env": { * "EVM_PRIVATE_KEY": "0x...", * "SPENDOS_URL": "https://spendos.xyz" * } * } * } * } */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import axios from 'axios'; import { x402Client, wrapAxiosWithPayment } from '@x402/axios'; import { registerExactEvmScheme } from '@x402/evm/exact/client'; import { privateKeyToAccount } from 'viem/accounts'; import { z } from 'zod'; const SPENDOS_URL = process.env.SPENDOS_URL ?? 'https://spendos.xyz'; const EVM_KEY = process.env.EVM_PRIVATE_KEY ?? ''; async function main() { // ── x402 Payment Client ────────────────────────────── const client = new x402Client(); if (EVM_KEY) { const signer = privateKeyToAccount(EVM_KEY as `0x${string}`); registerExactEvmScheme(client, { signer }); console.error(`[SpendOS MCP] Payment wallet: ${signer.address} (Base mainnet USDC)`); } else { console.error('[SpendOS MCP] No EVM_PRIVATE_KEY — x402 payments will fail'); } const api = wrapAxiosWithPayment(axios.create({ baseURL: SPENDOS_URL }), client); // ── MCP Server ──────────────────────────────────────── const server = new McpServer({ name: 'spendos-x402', version: '0.1.0', }); // Tool: Summarize URL ($0.01 USDC) server.tool( 'summarize_url', 'Summarize any URL into a concise brief. Costs $0.01 USDC on Base via x402. Revenue goes to the SpendOS autonomous agent.', { url: z.string().describe('The URL to summarize') }, async ({ url }) => { const res = await api.post('/api/summarize', { url }); return { content: [{ type: 'text', text: `Summary: ${res.data.summary}\n\nPaid: $${res.data.cost?.earned ?? '0.01'} USDC → SpendOS agent`, }], }; }, ); // Tool: Generate Image ($0.05 USDC) server.tool( 'generate_image', 'Generate an AI image from a text prompt. Costs $0.05 USDC on Base via x402. Revenue goes to the SpendOS autonomous agent.', { prompt: z.string().describe('Text description of the image to generate') }, async ({ prompt }) => { const res = await api.post('/api/generate-image', { prompt }); const imageUrl = res.data.images?.[0]?.url ?? '[base64 image data]'; return { content: [{ type: 'text', text: `Image: ${imageUrl}\nPaid: $0.05 USDC → SpendOS agent` }], }; }, ); // Tool: Check Agent P&L (free) server.tool( 'check_agent_pnl', 'Check the SpendOS agent revenue, costs, and profit margin. Free — no payment required.', {}, async () => { const res = await axios.get(`${SPENDOS_URL}/api/pnl`); const pnl = res.data; const margin = pnl.totalEarned > 0 ? ((pnl.profit / pnl.totalEarned) * 100).toFixed(0) : 'n/a'; return { content: [{ type: 'text', text: `SpendOS Agent P&L (live):\n Revenue: $${pnl.totalEarned.toFixed(3)} USDC\n Costs: $${pnl.totalSpent.toFixed(4)}\n Profit: $${pnl.profit.toFixed(4)}\n Queries: ${pnl.queryCount}\n Margin: ${margin}%`, }], }; }, ); // ── Dynamic Jobs (loaded from agent's job registry) ──── try { const jobsRes = await axios.get(`${SPENDOS_URL}/api/jobs`); const jobs = jobsRes.data as Array<{ name: string; price: string; inputs: string[]; description?: string }>; for (const job of jobs) { const inputSchema: Record = {}; for (const input of job.inputs) { inputSchema[input] = z.string().describe(input); } server.tool( `job_${job.name}`, `${job.description ?? job.name}. Costs ${job.price} USDC on Base via x402.`, inputSchema, async (args) => { const res = await api.post(`/api/jobs/${job.name}`, args); return { content: [{ type: 'text', text: `Result: ${res.data.result}\nPaid: ${job.price} USDC → SpendOS agent` }], }; }, ); } console.error(`[SpendOS MCP] Loaded ${jobs.length} dynamic jobs from registry`); } catch (err) { console.error(`[SpendOS MCP] Failed to load jobs: ${err}`); } // ── Start ────────────────────────────────────────────── const transport = new StdioServerTransport(); await server.connect(transport); console.error(`[SpendOS MCP] Ready — 3 tools (2 paid, 1 free) → ${SPENDOS_URL}`); } main().catch((err) => { console.error(`[SpendOS MCP] Fatal: ${err}`); process.exit(1); });