#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Database from "better-sqlite3";
import { LLMClient } from "@blockrun/llm";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
formatUnits,
encodeFunctionData,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, baseSepolia } from "viem/chains";
import {
toCoinbaseSmartAccount,
createBundlerClient,
type SmartAccount,
} from "viem/account-abstraction";
// ============================================================================
// Constants
// ============================================================================
const VERSION = "0.2.0"; // Smart Wallet support
// Get Paymaster URL from environment (for gasless transactions)
function getPaymasterUrl(): string | undefined {
return process.env.PAYMASTER_URL || process.env.CDP_PAYMASTER_URL;
}
// Detect if we're on testnet based on Paymaster URL
function isTestnet(): boolean {
const paymasterUrl = getPaymasterUrl();
return paymasterUrl?.includes("base-sepolia") ?? false;
}
// Get the appropriate chain configuration
function getChainConfig() {
const testnet = isTestnet();
return {
chain: testnet ? baseSepolia : base,
chainId: testnet ? 84532 : 8453,
usdc: testnet
? "0x036CbD53842c5426634e7929541eC2318f3dCF7e" // Base Sepolia USDC
: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base Mainnet USDC
weth: testnet
? "0x4200000000000000000000000000000000000006" // Same on both
: "0x4200000000000000000000000000000000000006",
rpcUrl: testnet
? "https://sepolia.base.org"
: "https://mainnet.base.org",
};
}
// Legacy constants (for backwards compatibility)
const BASE_CHAIN_ID = 8453;
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
// Check if Smart Wallet mode is enabled
function isSmartWalletEnabled(): boolean {
return !!getPaymasterUrl();
}
// Risk limits (hardcoded for safety)
const RISK_LIMITS = {
maxPositionSize: 0.15, // 15% max per position
maxTotalExposure: 0.50, // 50% max total
maxDailyLoss: 0.05, // 5% daily loss limit
minCashReserve: 0.50, // 50% min cash
stopLossPercent: 0.15, // 15% stop loss
};
// ============================================================================
// Database Setup
// ============================================================================
function getDataDir(): string {
const dataDir = path.join(os.homedir(), ".blockrun", "alpha");
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
return dataDir;
}
function initDatabase(): Database.Database {
const dbPath = path.join(getDataDir(), "alpha.db");
const db = new Database(dbPath);
// Create tables
db.exec(`
-- Portfolio positions
CREATE TABLE IF NOT EXISTS positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
amount REAL NOT NULL,
entry_price REAL NOT NULL,
entry_time TEXT NOT NULL,
notes TEXT
);
-- Trade history
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
side TEXT NOT NULL,
amount REAL NOT NULL,
price REAL NOT NULL,
timestamp TEXT NOT NULL,
tx_hash TEXT,
pnl REAL,
notes TEXT
);
-- Trade memory for vector search
CREATE TABLE IF NOT EXISTS trade_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
symbol TEXT NOT NULL,
action TEXT NOT NULL,
reasoning TEXT NOT NULL,
outcome TEXT,
pnl_percent REAL,
tags TEXT,
embedding BLOB
);
-- Daily PnL tracking
CREATE TABLE IF NOT EXISTS daily_pnl (
date TEXT PRIMARY KEY,
starting_value REAL NOT NULL,
ending_value REAL,
pnl_percent REAL
);
`);
return db;
}
// ============================================================================
// Technical Analysis Functions
// ============================================================================
function calculateEMA(prices: number[], period: number): number[] {
const k = 2 / (period + 1);
const ema: number[] = [prices[0]];
for (let i = 1; i < prices.length; i++) {
ema.push(prices[i] * k + ema[i - 1] * (1 - k));
}
return ema;
}
function calculateRSI(prices: number[], period: number = 14): number {
if (prices.length < period + 1) return 50;
let gains = 0, losses = 0;
for (let i = prices.length - period; i < prices.length; i++) {
const change = prices[i] - prices[i - 1];
if (change > 0) gains += change;
else losses -= change;
}
const avgGain = gains / period;
const avgLoss = losses / period;
if (avgLoss === 0) return 100;
const rs = avgGain / avgLoss;
return 100 - (100 / (1 + rs));
}
function calculateMACD(prices: number[]): { value: number; signal: number; histogram: number } {
if (prices.length < 26) {
return { value: 0, signal: 0, histogram: 0 };
}
const ema12 = calculateEMA(prices, 12);
const ema26 = calculateEMA(prices, 26);
const macdLine = ema12.map((v, i) => v - ema26[i]);
const signalLine = calculateEMA(macdLine.slice(-9), 9);
const value = macdLine[macdLine.length - 1];
const signal = signalLine[signalLine.length - 1];
return {
value,
signal,
histogram: value - signal
};
}
// ============================================================================
// API Functions
// ============================================================================
// Map common symbols to CoinGecko IDs
const COINGECKO_IDS: Record<string, string> = {
"BTC": "bitcoin",
"ETH": "ethereum",
"SOL": "solana",
"AVAX": "avalanche-2",
"MATIC": "matic-network",
"LINK": "chainlink",
"UNI": "uniswap",
"AAVE": "aave",
"ARB": "arbitrum",
"OP": "optimism",
"PEPE": "pepe",
"DOGE": "dogecoin",
"SHIB": "shiba-inu",
};
async function fetchCoinGeckoPrices(symbol: string, days: number = 7): Promise<number[]> {
const coinId = COINGECKO_IDS[symbol.toUpperCase()] || symbol.toLowerCase();
const url = `https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`CoinGecko API error: ${response.status} - Try using full coin ID`);
}
const data = await response.json();
return data.prices.map((p: number[]) => p[1]); // [timestamp, price]
}
async function fetchDexScreenerToken(tokenAddress: string): Promise<any> {
const url = `https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`DexScreener API error: ${response.status}`);
}
return response.json();
}
async function fetchDexScreenerSearch(query: string): Promise<any> {
const url = `https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(query)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`DexScreener API error: ${response.status}`);
}
return response.json();
}
// DEX Aggregator Quote (supports multiple aggregators)
async function fetchSwapQuote(
sellToken: string,
buyToken: string,
sellAmount: string,
chainId: number = BASE_CHAIN_ID
): Promise<{ quote: any; aggregator: string }> {
// Try 1inch first (if API key available)
if (process.env.ONEINCH_API_KEY) {
try {
const url = `https://api.1inch.dev/swap/v6.0/${chainId}/quote?src=${sellToken}&dst=${buyToken}&amount=${sellAmount}`;
const response = await fetch(url, {
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${process.env.ONEINCH_API_KEY}`,
},
});
if (response.ok) {
const data = await response.json();
return { quote: data, aggregator: "1inch" };
}
} catch {
// Fall through to next aggregator
}
}
// Try 0x (if API key available)
if (process.env.ZEROX_API_KEY) {
const url = `https://api.0x.org/swap/v1/quote?sellToken=${sellToken}&buyToken=${buyToken}&sellAmount=${sellAmount}&chainId=${chainId}`;
const response = await fetch(url, {
headers: {
"0x-api-key": process.env.ZEROX_API_KEY,
},
});
if (response.ok) {
const data = await response.json();
return { quote: data, aggregator: "0x" };
}
}
// Try Paraswap (no API key required)
try {
const url = `https://apiv5.paraswap.io/prices?srcToken=${sellToken}&destToken=${buyToken}&amount=${sellAmount}&srcDecimals=6&destDecimals=18&side=SELL&network=${chainId}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
return { quote: data, aggregator: "paraswap" };
}
} catch {
// Fall through
}
throw new Error("No swap aggregator available. Set ONEINCH_API_KEY or ZEROX_API_KEY environment variable.");
}
// ============================================================================
// Wallet Functions
// ============================================================================
function getSessionPath(): string {
return path.join(os.homedir(), ".blockrun", ".session");
}
function loadWallet(): { address: string; privateKey: string } | null {
const sessionPath = getSessionPath();
if (!fs.existsSync(sessionPath)) {
return null;
}
const privateKey = fs.readFileSync(sessionPath, "utf-8").trim();
if (!privateKey.startsWith("0x")) {
return null;
}
const account = privateKeyToAccount(privateKey as Hex);
return { address: account.address, privateKey };
}
// Base RPC client (dynamic chain based on Paymaster URL)
const chainConfig = getChainConfig();
const publicClient = createPublicClient({
chain: chainConfig.chain,
transport: http(chainConfig.rpcUrl),
});
// ERC20 ABI for balance and approve
const ERC20_ABI = parseAbi([
"function balanceOf(address) view returns (uint256)",
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
]);
// Get USDC balance for an address
async function getUsdcBalance(address: string): Promise<number> {
try {
const config = getChainConfig();
const balance = await publicClient.readContract({
address: config.usdc as Hex,
abi: ERC20_ABI,
functionName: "balanceOf",
args: [address as Hex],
});
return Number(formatUnits(balance, 6)); // USDC has 6 decimals
} catch {
return 0;
}
}
// Get ETH balance for an address
async function getEthBalance(address: string): Promise<number> {
try {
const balance = await publicClient.getBalance({ address: address as Hex });
return Number(formatUnits(balance, 18));
} catch {
return 0;
}
}
// Execute a swap transaction
async function executeSwap(
wallet: { privateKey: string },
to: string,
data: string,
value: string = "0"
): Promise<string> {
const account = privateKeyToAccount(wallet.privateKey as Hex);
const config = getChainConfig();
const walletClient = createWalletClient({
account,
chain: config.chain,
transport: http(config.rpcUrl),
});
const hash = await walletClient.sendTransaction({
to: to as Hex,
data: data as Hex,
value: BigInt(value),
});
return hash;
}
// Approve token spending
async function approveToken(
wallet: { privateKey: string },
tokenAddress: string,
spenderAddress: string,
amount: bigint
): Promise<string> {
const account = privateKeyToAccount(wallet.privateKey as Hex);
const config = getChainConfig();
const walletClient = createWalletClient({
account,
chain: config.chain,
transport: http(config.rpcUrl),
});
const hash = await walletClient.writeContract({
address: tokenAddress as Hex,
abi: ERC20_ABI,
functionName: "approve",
args: [spenderAddress as Hex, amount],
});
return hash;
}
// ============================================================================
// Smart Wallet Functions (Gasless with Paymaster)
// ============================================================================
// Cache for smart account
let cachedSmartAccount: SmartAccount | null = null;
// Create Smart Wallet from EOA signer
async function getSmartAccount(privateKey: string): Promise<SmartAccount> {
if (cachedSmartAccount) return cachedSmartAccount;
const owner = privateKeyToAccount(privateKey as Hex);
cachedSmartAccount = await toCoinbaseSmartAccount({
client: publicClient,
owners: [owner],
version: "1", // Coinbase Smart Wallet v1
});
return cachedSmartAccount;
}
// Get Smart Wallet address
async function getSmartWalletAddress(privateKey: string): Promise<string> {
const smartAccount = await getSmartAccount(privateKey);
return smartAccount.address;
}
// Execute swap via Smart Wallet with Paymaster (gasless)
async function executeSwapWithSmartWallet(
wallet: { privateKey: string },
to: string,
data: string,
value: string = "0"
): Promise<string> {
const paymasterUrl = getPaymasterUrl();
if (!paymasterUrl) {
throw new Error("PAYMASTER_URL not set. Cannot use Smart Wallet without Paymaster.");
}
const smartAccount = await getSmartAccount(wallet.privateKey);
const config = getChainConfig();
const bundlerClient = createBundlerClient({
account: smartAccount,
chain: config.chain,
transport: http(paymasterUrl),
paymaster: true, // Enable paymaster for gas sponsorship
});
const userOpHash = await bundlerClient.sendUserOperation({
calls: [
{
to: to as Hex,
data: data as Hex,
value: BigInt(value),
},
],
});
// Wait for the user operation to be included
const receipt = await bundlerClient.waitForUserOperationReceipt({
hash: userOpHash,
});
return receipt.receipt.transactionHash;
}
// Approve token via Smart Wallet (gasless)
async function approveTokenWithSmartWallet(
wallet: { privateKey: string },
tokenAddress: string,
spenderAddress: string,
amount: bigint
): Promise<string> {
const approveData = encodeFunctionData({
abi: ERC20_ABI,
functionName: "approve",
args: [spenderAddress as Hex, amount],
});
return executeSwapWithSmartWallet(wallet, tokenAddress, approveData, "0");
}
// Get the appropriate wallet address (EOA or Smart Wallet)
async function getActiveWalletAddress(wallet: { address: string; privateKey: string }): Promise<string> {
if (isSmartWalletEnabled()) {
return getSmartWalletAddress(wallet.privateKey);
}
return wallet.address;
}
// BlockRun LLM Client (lazy initialized)
let blockrunClient: LLMClient | null = null;
function getBlockRunClient(): LLMClient | null {
if (blockrunClient) return blockrunClient;
// Try to load private key from session file (same as blockrun-mcp)
const sessionPath = path.join(os.homedir(), ".blockrun", ".session");
if (fs.existsSync(sessionPath)) {
const privateKey = fs.readFileSync(sessionPath, "utf-8").trim();
if (privateKey.startsWith("0x")) {
blockrunClient = new LLMClient({ privateKey });
return blockrunClient;
}
}
return null;
}
// ============================================================================
// Simple Vector Similarity (cosine similarity without external deps)
// ============================================================================
function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Simple text to vector (bag of words style - MVP, replace with real embeddings later)
function textToVector(text: string): number[] {
const words = text.toLowerCase().split(/\s+/);
const vector = new Array(384).fill(0);
for (const word of words) {
let hash = 0;
for (let i = 0; i < word.length; i++) {
hash = ((hash << 5) - hash) + word.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash) % 384;
vector[index] += 1;
}
// Normalize
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
if (norm > 0) {
for (let i = 0; i < vector.length; i++) {
vector[i] /= norm;
}
}
return vector;
}
// ============================================================================
// MCP Server Setup
// ============================================================================
const server = new McpServer({
name: "alpha",
version: VERSION,
});
const db = initDatabase();
// ============================================================================
// Tool: alpha_signal
// ============================================================================
server.tool(
"alpha_signal",
"Get technical analysis signals (RSI, MACD, EMA) for a cryptocurrency",
{
symbol: z.string().describe("Trading symbol (e.g., 'ETH', 'BTC')"),
days: z.number().optional().describe("Days of history for analysis: 1, 7, 14, 30 (default: 7)"),
},
async ({ symbol, days = 7 }) => {
try {
const prices = await fetchCoinGeckoPrices(symbol, days);
const rsi = calculateRSI(prices);
const macd = calculateMACD(prices);
const ema9 = calculateEMA(prices, 9);
const ema21 = calculateEMA(prices, 21);
const ema50 = calculateEMA(prices, 50);
const currentPrice = prices[prices.length - 1];
const ema9Current = ema9[ema9.length - 1];
const ema21Current = ema21[ema21.length - 1];
const ema50Current = ema50[ema50.length - 1];
// Generate recommendation
let bullishSignals = 0;
let bearishSignals = 0;
// RSI
if (rsi < 30) bullishSignals += 2;
else if (rsi < 40) bullishSignals += 1;
else if (rsi > 70) bearishSignals += 2;
else if (rsi > 60) bearishSignals += 1;
// MACD
if (macd.histogram > 0 && macd.value > macd.signal) bullishSignals += 1;
if (macd.histogram < 0 && macd.value < macd.signal) bearishSignals += 1;
// EMA alignment
if (ema9Current > ema21Current && ema21Current > ema50Current) bullishSignals += 2;
if (ema9Current < ema21Current && ema21Current < ema50Current) bearishSignals += 2;
// Price vs EMAs
if (currentPrice > ema9Current && currentPrice > ema21Current) bullishSignals += 1;
if (currentPrice < ema9Current && currentPrice < ema21Current) bearishSignals += 1;
const totalSignals = bullishSignals + bearishSignals;
const confidence = totalSignals > 0 ? Math.max(bullishSignals, bearishSignals) / totalSignals : 0.5;
let recommendation = "NEUTRAL";
if (bullishSignals > bearishSignals + 2) recommendation = "STRONG_BUY";
else if (bullishSignals > bearishSignals) recommendation = "BUY";
else if (bearishSignals > bullishSignals + 2) recommendation = "STRONG_SELL";
else if (bearishSignals > bullishSignals) recommendation = "SELL";
const result = {
symbol: symbol.toUpperCase(),
days,
price: currentPrice,
indicators: {
rsi: Math.round(rsi * 100) / 100,
macd: {
value: Math.round(macd.value * 1000) / 1000,
signal: Math.round(macd.signal * 1000) / 1000,
histogram: Math.round(macd.histogram * 1000) / 1000,
},
ema: {
ema9: Math.round(ema9Current * 100) / 100,
ema21: Math.round(ema21Current * 100) / 100,
ema50: Math.round(ema50Current * 100) / 100,
}
},
signals: {
bullish: bullishSignals,
bearish: bearishSignals,
},
recommendation,
confidence: Math.round(confidence * 100) / 100,
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_dex
// ============================================================================
server.tool(
"alpha_dex",
"Get DEX market data for a token (price, volume, liquidity)",
{
token: z.string().describe("Token address or symbol (e.g., 'ETH', '0x...')"),
},
async ({ token }) => {
try {
let data;
// Check if it's an address or symbol
if (token.startsWith("0x")) {
data = await fetchDexScreenerToken(token);
} else {
data = await fetchDexScreenerSearch(token);
}
if (!data.pairs || data.pairs.length === 0) {
return {
content: [{ type: "text", text: `No DEX data found for ${token}` }],
};
}
// Get the most liquid pair (prioritize Base/Ethereum, but fall back to any chain)
let pairs = data.pairs
.filter((p: any) => p.chainId === "base" || p.chainId === "ethereum")
.sort((a: any, b: any) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0));
// Fall back to all pairs if no Base/Ethereum pairs found
if (pairs.length === 0) {
pairs = data.pairs.sort((a: any, b: any) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0));
}
if (pairs.length === 0) {
return {
content: [{ type: "text", text: `No DEX pairs found for ${token}` }],
};
}
const topPair = pairs[0];
const result = {
token: topPair.baseToken?.symbol || token,
address: topPair.baseToken?.address,
chain: topPair.chainId,
price: parseFloat(topPair.priceUsd || "0"),
priceChange: {
h1: topPair.priceChange?.h1 || 0,
h24: topPair.priceChange?.h24 || 0,
},
volume24h: topPair.volume?.h24 || 0,
liquidity: topPair.liquidity?.usd || 0,
fdv: topPair.fdv || 0,
dex: topPair.dexId,
pairAddress: topPair.pairAddress,
topPairs: pairs.slice(0, 5).map((p: any) => ({
dex: p.dexId,
chain: p.chainId,
liquidity: p.liquidity?.usd || 0,
volume24h: p.volume?.h24 || 0,
})),
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_sentiment
// ============================================================================
server.tool(
"alpha_sentiment",
"Get market sentiment from X/Twitter via Grok (requires BlockRun wallet with USDC)",
{
query: z.string().describe("Search query (e.g., 'ETH sentiment', '@VitalikButerin', 'crypto news')"),
max_results: z.number().optional().describe("Max results to return (default: 10)"),
},
async ({ query, max_results = 10 }) => {
try {
const client = getBlockRunClient();
if (!client) {
return {
content: [{
type: "text",
text: `Sentiment analysis requires BlockRun wallet.\n\nSetup:\n1. Run: claude mcp add blockrun npx @blockrun/mcp\n2. Run: blockrun_setup to create wallet\n3. Fund wallet with USDC on Base\n\nAlternatively, use alpha_signal for free technical analysis.`
}],
};
}
// Call Grok with Twitter search enabled
const response = await client.chat("xai/grok-3", query, {
system: `You are a crypto market sentiment analyst. Analyze X/Twitter posts about the query.
Return a JSON object with:
- sentiment: "bullish" | "bearish" | "neutral"
- score: -1.0 to 1.0
- summary: brief analysis (2-3 sentences)
- key_mentions: array of notable accounts/posts
- trending_topics: related trending topics
Max results to analyze: ${max_results}`,
search: true,
} as any);
// Try to parse as JSON, otherwise return raw response
let parsed;
try {
// Extract JSON from response if wrapped in markdown
const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/) ||
response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
}
} catch {
// Return raw response if not JSON
parsed = { raw_response: response };
}
return {
content: [{
type: "text",
text: JSON.stringify({
query,
source: "X/Twitter via Grok",
...parsed,
}, null, 2)
}],
};
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
// Check for insufficient balance
if (msg.includes("402") || msg.includes("balance") || msg.includes("insufficient")) {
return {
content: [{
type: "text",
text: `Insufficient USDC balance. Fund your wallet:\n1. Run: blockrun_wallet to get address\n2. Send USDC on Base network\n\nError: ${msg}`
}],
isError: true,
};
}
return {
content: [{ type: "text", text: `Error: ${msg}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_swap
// ============================================================================
server.tool(
"alpha_swap",
"Get token swap quotes or execute swaps on Base chain",
{
fromToken: z.string().describe("Token to sell (e.g., 'USDC', 'ETH', or address)"),
toToken: z.string().describe("Token to buy (e.g., 'ETH', 'USDC', or address)"),
amount: z.string().describe("Amount to sell (in token units, e.g., '100' for 100 USDC)"),
execute: z.boolean().optional().describe("Set to true to execute the swap (default: false, quote only)"),
slippage: z.number().optional().describe("Slippage tolerance in percent (default: 0.5)"),
},
async ({ fromToken, toToken, amount, execute = false, slippage = 0.5 }) => {
try {
const wallet = loadWallet();
if (!wallet) {
return {
content: [{
type: "text",
text: "No wallet found. Please setup BlockRun wallet first:\n1. Run: claude mcp add blockrun npx @blockrun/mcp\n2. Use blockrun_setup to create wallet"
}],
};
}
// Resolve token addresses (dynamic based on network)
const config = getChainConfig();
const tokenMap: Record<string, string> = {
"USDC": config.usdc,
"ETH": config.weth,
"WETH": config.weth,
};
const sellToken = tokenMap[fromToken.toUpperCase()] || fromToken;
const buyToken = tokenMap[toToken.toUpperCase()] || toToken;
// Convert amount to wei (assuming 6 decimals for USDC, 18 for ETH)
const decimals = fromToken.toUpperCase() === "USDC" ? 6 : 18;
const sellAmount = (parseFloat(amount) * Math.pow(10, decimals)).toString();
// Determine which wallet address to use (Smart Wallet or EOA)
const useSmartWallet = isSmartWalletEnabled();
const activeAddress = useSmartWallet
? await getSmartWalletAddress(wallet.privateKey)
: wallet.address;
// Check balance before proceeding
if (execute) {
const balance = fromToken.toUpperCase() === "USDC"
? await getUsdcBalance(activeAddress)
: await getEthBalance(activeAddress);
if (balance < parseFloat(amount)) {
return {
content: [{
type: "text",
text: `Insufficient balance. Have: ${balance.toFixed(4)} ${fromToken}, Need: ${amount} ${fromToken}\nWallet: ${activeAddress} (${useSmartWallet ? "Smart Wallet" : "EOA"})`
}],
isError: true,
};
}
}
// Get quote from available aggregator
const { quote, aggregator } = await fetchSwapQuote(sellToken, buyToken, sellAmount);
// Calculate output amount based on aggregator response format
const toDecimals = toToken.toUpperCase() === "USDC" ? 6 : 18;
let outputAmount: string;
let gas: string | undefined;
let txTo: string | undefined;
let txData: string | undefined;
let txValue: string | undefined;
if (aggregator === "1inch") {
outputAmount = (parseFloat(quote.dstAmount) / Math.pow(10, toDecimals)).toFixed(6);
gas = quote.gas;
txTo = quote.tx?.to;
txData = quote.tx?.data;
txValue = quote.tx?.value;
} else if (aggregator === "0x") {
outputAmount = (parseFloat(quote.buyAmount) / Math.pow(10, toDecimals)).toFixed(6);
gas = quote.estimatedGas;
txTo = quote.to;
txData = quote.data;
txValue = quote.value;
} else {
// paraswap - need to build tx separately
outputAmount = (parseFloat(quote.priceRoute?.destAmount || "0") / Math.pow(10, toDecimals)).toFixed(6);
gas = quote.priceRoute?.gasCost;
// Paraswap requires a separate call to build tx
}
// Execute the swap if requested
if (execute) {
if (!txTo || !txData) {
return {
content: [{
type: "text",
text: `Aggregator ${aggregator} does not support execution yet. Use 1inch or 0x with API key.`
}],
isError: true,
};
}
// Approve token if selling ERC20 (not ETH)
if (fromToken.toUpperCase() !== "ETH" && fromToken.toUpperCase() !== "WETH") {
if (useSmartWallet) {
await approveTokenWithSmartWallet(
wallet,
sellToken,
txTo,
BigInt(sellAmount)
);
} else {
await approveToken(
wallet,
sellToken,
txTo,
BigInt(sellAmount)
);
// Wait for approval (simplified - in production would wait for confirmation)
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
// Execute the swap (use Smart Wallet if enabled for gasless)
const txHash = useSmartWallet
? await executeSwapWithSmartWallet(wallet, txTo, txData, txValue || "0")
: await executeSwap(wallet, txTo, txData, txValue || "0");
// Record trade in database
db.prepare(
"INSERT INTO trades (symbol, side, amount, price, timestamp, tx_hash, notes) VALUES (?, ?, ?, ?, ?, ?, ?)"
).run(
toToken.toUpperCase(),
"buy",
parseFloat(outputAmount),
parseFloat(amount) / parseFloat(outputAmount),
new Date().toISOString(),
txHash,
`Swapped ${amount} ${fromToken} for ${outputAmount} ${toToken} via ${aggregator}`
);
return {
content: [{
type: "text",
text: JSON.stringify({
status: "executed",
txHash,
explorer: `https://basescan.org/tx/${txHash}`,
aggregator,
walletType: useSmartWallet ? "Smart Wallet (gasless)" : "EOA",
walletAddress: activeAddress,
from: { token: fromToken, amount },
to: { token: toToken, amount: outputAmount },
}, null, 2)
}],
};
}
// Quote only
const result = {
status: "quote_ready",
aggregator,
walletType: useSmartWallet ? "Smart Wallet (gasless)" : "EOA",
walletAddress: useSmartWallet ? await getSmartWalletAddress(wallet.privateKey) : wallet.address,
from: {
token: fromToken,
amount: amount,
address: sellToken,
},
to: {
token: toToken,
amount: outputAmount,
address: buyToken,
},
gas: useSmartWallet ? "sponsored by Paymaster" : gas,
slippage: `${slippage}%`,
note: useSmartWallet
? "Set execute=true to execute this gasless swap"
: "Set execute=true to execute this swap (requires ETH for gas)",
};
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_portfolio
// ============================================================================
server.tool(
"alpha_portfolio",
"Manage and view portfolio positions",
{
action: z.enum(["status", "add", "remove", "history"]).describe("Action to perform"),
symbol: z.string().optional().describe("Token symbol (for add/remove)"),
amount: z.number().optional().describe("Amount (for add/remove)"),
price: z.number().optional().describe("Entry price (for add)"),
},
async ({ action, symbol, amount, price }) => {
try {
if (action === "status") {
const positions = db.prepare("SELECT * FROM positions").all();
const trades = db.prepare("SELECT * FROM trades ORDER BY timestamp DESC LIMIT 10").all();
// Calculate total value (simplified - would need real-time prices)
let totalValue = 0;
const positionsWithValue = positions.map((p: any) => {
const value = p.amount * p.entry_price; // Simplified
totalValue += value;
return {
...p,
currentValue: value,
pnl: 0, // Would calculate with real prices
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
totalValue,
positions: positionsWithValue,
recentTrades: trades,
riskLimits: RISK_LIMITS,
}, null, 2)
}],
};
}
if (action === "add" && symbol && amount && price) {
db.prepare(
"INSERT INTO positions (symbol, amount, entry_price, entry_time) VALUES (?, ?, ?, ?)"
).run(symbol.toUpperCase(), amount, price, new Date().toISOString());
return {
content: [{ type: "text", text: `Added position: ${amount} ${symbol} at $${price}` }],
};
}
if (action === "remove" && symbol) {
const result = db.prepare("DELETE FROM positions WHERE symbol = ?").run(symbol.toUpperCase());
return {
content: [{ type: "text", text: `Removed ${result.changes} position(s) for ${symbol}` }],
};
}
if (action === "history") {
const trades = db.prepare("SELECT * FROM trades ORDER BY timestamp DESC LIMIT 50").all();
return {
content: [{ type: "text", text: JSON.stringify(trades, null, 2) }],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_memory
// ============================================================================
server.tool(
"alpha_memory",
"Store and retrieve trading decisions using semantic search",
{
action: z.enum(["find", "store", "stats"]).describe("Action: find similar trades, store new trade, or get stats"),
query: z.string().optional().describe("Search query for finding similar trades"),
trade: z.object({
symbol: z.string(),
action: z.string(),
reasoning: z.string(),
outcome: z.string().optional(),
pnl_percent: z.number().optional(),
tags: z.array(z.string()).optional(),
}).optional().describe("Trade data to store"),
},
async ({ action, query, trade }) => {
try {
if (action === "find" && query) {
const queryVector = textToVector(query);
const memories = db.prepare("SELECT * FROM trade_memory").all() as any[];
const results = memories
.map((m) => {
const embedding = m.embedding ? JSON.parse(m.embedding) : textToVector(m.reasoning);
const similarity = cosineSimilarity(queryVector, embedding);
return { ...m, similarity };
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 5);
return {
content: [{
type: "text",
text: JSON.stringify({
query,
similar_trades: results.map(r => ({
id: r.id,
date: r.timestamp,
symbol: r.symbol,
action: r.action,
reasoning: r.reasoning,
outcome: r.outcome,
pnl: r.pnl_percent ? `${r.pnl_percent}%` : null,
similarity: Math.round(r.similarity * 100) / 100,
}))
}, null, 2)
}],
};
}
if (action === "store" && trade) {
const embedding = textToVector(trade.reasoning);
db.prepare(`
INSERT INTO trade_memory (timestamp, symbol, action, reasoning, outcome, pnl_percent, tags, embedding)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
new Date().toISOString(),
trade.symbol.toUpperCase(),
trade.action,
trade.reasoning,
trade.outcome || null,
trade.pnl_percent || null,
trade.tags ? JSON.stringify(trade.tags) : null,
JSON.stringify(embedding)
);
return {
content: [{ type: "text", text: `Stored trade memory for ${trade.symbol}: ${trade.action}` }],
};
}
if (action === "stats") {
const total = db.prepare("SELECT COUNT(*) as count FROM trade_memory").get() as any;
const wins = db.prepare("SELECT COUNT(*) as count FROM trade_memory WHERE pnl_percent > 0").get() as any;
const losses = db.prepare("SELECT COUNT(*) as count FROM trade_memory WHERE pnl_percent < 0").get() as any;
const avgPnl = db.prepare("SELECT AVG(pnl_percent) as avg FROM trade_memory WHERE pnl_percent IS NOT NULL").get() as any;
return {
content: [{
type: "text",
text: JSON.stringify({
total_trades: total.count,
wins: wins.count,
losses: losses.count,
win_rate: total.count > 0 ? `${Math.round((wins.count / total.count) * 100)}%` : "N/A",
avg_pnl: avgPnl.avg ? `${Math.round(avgPnl.avg * 100) / 100}%` : "N/A",
}, null, 2)
}],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_risk
// ============================================================================
server.tool(
"alpha_risk",
"Check if a proposed trade passes risk management rules",
{
action: z.enum(["check", "limits"]).describe("Action: check a trade or view limits"),
proposed_trade: z.object({
symbol: z.string(),
side: z.enum(["buy", "sell"]),
amount_usd: z.number(),
}).optional().describe("Proposed trade to check"),
},
async ({ action, proposed_trade }) => {
try {
if (action === "limits") {
// Get wallet info for limits display
const wallet = loadWallet();
let walletInfo: any = { status: "not_configured" };
if (wallet) {
const useSmartWallet = isSmartWalletEnabled();
const activeAddress = useSmartWallet
? await getSmartWalletAddress(wallet.privateKey)
: wallet.address;
const usdcBalance = await getUsdcBalance(activeAddress);
const ethBalance = await getEthBalance(activeAddress);
walletInfo = {
status: "configured",
address: activeAddress,
type: useSmartWallet ? "Smart Wallet (gasless)" : "EOA",
balances: {
usdc: usdcBalance,
eth: ethBalance,
total_usd: usdcBalance + (ethBalance * 3300), // Approximate
},
};
}
return {
content: [{
type: "text",
text: JSON.stringify({
wallet: walletInfo,
risk_limits: {
max_position_size: `${RISK_LIMITS.maxPositionSize * 100}% of portfolio`,
max_total_exposure: `${RISK_LIMITS.maxTotalExposure * 100}% of portfolio`,
max_daily_loss: `${RISK_LIMITS.maxDailyLoss * 100}% triggers trading halt`,
min_cash_reserve: `${RISK_LIMITS.minCashReserve * 100}% must remain in cash`,
stop_loss: `${RISK_LIMITS.stopLossPercent * 100}% per position`,
},
note: walletInfo.status === "not_configured"
? "Run 'blockrun_setup' to create wallet, then send USDC on Base"
: walletInfo.balances.total_usd < 10
? `Send USDC to ${walletInfo.address} on Base network to start trading`
: "Wallet ready for trading",
}, null, 2)
}],
};
}
if (action === "check" && proposed_trade) {
// Get wallet for balance check
const wallet = loadWallet();
if (!wallet) {
return {
content: [{
type: "text",
text: "No wallet found. Please setup BlockRun wallet first."
}],
isError: true,
};
}
// Get the active wallet address (Smart Wallet or EOA)
const useSmartWallet = isSmartWalletEnabled();
const activeAddress = useSmartWallet
? await getSmartWalletAddress(wallet.privateKey)
: wallet.address;
// Get real USDC balance from wallet
const usdcBalance = await getUsdcBalance(activeAddress);
const ethBalance = await getEthBalance(activeAddress);
// Get current portfolio state
const positions = db.prepare("SELECT * FROM positions").all() as any[];
let totalValue = 0;
positions.forEach((p: any) => {
totalValue += p.amount * p.entry_price;
});
// Use real wallet balance as cash
const cashUsd = usdcBalance + (ethBalance * 3300); // Approximate ETH price
const portfolioTotal = totalValue + cashUsd;
const issues: string[] = [];
let approved = true;
// Check position size
const positionPercent = proposed_trade.amount_usd / portfolioTotal;
if (positionPercent > RISK_LIMITS.maxPositionSize) {
issues.push(`Position size ${(positionPercent * 100).toFixed(1)}% exceeds ${RISK_LIMITS.maxPositionSize * 100}% limit`);
approved = false;
}
// Check total exposure
const newExposure = (totalValue + proposed_trade.amount_usd) / portfolioTotal;
if (newExposure > RISK_LIMITS.maxTotalExposure) {
issues.push(`Total exposure would be ${(newExposure * 100).toFixed(1)}%, exceeds ${RISK_LIMITS.maxTotalExposure * 100}% limit`);
approved = false;
}
// Check cash reserve
const remainingCash = cashUsd - proposed_trade.amount_usd;
const cashPercent = remainingCash / portfolioTotal;
if (cashPercent < RISK_LIMITS.minCashReserve) {
issues.push(`Cash would drop to ${(cashPercent * 100).toFixed(1)}%, below ${RISK_LIMITS.minCashReserve * 100}% minimum`);
approved = false;
}
return {
content: [{
type: "text",
text: JSON.stringify({
approved,
proposed_trade,
portfolio: {
total_value: portfolioTotal,
current_exposure: `${((totalValue / portfolioTotal) * 100).toFixed(1)}%`,
cash_usdc: usdcBalance,
cash_eth: ethBalance,
cash_total_usd: cashUsd,
},
wallet: activeAddress,
walletType: useSmartWallet ? "Smart Wallet (gasless)" : "EOA",
issues: issues.length > 0 ? issues : ["All risk checks passed"],
recommendation: approved ? "Trade approved" : "Trade rejected - adjust size or wait",
}, null, 2)
}],
};
}
return {
content: [{ type: "text", text: "Invalid action or missing parameters" }],
isError: true,
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
isError: true,
};
}
}
);
// ============================================================================
// Tool: alpha_extensions
// ============================================================================
const MCP_EXTENSIONS = [
{
name: "nansen",
description: "On-chain analytics: Smart Money tracking, whale movements, token holders",
install: 'claude mcp add --transport http nansen https://mcp.nansen.ai/ra/mcp --header "NANSEN-API-KEY: YOUR_KEY"',
apiKeyUrl: "https://app.nansen.ai/api?tab=api",
pricing: "Free tier available, Pro $49/month for Smart Money API",
tools: ["token_current_top_holders", "token_dex_trades", "token_who_bought_sold", "smart_traders_and_funds_*"],
useCase: "Know what whales and funds are buying before price moves",
},
{
name: "blockrun",
description: "AI model access via x402 micropayments (GPT-5, Grok, Gemini, image generation)",
install: "claude mcp add blockrun npx @blockrun/mcp",
apiKeyUrl: null,
pricing: "Pay-per-request with USDC on Base",
tools: ["blockrun_chat", "blockrun_twitter", "blockrun_image"],
useCase: "Access any AI model, real-time X/Twitter data via Grok",
},
{
name: "dune",
description: "Custom SQL queries on blockchain data",
install: "claude mcp add dune npx @duneanalytics/mcp-server",
apiKeyUrl: "https://dune.com/settings/api",
pricing: "Free tier: 2500 credits/month",
tools: ["run_query", "get_results"],
useCase: "Run custom analytics queries across chains",
},
];
server.tool(
"alpha_extensions",
"Discover additional MCP servers to extend trading capabilities (on-chain data, AI models, analytics)",
{
category: z.enum(["all", "onchain", "ai", "analytics"]).optional().describe("Filter by category"),
},
async ({ category = "all" }) => {
let extensions = MCP_EXTENSIONS;
if (category === "onchain") {
extensions = extensions.filter(e => e.name === "nansen");
} else if (category === "ai") {
extensions = extensions.filter(e => e.name === "blockrun");
} else if (category === "analytics") {
extensions = extensions.filter(e => e.name === "dune");
}
return {
content: [{
type: "text",
text: JSON.stringify({
message: "Recommended MCP extensions for trading",
extensions: extensions.map(e => ({
name: e.name,
description: e.description,
useCase: e.useCase,
pricing: e.pricing,
install: e.install,
apiKeyUrl: e.apiKeyUrl,
tools: e.tools,
})),
note: "Run the install command in terminal, then restart Claude Code",
}, null, 2)
}],
};
}
);
// ============================================================================
// Start Server
// ============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
const config = getChainConfig();
const networkMode = isTestnet() ? "TESTNET (Base Sepolia)" : "MAINNET (Base)";
const walletMode = isSmartWalletEnabled() ? "Smart Wallet (Gasless)" : "EOA Wallet";
console.error(`@blockrun/alpha MCP server v${VERSION} running`);
console.error(` Network: ${networkMode}`);
console.error(` Wallet: ${walletMode}`);
console.error(` USDC: ${config.usdc}`);
}
main().catch(console.error);