index.js•8.76 kB
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { ethers } = require("ethers");
const { z } = require("zod");
const dotenv = require("dotenv");
// Load environment variables
dotenv.config();
const WS_ENDPOINT = process.env.WS_ENDPOINT;
const RPC_ENDPOINT = process.env.RPC_ENDPOINT;
if (!WS_ENDPOINT || !RPC_ENDPOINT) {
throw new Error("WS_ENDPOINT and RPC_ENDPOINT environment variables are required");
}
// Constants
const BINANCE_DEX_ROUTER = "0x5efc784d444126ecc05f22c49ff3fbd7d9f4868a";
const TOKEN_BASKET = {
"0x55d398326f99059fF775485246999027B3197955": "USDT",
"0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d": "USDC",
"0x0000000000000000000000000000000000000000": "BNB",
};
const PRICE_CACHE_DURATION = 5000; // 5 seconds in ms
const MAX_RECORD_AGE = 3600 * 1000; // 1 hour in ms
// ABI for OrderRecord event and ERC20 token
const ORDER_ABI = [
"event OrderRecord(address inputToken, address outputToken, address sender, uint256 inputAmount, uint256 outputAmount)",
];
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
];
// Data structures
class AppContext {
constructor() {
this.trades = [];
this.tradeUsdValues = {};
this.priceCache = {};
this.tokenInfoCache = {
"0x55d398326f99059fF775485246999027B3197955": ["Tether USD", "USDT", 18],
"0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d": ["USD Coin", "USDC", 18],
"0x0000000000000000000000000000000000000000": ["Binance Coin", "BNB", 18],
};
this.startTime = Date.now();
// Periodically clean old records
setInterval(() => this.cleanup(), MAX_RECORD_AGE / 10);
}
cleanup() {
const now = Date.now();
while (this.trades.length && now - this.trades[0].timestamp > MAX_RECORD_AGE) {
const oldTrade = this.trades.shift();
this.tradeUsdValues[oldTrade.inputToken] -= oldTrade.usdValue;
if (this.tradeUsdValues[oldTrade.inputToken] <= 0) {
delete this.tradeUsdValues[oldTrade.inputToken];
}
}
}
}
// Initialize MCP server
const server = new McpServer({
name: "BinanceAlpha",
version: "1.0.0",
});
// Initialize app context
const appContext = new AppContext();
async function getUsdPrice(tokenSymbol) {
const now = Date.now();
if (appContext.priceCache[tokenSymbol] && now - appContext.priceCache[tokenSymbol][1] < PRICE_CACHE_DURATION) {
return appContext.priceCache[tokenSymbol][0];
}
try {
const response = await fetch(
`https://min-api.cryptocompare.com/data/price?fsym=${tokenSymbol}&tsyms=USD`
);
const data = await response.json();
const price = data.USD || 1.0;
appContext.priceCache[tokenSymbol] = [price, now];
return price;
} catch (error) {
console.error(`Failed to fetch price for ${tokenSymbol}:`, error);
return 1.0;
}
}
async function getTokenInfo(tokenAddress, provider) {
if (appContext.tokenInfoCache[tokenAddress]) {
return appContext.tokenInfoCache[tokenAddress];
}
try {
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
]);
const tokenInfo = [name, symbol, Number(decimals)];
appContext.tokenInfoCache[tokenAddress] = tokenInfo;
return tokenInfo;
} catch (error) {
console.error(`Failed to fetch token info for ${tokenAddress}:`, error);
return ["Unknown", "Unknown", 18];
}
}
async function processLog(log, provider) {
const inputToken = log.inputToken.toLowerCase();
if (!(inputToken in TOKEN_BASKET)) {
return;
}
const outputToken = log.outputToken.toLowerCase();
const sender = log.sender.toLowerCase();
const inputAmount = log.inputAmount;
const outputAmount = log.outputAmount;
const inputInfo = await getTokenInfo(inputToken, provider);
const outputInfo = await getTokenInfo(outputToken, provider);
const inputAmountFloat = parseFloat(ethers.formatUnits(inputAmount, inputInfo[2]));
const outputAmountFloat = parseFloat(ethers.formatUnits(outputAmount, outputInfo[2]));
const usdPrice = await getUsdPrice(TOKEN_BASKET[inputToken]);
const usdValue = inputAmountFloat * usdPrice;
const trade = {
inputToken,
outputToken,
sender,
inputAmount: inputAmountFloat,
outputAmount: outputAmountFloat,
usdValue,
timestamp: Date.now(),
};
appContext.trades.push(trade);
appContext.tradeUsdValues[outputToken] = (appContext.tradeUsdValues[outputToken] || 0) + usdValue;
}
// Tools
server.tool(
"get_top_tokens",
"Returns a markdown table of the top tokens by USD trading volume",
z.object({
limit: z.number().optional().default(10),
}),
async ({ limit }) => {
const topTokens = Object.entries(appContext.tradeUsdValues)
.sort(([, usdValueA], [, usdValueB]) => usdValueB - usdValueA)
.slice(0, limit)
.map(async ([token, usdValue]) => {
const tokenInfo = appContext.tokenInfoCache[token] || ["Unknown", "Unknown", 18];
return {
address: token,
name: tokenInfo[0],
symbol: tokenInfo[1],
usd_volume: usdValue,
};
});
const tokens = await Promise.all(topTokens);
const minutesSinceStart = (Date.now() - appContext.startTime) / 60000;
const period = `period: last ${Math.min(minutesSinceStart, MAX_RECORD_AGE / 60000).toFixed(0)} minutes`;
let markdown = `${period}\n`;
markdown += `| Symbol | USD Volume | Name | Address |\n`;
markdown += `|--------|------------|------|---------|\n`;
for (const token of tokens) {
markdown += `| ${token.symbol} | $${token.usd_volume.toFixed(2)} | ${token.name} | ${token.address} |\n`;
}
return {
content: [
{
type: "text",
text: markdown,
},
],
};
}
);
server.tool(
"get_trade_stats",
"Returns statistics about trade USD values including min, max, median, and distribution",
z.object({
buckets: z.number().optional().default(10),
}),
async ({ buckets }) => {
const usdValues = appContext.trades.map((trade) => trade.usdValue);
if (!usdValues.length) {
return {
content: [
{
type: "text",
text: "No trade data available",
},
],
};
}
const sorted = [...usdValues].sort((a, b) => a - b);
const min = Math.min(...usdValues);
const max = Math.max(...usdValues);
const median = sorted[Math.floor(usdValues.length / 2)];
// Calculate distribution
const bucketSize = (max - min) / buckets;
const distribution = Array(buckets).fill(0);
for (const usdValue of usdValues) {
if (bucketSize == 0) continue;
const bucketIndex = Math.min(
Math.floor((usdValue - min) / bucketSize),
buckets - 1
);
distribution[bucketIndex]++;
}
const minutesSinceStart = (Date.now() - appContext.startTime) / 60000;
const period = `period: last ${Math.min(minutesSinceStart, MAX_RECORD_AGE / 60000).toFixed(0)} minutes`;
let markdown = `${period}\n`;
markdown += `min: $${min.toFixed(2)}, max: $${max.toFixed(2)}, median: $${median.toFixed(2)}\n`;
markdown += `| range | count |\n`;
markdown += `|-------|-------|\n`;
for (let i = 0; i < distribution.length; i++) {
if (distribution[i] > 0) {
const rangeStart = (min + i * bucketSize).toFixed(2);
const rangeEnd = (min + (i + 1) * bucketSize).toFixed(2);
markdown += `| ${rangeStart}~${rangeEnd} | ${distribution[i]} |\n`;
}
}
return {
content: [
{
type: "text",
text: markdown,
},
],
};
}
);
// Start blockchain listener
async function startListener() {
const provider = new ethers.WebSocketProvider(WS_ENDPOINT);
const contract = new ethers.Contract(BINANCE_DEX_ROUTER, ORDER_ABI, provider);
contract.on(
"OrderRecord",
(inputToken, outputToken, sender, inputAmount, outputAmount) => {
// Use RPC_ENDPOINT for token info queries in processLog
const queryProvider = new ethers.JsonRpcProvider(RPC_ENDPOINT);
processLog({
inputToken,
outputToken,
sender,
inputAmount: inputAmount.toString(),
outputAmount: outputAmount.toString(),
}, queryProvider).catch(console.error);
}
);
}
// Start server and listener
async function main() {
await startListener();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);