#!/usr/bin/env node
// SmartPerps MCP server over stdio (newline-delimited JSON-RPC 2.0).
// Free by default: the hosted free preview (smart-money bias label, cohort traders long/short; 10/min per IP).
// Full data: set SMARTPERPS_X402_PRIVATE_KEY (or X402_PRIVATE_KEY) to a Base wallet key holding a little USDC, and this
// server pays the hosted API per call over x402 (coin $0.02, scan $0.03, wallet $0.01): https://api.youraitools.co/smartperps
// node src/mcp-stdio.js (run from a source checkout: https://agentgate-source.pages.dev/x402-smartperps/ ; remote MCP: https://api.youraitools.co/smartperps/mcp)
import { createInterface } from 'node:readline';
import { toSymbol, toAddress, preview, DISCLAIMER, FULL_DETAILS, API_BASE, PRICES } from './lite.js';
import { createPaidClient } from './paid.js';
import { VERSION_STR as VERSION } from './version.js';
export { VERSION };
const SYM = { type: 'string', description: 'Hyperliquid perp ticker, e.g. BTC, ETH, SOL, HYPE' };
const RO = { readOnlyHint: true, openWorldHint: true, idempotentHint: false };
export const TOOLS = [
{ name: 'smart_money_positioning', title: 'Hyperliquid smart-money positioning for a coin', description: `How the top-120 30-day-PnL Hyperliquid cohort is positioned in a coin. Free by default: bias label (strong long ... strong short), cohort traders long vs short, snapshot age. With SMARTPERPS_X402_PRIVATE_KEY set, returns the paid coin tier (${PRICES.coin} USDC on Base per call over x402): long/short/net notional, 1h/4h/24h flows, average entry vs mark, leverage, liquidation price bands (aggregated, never per account) and the 10 largest cohort positions (addresses truncated). Read-only; not financial advice.`, inputSchema: { type: 'object', properties: { symbol: SYM }, required: ['symbol'] }, annotations: RO },
{ name: 'smart_money_scan', title: 'Scan Hyperliquid smart-money positioning across all coins', description: `Paid only (needs SMARTPERPS_X402_PRIVATE_KEY, ${PRICES.scan} USDC on Base per call): the cohort's gross book and the coins it is most net long, most net short and most one-sided on, plus the biggest 24h net buying and selling. Read-only; not financial advice.`, inputSchema: { type: 'object', properties: { limit: { type: 'integer', description: '1-25, default 10' } } }, annotations: RO },
{ name: 'hyperliquid_wallet_profile', title: 'Profile a Hyperliquid account', description: `Paid only (needs SMARTPERPS_X402_PRIVATE_KEY, ${PRICES.wallet} USDC on Base per call): live equity, leverage, margin usage, open positions with distance to liquidation (whole %), 30-day fills summary and cohort rank for the Hyperliquid address you supply. Public on-chain data only; do not use it to target or harass account holders. Read-only; not financial advice.`, inputSchema: { type: 'object', properties: { address: { type: 'string', description: '0x + 40 hex' } }, required: ['address'] }, annotations: RO },
];
const PROTOCOLS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
const lbl = (b) => String(b).replace(/_/g, ' ');
const summarizePreview = (r, note) => `${note ? note + '\n' : ''}SmartPerps ${r.symbol}: ${lbl(r.bias)}. ${r.bias_note || ''} Cohort traders long ${r.traders_long} / short ${r.traders_short}; snapshot ${r.snapshot_age_s}s old; confidence ${r.confidence}.\n${FULL_DETAILS} Set SMARTPERPS_X402_PRIVATE_KEY to get them here.\n${DISCLAIMER}`;
const summarizeCoin = (r) => `SmartPerps ${r.symbol}: ${lbl(r.bias)}; traders long ${r.traders_long} / short ${r.traders_short}; net notional $${r.net_notional_usd} (long share ${r.long_share_notional_pct}%); 24h net flow $${r.flows?.['24h']?.net_notional_change_usd ?? 'n/a'}; avg leverage ${r.avg_leverage}.\nTop positions:\n${(r.top_positions || []).slice(0, 5).map((p) => `- ${p.address ?? 'n/a'} ${p.side} $${p.notional_usd} @ ${p.entry_price} (${p.leverage}x), rank ${p.cohort_rank_30d_pnl}`).join('\n')}\n${DISCLAIMER}`;
const summarizeScan = (r) => `SmartPerps scan: cohort book long $${r.cohort_book?.gross_long_usd} / short $${r.cohort_book?.gross_short_usd} (long share ${r.cohort_book?.long_share_pct}%).\nMost net long: ${(r.most_net_long || []).map((x) => `${x.symbol} $${x.net_notional_usd}`).join(', ')}\nMost net short: ${(r.most_net_short || []).map((x) => `${x.symbol} $${x.net_notional_usd}`).join(', ')}\n${DISCLAIMER}`;
const summarizeWallet = (r) => `Hyperliquid ${r.address}: equity $${r.account_value_usd}, notional $${r.total_notional_usd} (${r.effective_leverage}x), ${r.open_positions} positions; 30d realized PnL $${r.last_30d?.realized_pnl_usd ?? 'n/a'}; in cohort: ${r.in_smart_money_cohort}${r.cohort_entry ? ` (rank ${r.cohort_entry.rank_30d_pnl})` : ''}.\n${DISCLAIMER}`;
let paidClient;
const paid = async (opts) => { if (paidClient === undefined) paidClient = await createPaidClient(opts); return paidClient; };
const paidOnly = (name, price, path, client) => `${name} is paid only: set SMARTPERPS_X402_PRIVATE_KEY (Base USDC) or call ${API_BASE}${path} with any x402 client (${price}).${client?.error ? ` (${client.error})` : ''}`;
export async function handle(m, opts = {}) {
if (!m || m.jsonrpc !== '2.0' || typeof m.method !== 'string') return { jsonrpc: '2.0', id: m?.id ?? null, error: { code: -32600, message: 'Invalid Request' } };
const note = m.id === undefined || m.id === null;
const ok = (result) => (note ? null : { jsonrpc: '2.0', id: m.id, result });
const err = (text) => ok({ isError: true, content: [{ type: 'text', text }] });
const callPaid = async (client, tier, input, summarize) => {
try {
const r = await client.call(tier, input);
if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarize(r.result) }], structuredContent: { ...r.result, payer: client.payer }, isError: false });
return err(`${r.error}${r.detail ? `: ${r.detail}` : ''}`);
} catch (e) { return err(`paid call failed: ${String(e?.message || e).slice(0, 200)}`); }
};
switch (m.method) {
case 'initialize': {
const client = await paid(opts);
const mode = client && !client.error ? `Paid mode: full data from ${client.apiBase} over x402.` : `Free preview mode; notional, flows and scans at ${API_BASE} (x402).`;
return ok({ protocolVersion: PROTOCOLS.includes(m.params?.protocolVersion) ? m.params.protocolVersion : '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'smartperps', title: 'SmartPerps', version: VERSION }, instructions: `Call smart_money_positioning with a Hyperliquid ticker for the smart-money cohort read. ${mode} Not financial advice.` });
}
case 'ping': return ok({});
case 'tools/list': return ok({ tools: TOOLS });
case 'tools/call': {
const name = m.params?.name;
if (!TOOLS.some((t) => t.name === name)) return note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32602, message: `Unknown tool: ${name}` } };
const args = m.params?.arguments || {};
const client = await paid(opts);
if (name === 'smart_money_scan') {
if (!client || client.error) return err(paidOnly(name, PRICES.scan, '/v1/scan?limit=10', client));
return callPaid(client, 'scan', { limit: args.limit === undefined ? 10 : Number(args.limit) }, summarizeScan);
}
if (name === 'hyperliquid_wallet_profile') {
const a = toAddress(args); if (a.error) return err(`bad_request: ${a.error}`);
if (!client || client.error) return err(paidOnly(name, PRICES.wallet, `/v1/wallet?address=${a.address}`, client));
return callPaid(client, 'wallet', { address: a.address }, summarizeWallet);
}
const t = toSymbol(args);
if (t.error) return err(`bad_request: ${t.error}`);
let fallbackNote = '';
if (client?.error) fallbackNote = client.error;
else if (client) {
try {
const r = await client.call('coin', { symbol: t.symbol });
if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarizeCoin(r.result) }], structuredContent: { ...r.result, payer: client.payer, ...(r.paymentResponse ? { payment_response: r.paymentResponse } : {}) }, isError: false });
fallbackNote = `Paid call failed (${r.error}${r.detail ? `: ${r.detail}` : ''}); showing the free preview.`;
} catch (e) { fallbackNote = `Paid call failed (${String(e?.message || e).slice(0, 200)}); showing the free preview.`; }
}
const r = await preview(t.symbol, { fetchImpl: opts.fetchImpl, apiBase: opts.apiBase || process.env.SMARTPERPS_API_URL || API_BASE });
if (r.error) return err(`${r.error}: ${r.message}${fallbackNote ? ` (${fallbackNote})` : ''}`);
delete r.full;
return ok({ content: [{ type: 'text', text: summarizePreview(r, fallbackNote) }], structuredContent: fallbackNote ? { ...r, paid_error: fallbackNote } : r, isError: false });
}
default:
return m.method.startsWith('notifications/') || note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32601, message: `Method not found: ${m.method}` } };
}
}
export function serve({ input = process.stdin, output = process.stdout } = {}) {
const rl = createInterface({ input, crlfDelay: Infinity });
const write = (o) => output.write(JSON.stringify(o) + '\n');
rl.on('line', async (l) => {
if (!l.trim()) return;
let msg; try { msg = JSON.parse(l); } catch { return write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); }
const batch = Array.isArray(msg);
const replies = (await Promise.all((batch ? msg : [msg]).map((x) => handle(x)))).filter(Boolean);
if (replies.length) write(batch ? replies : replies[0]);
});
return rl;
}
const self = process.argv[1] || '';
if (import.meta.url === `file://${self}` || /smartperps(-mcp)?$/.test(self)) serve();
agentgate-source: code mirror on Cloudflare Pages. All repos