Skip to main content
Glama
Bichev
by Bichev
coinbase-client.ts11.2 kB
import axios, { AxiosInstance } from 'axios'; import { CoinbaseAsset, CoinbaseSpotPrice, CoinbaseHistoricalPrice, CoinbaseExchangeRate, CoinbaseStats, CoinbaseTime, PriceAnalysis, CoinbaseAPIError, RateLimitError } from './types.js'; export class CoinbaseClient { private client: AxiosInstance; private baseUrl: string; private requestCount: number = 0; private requestWindow: number = Date.now(); private readonly maxRequestsPerMinute: number = 100; constructor(apiUrl: string = 'https://api.coinbase.com/v2') { this.baseUrl = apiUrl; this.client = axios.create({ baseURL: this.baseUrl, timeout: 30000, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'coinbase-mcp-server/1.0.0' } }); // Request interceptor for rate limiting this.client.interceptors.request.use( async (config) => { this.checkRateLimit(); return config; }, (error) => Promise.reject(error) ); // Response interceptor for error handling this.client.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 429) { const retryAfter = error.response.headers['retry-after'] || 60; throw new RateLimitError( 'Rate limit exceeded from Coinbase API', parseInt(retryAfter, 10) ); } if (error.response?.status >= 400) { throw new CoinbaseAPIError( error.response.data?.error?.message || error.message, error.response.status, error.response.data?.error?.id ); } return Promise.reject(error); } ); } private checkRateLimit(): void { const now = Date.now(); const oneMinute = 60 * 1000; // Reset counter if window has passed if (now - this.requestWindow > oneMinute) { this.requestCount = 0; this.requestWindow = now; } if (this.requestCount >= this.maxRequestsPerMinute) { throw new RateLimitError( 'Rate limit exceeded. Please wait before making more requests.', Math.ceil((oneMinute - (now - this.requestWindow)) / 1000) ); } this.requestCount++; } /** * Get current spot price for a currency pair */ async getSpotPrice(currencyPair: string): Promise<CoinbaseSpotPrice> { try { const response = await this.client.get(`/prices/${currencyPair}/spot`); return response.data; } catch (error) { throw this.handleError(error, 'Failed to fetch spot price'); } } /** * Get historical prices for a currency pair */ async getHistoricalPrices( currencyPair: string, start?: string, end?: string, period: 'hour' | 'day' = 'day' ): Promise<CoinbaseHistoricalPrice> { try { const params: Record<string, string> = { period }; if (start) params.start = start; if (end) params.end = end; const response = await this.client.get(`/prices/${currencyPair}/historic`, { params }); return response.data; } catch (error) { throw this.handleError(error, 'Failed to fetch historical prices'); } } /** * Get exchange rates for a currency */ async getExchangeRates(currency: string): Promise<CoinbaseExchangeRate> { try { const response = await this.client.get(`/exchange-rates?currency=${currency}`); return response.data; } catch (error) { throw this.handleError(error, 'Failed to fetch exchange rates'); } } /** * List all currencies (both fiat and crypto) */ async getCurrencies(): Promise<{ data: CoinbaseAsset[] }> { try { const response = await this.client.get('/currencies'); return response.data; } catch (error) { throw this.handleError(error, 'Failed to fetch currencies'); } } /** * Get 24hr stats for a currency pair (simulated from available data) */ async getStats(currencyPair: string): Promise<CoinbaseStats> { try { // Since Coinbase API v2 doesn't have a stats endpoint, we'll simulate it // using current price and historical data const [spotPrice, historicalData] = await Promise.all([ this.getSpotPrice(currencyPair), this.getHistoricalPrices(currencyPair, undefined, undefined, 'hour').catch(() => null) ]); const currentPrice = parseFloat(spotPrice.data.amount); // If we have historical data, use it for calculations let open = currentPrice; let high = currentPrice; let low = currentPrice; let volume = '0'; if (historicalData && historicalData.data.prices && historicalData.data.prices.length > 0) { const prices = historicalData.data.prices.map((p: any) => parseFloat(p.price)); const last24Hours = prices.slice(-24); // Last 24 hours of data if (last24Hours.length > 0) { open = last24Hours[0]; high = Math.max(...last24Hours); low = Math.min(...last24Hours); // Simulate volume based on price volatility const volatility = (high - low) / open; volume = (volatility * 1000000).toFixed(2); } } else { // Fallback: simulate some basic stats const priceVariation = currentPrice * 0.02; // 2% variation open = currentPrice * (0.98 + Math.random() * 0.04); // Random between -2% and +2% high = currentPrice + priceVariation * Math.random(); low = currentPrice - priceVariation * Math.random(); volume = (Math.random() * 1000000).toFixed(2); } return { data: { open: open.toFixed(2), high: high.toFixed(2), low: low.toFixed(2), volume: volume, last: currentPrice.toFixed(2), volume_30day: (parseFloat(volume) * 30).toFixed(2) // Simulate 30-day volume } }; } catch (error) { throw this.handleError(error, 'Failed to fetch market stats'); } } /** * Get current server time */ async getTime(): Promise<CoinbaseTime> { try { const response = await this.client.get('/time'); return response.data; } catch (error) { throw this.handleError(error, 'Failed to fetch server time'); } } /** * Search for assets by name or symbol */ async searchAssets(query: string, limit: number = 10): Promise<CoinbaseAsset[]> { try { const currencies = await this.getCurrencies(); const searchTerm = query.toLowerCase(); return currencies.data .filter(asset => (asset.name && asset.name.toLowerCase().includes(searchTerm)) || (asset.code && asset.code.toLowerCase().includes(searchTerm)) || (asset.id && asset.id.toLowerCase().includes(searchTerm)) ) .slice(0, limit); } catch (error) { throw this.handleError(error, 'Failed to search assets'); } } /** * Get detailed information about a specific asset */ async getAssetDetails(assetId: string): Promise<CoinbaseAsset | null> { try { const currencies = await this.getCurrencies(); const searchTerm = assetId.toLowerCase(); // Try multiple search strategies let asset = currencies.data.find(a => (a.id && a.id.toLowerCase() === searchTerm) || (a.code && a.code.toLowerCase() === searchTerm) ); // If not found, try partial matching if (!asset) { asset = currencies.data.find(a => (a.name && a.name.toLowerCase().includes(searchTerm)) || (a.id && a.id.toLowerCase().includes(searchTerm)) || (a.code && a.code.toLowerCase().includes(searchTerm)) ); } // If still not found, create a fallback for common cryptocurrencies if (!asset && ['btc', 'eth', 'ltc', 'bch', 'ada', 'dot', 'uni', 'link'].includes(searchTerm)) { asset = { id: searchTerm.toUpperCase(), name: this.getCryptoName(searchTerm), code: searchTerm.toUpperCase(), color: '#000000', type: 'crypto' as const, sort_index: 0, exponent: 8 }; } return asset || null; } catch (error) { throw this.handleError(error, 'Failed to fetch asset details'); } } private getCryptoName(code: string): string { const cryptoNames: Record<string, string> = { 'btc': 'Bitcoin', 'eth': 'Ethereum', 'ltc': 'Litecoin', 'bch': 'Bitcoin Cash', 'ada': 'Cardano', 'dot': 'Polkadot', 'uni': 'Uniswap', 'link': 'Chainlink' }; return cryptoNames[code.toLowerCase()] || code.toUpperCase(); } /** * Analyze price data for trends and patterns */ async analyzePriceData( currencyPair: string, _period: '1d' | '7d' | '30d' | '1y', metrics: ('volatility' | 'trend' | 'support_resistance' | 'volume')[] = ['volatility', 'trend'] ): Promise<PriceAnalysis> { try { // Get current price and stats const [spotPrice, stats] = await Promise.all([ this.getSpotPrice(currencyPair), this.getStats(currencyPair) ]); const currentPrice = parseFloat(spotPrice.data.amount); const open = parseFloat(stats.data.open); const high = parseFloat(stats.data.high); const low = parseFloat(stats.data.low); const volume = parseFloat(stats.data.volume); // Calculate basic metrics const priceChange24h = currentPrice - open; const priceChangePercent24h = ((currentPrice - open) / open) * 100; // Calculate volatility (simplified using high-low range) const volatility = ((high - low) / open) * 100; // Determine trend let trend: 'bullish' | 'bearish' | 'sideways' = 'sideways'; if (priceChangePercent24h > 2) trend = 'bullish'; else if (priceChangePercent24h < -2) trend = 'bearish'; // Basic support and resistance levels const supportLevel = metrics.includes('support_resistance') ? low * 0.98 : undefined; const resistanceLevel = metrics.includes('support_resistance') ? high * 1.02 : undefined; return { currentPrice, volatility, trend, supportLevel, resistanceLevel, volume24h: metrics.includes('volume') ? volume : undefined, priceChange24h, priceChangePercent24h }; } catch (error) { throw this.handleError(error, 'Failed to analyze price data'); } } /** * Get popular trading pairs */ async getPopularPairs(): Promise<string[]> { // Return commonly traded pairs with USD return [ 'BTC-USD', 'ETH-USD', 'LTC-USD', 'BCH-USD', 'ADA-USD', 'DOT-USD', 'UNI-USD', 'LINK-USD', 'XLM-USD', 'USDC-USD', 'AAVE-USD', 'SOL-USD', 'MATIC-USD', 'AVAX-USD', 'ALGO-USD' ]; } private handleError(error: any, message: string): Error { if (error instanceof CoinbaseAPIError || error instanceof RateLimitError) { return error; } return new Error(`${message}: ${error.message}`); } }

Latest Blog Posts

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/Bichev/coinbase-chat-mcp'

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