Skip to main content
Glama

Trading Simulator MCP Server

by recallnet
import { config, logger } from './env.js'; import { BlockchainType, SpecificChain, TradeParams, TradeHistoryParams, PriceHistoryParams, BalancesResponse, PortfolioResponse, TradeHistoryResponse, PriceResponse, TokenInfoResponse, PriceHistoryResponse, TradeResponse, QuoteResponse, CompetitionStatusResponse, LeaderboardResponse, CompetitionRulesResponse, COMMON_TOKENS, TeamProfileResponse, HealthCheckResponse, DetailedHealthCheckResponse, TeamMetadata, ApiResponse, ErrorResponse, } from './types.js'; /** * Trading Simulator API Client * * Handles authentication and provides methods for interacting * with the Trading Simulator API. */ export class TradingSimulatorClient { private readonly apiKey: string; private readonly baseUrl: string; private debug: boolean; /** * Create a new instance of the Trading Simulator client * * @param apiKey The API key for your team * @param baseUrl The base URL of the Trading Simulator API * @param debug Whether to enable debug logging */ constructor( apiKey?: string | undefined, baseUrl: string = config.TRADING_SIM_API_URL, debug: boolean = config.DEBUG ) { // Trim the API key to avoid whitespace issues (if provided) const providedKey = apiKey || config.TRADING_SIM_API_KEY; this.apiKey = providedKey ? providedKey.trim() : ''; // Check for empty API key but don't throw - this allows client creation // but will fail on actual API calls if (!this.apiKey) { logger.error('No API key provided. API calls will fail until a key is set.'); } // Normalize the base URL to ensure no trailing slash this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; this.debug = debug; } /** * Generate the required headers for API authentication * * @returns An object containing the required headers */ private generateHeaders(): Record<string, string> { const headers: Record<string, string> = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'User-Agent': 'TradingSimMCP/1.0' }; if (this.debug) { logger.info('[ApiClient] Request headers:'); logger.info('[ApiClient] Authorization: Bearer xxxxx... (masked)'); logger.info('[ApiClient] Content-Type:', headers['Content-Type']); } return headers; } /** * Type guard to check if response is an ErrorResponse */ private isErrorResponse(response: any): response is ErrorResponse { return ( response !== null && typeof response === 'object' && response.success === false && 'error' in response && 'status' in response ); } /** * Helper method to handle API errors consistently */ private handleApiError(error: any, operation: string): ErrorResponse { logger.error(`Failed to ${operation}:`, error); // Handle fetch error responses if (error instanceof Error) { const status = 'status' in error ? (error as any).status : 500; return { success: false, error: error.message, status }; } // Fallback for unexpected error formats return { success: false, error: String(error), status: 500 }; } /** * Make a request to the API * * @param method The HTTP method * @param path The API endpoint path * @param body The request body (if any) * @returns A promise that resolves to the API response or an error response */ private async request<T extends ApiResponse>( method: string, path: string, body: any = null, operation: string ): Promise<T | ErrorResponse> { const url = `${this.baseUrl}${path}`; const bodyString = body ? JSON.stringify(body) : undefined; const headers = this.generateHeaders(); const options: RequestInit = { method: method.toUpperCase(), headers, body: bodyString, }; if (this.debug) { logger.info('[ApiClient] Request details:'); logger.info('[ApiClient] Method:', method); logger.info('[ApiClient] URL:', url); logger.info('[ApiClient] Body:', body ? JSON.stringify(body, null, 2) : 'none'); } try { const response = await fetch(url, options); const responseText = await response.text(); if (!response.ok) { let errorMessage = `API request failed with status ${response.status}`; if (responseText.trim()) { try { const errorData = JSON.parse(responseText); errorMessage = errorData.error?.message || errorData.message || errorMessage; } catch { errorMessage = responseText; } } return { success: false, error: errorMessage, status: response.status }; } try { const data = JSON.parse(responseText); return data as T; } catch (parseError) { return { success: false, error: `Failed to parse successful response: ${parseError}`, status: 500 }; } } catch (networkError) { return this.handleApiError(networkError, operation); } } /** * Detect blockchain type from token address format * * @param token The token address * @returns The detected blockchain type (SVM or EVM) */ public detectChain(token: string): BlockchainType { // Ethereum addresses start with '0x' followed by 40 hex characters if (/^0x[a-fA-F0-9]{40}$/.test(token)) { return BlockchainType.EVM; } // Solana addresses are base58 encoded, typically around 44 characters return BlockchainType.SVM; } /** * Get your team profile information * * @returns Team profile information or error response */ async getProfile(): Promise<TeamProfileResponse | ErrorResponse> { return this.request<TeamProfileResponse>( 'GET', '/api/account/profile', null, 'get team profile' ); } /** * Update your team profile information * * @param contactPerson New contact person name (optional) * @param metadata New metadata object (optional) * @returns Updated team profile information or error response */ async updateProfile( contactPerson?: string, metadata?: TeamMetadata ): Promise<TeamProfileResponse | ErrorResponse> { const body: { contactPerson?: string; metadata?: TeamMetadata } = {}; if (contactPerson !== undefined) { body.contactPerson = contactPerson; } if (metadata !== undefined) { body.metadata = metadata; } return this.request<TeamProfileResponse>( 'PUT', '/api/account/profile', body, 'update team profile' ); } /** * Get your team's token balances across all supported chains * * @returns Balance information including tokens on all chains or error response */ async getBalances(): Promise<BalancesResponse | ErrorResponse> { return this.request<BalancesResponse>( 'GET', '/api/account/balances', null, 'get balances' ); } /** * Get your team's portfolio information * * @returns Portfolio information including positions and total value or error response */ async getPortfolio(): Promise<PortfolioResponse | ErrorResponse> { return this.request<PortfolioResponse>( 'GET', '/api/account/portfolio', null, 'get portfolio' ); } /** * Get trade history * * @param options Optional parameters to filter the trade history * @returns Trade history or error response */ async getTradeHistory(options?: TradeHistoryParams): Promise<TradeHistoryResponse | ErrorResponse> { let path = '/api/account/trades'; // Add query parameters if provided if (options) { const params = new URLSearchParams(); if (options.limit !== undefined) { params.append('limit', options.limit.toString()); } if (options.offset !== undefined) { params.append('offset', options.offset.toString()); } if (options.token) { params.append('token', options.token); } if (options.chain) { params.append('chain', options.chain); } // Append query string if we have parameters const queryString = params.toString(); if (queryString) { path += `?${queryString}`; } } return this.request<TradeHistoryResponse>( 'GET', path, null, 'get trade history' ); } /** * Get the current price for a token * * @param token The token address to get the price for * @param chain Optional blockchain type (auto-detected if not provided) * @param specificChain Optional specific chain for EVM tokens (like eth, polygon, base, etc.) * @returns A promise that resolves to the price response or error response */ async getPrice( token: string, chain?: BlockchainType, specificChain?: SpecificChain ): Promise<PriceResponse | ErrorResponse> { const params = new URLSearchParams(); params.append('token', token); if (chain) params.append('chain', chain); if (specificChain) params.append('specificChain', specificChain); return this.request<PriceResponse>( 'GET', `/api/price?${params.toString()}`, null, 'get token price' ); } /** * Get detailed token information including specific chain * * @param token The token address * @param chain Optional blockchain type (auto-detected if not provided) * @param specificChain Optional specific chain for EVM tokens * @returns A promise that resolves to the token info response or error response */ async getTokenInfo( token: string, chain?: BlockchainType, specificChain?: SpecificChain ): Promise<TokenInfoResponse | ErrorResponse> { const params = new URLSearchParams(); params.append('token', token); if (chain) params.append('chain', chain); if (specificChain) params.append('specificChain', specificChain); return this.request<TokenInfoResponse>( 'GET', `/api/price/token-info?${params.toString()}`, null, 'get token info' ); } /** * Get historical price data for a token * * @param params Parameters for the price history request * @returns A promise that resolves to the price history response or error response */ async getPriceHistory( tokenOrParams: string | PriceHistoryParams, interval?: string, chain?: BlockchainType, specificChain?: SpecificChain, startTime?: string, endTime?: string ): Promise<PriceHistoryResponse | ErrorResponse> { const urlParams = new URLSearchParams(); // Handle both object-based and individual parameter calls if (typeof tokenOrParams === 'object') { // Object parameter version const params = tokenOrParams; urlParams.append('token', params.token); if (params.startTime) urlParams.append('startTime', params.startTime); if (params.endTime) urlParams.append('endTime', params.endTime); if (params.interval) urlParams.append('interval', params.interval); if (params.chain) urlParams.append('chain', params.chain); if (params.specificChain) urlParams.append('specificChain', params.specificChain); } else { // Individual parameters version urlParams.append('token', tokenOrParams); if (interval) urlParams.append('interval', interval); if (chain) urlParams.append('chain', chain); if (specificChain) urlParams.append('specificChain', specificChain); if (startTime) urlParams.append('startTime', startTime); if (endTime) urlParams.append('endTime', endTime); } return this.request<PriceHistoryResponse>( 'GET', `/api/price/history?${urlParams.toString()}`, null, 'get price history' ); } /** * Find token chain information from common tokens * * @param token The token address * @returns Chain information or null if not found */ private findTokenChainInfo(token: string): { chain: BlockchainType, specificChain: SpecificChain } | null { // Check SVM tokens for (const chainType in COMMON_TOKENS.SVM) { const tokens = COMMON_TOKENS.SVM[chainType as keyof typeof COMMON_TOKENS.SVM]; for (const symbolKey in tokens) { if (tokens[symbolKey as keyof typeof tokens] === token) { return { chain: BlockchainType.SVM, specificChain: SpecificChain.SVM }; } } } // Check EVM tokens for (const network in COMMON_TOKENS.EVM) { const tokens = COMMON_TOKENS.EVM[network as keyof typeof COMMON_TOKENS.EVM]; for (const symbolKey in tokens) { if (tokens[symbolKey as keyof typeof tokens] === token) { return { chain: BlockchainType.EVM, specificChain: network as unknown as SpecificChain }; } } } return null; } /** * Execute a trade between two tokens * * @param params Trade execution parameters * @returns A promise that resolves to the trade response or error response */ async executeTrade(params: TradeParams): Promise<TradeResponse | ErrorResponse> { if (this.debug) { logger.info('[ApiClient] executeTrade called with params:', JSON.stringify(params, null, 2)); } return this.request<TradeResponse>( 'POST', '/api/trade/execute', params, 'execute trade' ); } /** * Get a quote for a potential trade * * @param fromToken The source token address * @param toToken The destination token address * @param amount The amount of fromToken to trade * @param fromChain Optional source chain type * @param toChain Optional destination chain type * @param fromSpecificChain Optional specific chain for the source token * @param toSpecificChain Optional specific chain for the destination token * @returns A promise that resolves to the quote response or error response */ async getQuote( fromToken: string, toToken: string, amount: string, fromChain?: BlockchainType, toChain?: BlockchainType, fromSpecificChain?: SpecificChain, toSpecificChain?: SpecificChain ): Promise<QuoteResponse | ErrorResponse> { const params = new URLSearchParams(); params.append('fromToken', fromToken); params.append('toToken', toToken); params.append('amount', amount); if (fromChain) { params.append('fromChain', fromChain); } if (toChain) { params.append('toChain', toChain); } if (fromSpecificChain) { params.append('fromSpecificChain', fromSpecificChain); } if (toSpecificChain) { params.append('toSpecificChain', toSpecificChain); } return this.request<QuoteResponse>( 'GET', `/api/trade/quote?${params.toString()}`, null, 'get quote' ); } /** * Get the status of the current competition * * @returns A promise that resolves to the competition status response or error response */ async getCompetitionStatus(): Promise<CompetitionStatusResponse | ErrorResponse> { return this.request<CompetitionStatusResponse>( 'GET', '/api/competition/status', null, 'get competition status' ); } /** * Get the leaderboard for the active competition * * @param competitionId Optional competition ID (if not provided, the active competition is used) * @returns A promise that resolves to the leaderboard response or error response */ async getLeaderboard(competitionId?: string): Promise<LeaderboardResponse | ErrorResponse> { let query = ''; if (competitionId) { query = `?competitionId=${encodeURIComponent(competitionId)}`; } return this.request<LeaderboardResponse>( 'GET', `/api/competition/leaderboard${query}`, null, 'get leaderboard' ); } /** * Get competition rules * * @returns A promise that resolves to the competition rules response or error response */ async getRules(): Promise<CompetitionRulesResponse | ErrorResponse> { return this.request<CompetitionRulesResponse>( 'GET', '/api/competition/rules', null, 'get competition rules' ); } /** * Get basic health status of the API * * @returns A promise that resolves to the health status response or error response */ async getHealthStatus(): Promise<HealthCheckResponse | ErrorResponse> { return this.request<HealthCheckResponse>( 'GET', '/api/health', null, 'get health status' ); } /** * Get detailed health status of the API and its services * * @returns A promise that resolves to the detailed health status response or error response */ async getDetailedHealthStatus(): Promise<DetailedHealthCheckResponse | ErrorResponse> { return this.request<DetailedHealthCheckResponse>( 'GET', '/api/health/detailed', null, 'get detailed health status' ); } } // Export a pre-configured instance of the client export const tradingClient = new TradingSimulatorClient();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/recallnet/trading-simulator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server