Skip to main content
Glama
Bichev
by Bichev
index.ts25.2 kB
import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import rateLimit from 'express-rate-limit'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; import { config } from 'dotenv'; import { createLogger, format, transports } from 'winston'; import { CoinbaseClient } from './coinbase-client.js'; import { DemoWalletClient } from './demo-wallet-client.js'; import path from 'path'; import { fileURLToPath } from 'url'; // Load environment variables config(); // Logger setup const logger = createLogger({ level: process.env.LOG_LEVEL || 'info', format: format.combine( format.timestamp(), format.errors({ stack: true }), format.json() ), transports: [ new transports.File({ filename: 'logs/error.log', level: 'error' }), new transports.File({ filename: 'logs/combined.log' }), new transports.Console({ format: format.combine( format.colorize(), format.simple() ) }) ] }); // Express app setup const app = express(); const PORT = process.env.PORT || 3002; // Middleware app.use(helmet()); app.use(cors()); app.use(compression()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests from this IP, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }); app.use('/api/', limiter); // Coinbase client const coinbaseClient = new CoinbaseClient(process.env.COINBASE_API_URL); // Demo Wallet client const demoWalletClient = new DemoWalletClient(coinbaseClient); // Swagger documentation setup const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'Coinbase Chat MCP API', version: '1.0.0', description: 'REST API for Coinbase public cryptocurrency data', license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' } }, servers: [ { url: `http://localhost:${PORT}`, description: 'Development server' } ] }, apis: ['./src/routes/*.ts', './src/index.ts'] }; const swaggerSpec = swaggerJsdoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Health check endpoint /** * @swagger * /health: * get: * summary: Health check endpoint * description: Returns the health status of the API server * responses: * 200: * description: API is healthy * content: * application/json: * schema: * type: object * properties: * status: * type: string * example: "healthy" * timestamp: * type: string * format: date-time * uptime: * type: number * description: Server uptime in seconds */ app.get('/health', (_req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // API Routes /** * @swagger * /api/v1/prices/{currencyPair}: * get: * summary: Get current spot price * description: Get the current spot price for a cryptocurrency pair * parameters: * - in: path * name: currencyPair * required: true * schema: * type: string * description: Currency pair (e.g., BTC-USD) * example: BTC-USD * responses: * 200: * description: Current spot price * content: * application/json: * schema: * type: object * properties: * data: * type: object * properties: * amount: * type: string * example: "45000.00" * base: * type: string * example: "BTC" * currency: * type: string * example: "USD" * 400: * description: Invalid currency pair * 500: * description: Internal server error */ // Get spot price app.get('/api/v1/prices/:currencyPair/spot', async (req, res) => { try { const { currencyPair } = req.params; const data = await coinbaseClient.getSpotPrice(currencyPair); res.json(data); } catch (error) { logger.error('Error fetching spot price:', error); res.status(500).json({ error: 'Failed to fetch spot price', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Legacy route for compatibility app.get('/api/v1/prices/:currencyPair', async (req, res) => { try { const { currencyPair } = req.params; const data = await coinbaseClient.getSpotPrice(currencyPair); res.json(data); } catch (error) { logger.error('Error fetching spot price:', error); res.status(500).json({ error: 'Failed to fetch spot price', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/prices/{currencyPair}/historical: * get: * summary: Get historical prices * description: Get historical price data for a cryptocurrency pair * parameters: * - in: path * name: currencyPair * required: true * schema: * type: string * description: Currency pair (e.g., BTC-USD) * - in: query * name: start * schema: * type: string * format: date * description: Start date (YYYY-MM-DD) * - in: query * name: end * schema: * type: string * format: date * description: End date (YYYY-MM-DD) * - in: query * name: period * schema: * type: string * enum: [hour, day] * default: day * description: Data granularity * responses: * 200: * description: Historical price data * 400: * description: Invalid parameters * 500: * description: Internal server error */ // Get historical prices - new route app.get('/api/v1/prices/:currencyPair/historic', async (req, res) => { try { const { currencyPair } = req.params; const { start, end, period = 'day' } = req.query; const data = await coinbaseClient.getHistoricalPrices( currencyPair, start as string, end as string, period as 'hour' | 'day' ); res.json(data); } catch (error) { logger.error('Error fetching historical prices:', error); res.status(500).json({ error: 'Failed to fetch historical prices', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Legacy route app.get('/api/v1/prices/:currencyPair/historical', async (req, res) => { try { const { currencyPair } = req.params; const { start, end, period = 'day' } = req.query; const data = await coinbaseClient.getHistoricalPrices( currencyPair, start as string, end as string, period as 'hour' | 'day' ); res.json(data); } catch (error) { logger.error('Error fetching historical prices:', error); res.status(500).json({ error: 'Failed to fetch historical prices', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/exchange-rates: * get: * summary: Get exchange rates * description: Get exchange rates for a base currency * parameters: * - in: query * name: currency * required: true * schema: * type: string * description: Base currency code * example: USD * responses: * 200: * description: Exchange rates data * 400: * description: Missing currency parameter * 500: * description: Internal server error */ app.get('/api/v1/exchange-rates', async (req, res) => { try { const { currency } = req.query; if (!currency) { return res.status(400).json({ error: 'Currency parameter is required' }); } const data = await coinbaseClient.getExchangeRates(currency as string); res.json(data); } catch (error) { logger.error('Error fetching exchange rates:', error); res.status(500).json({ error: 'Failed to fetch exchange rates', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/assets: * get: * summary: Get all available assets * description: Get list of all available cryptocurrencies and fiat currencies * parameters: * - in: query * name: search * schema: * type: string * description: Search query for asset filtering * - in: query * name: limit * schema: * type: integer * default: 50 * description: Maximum number of results * responses: * 200: * description: List of assets * 500: * description: Internal server error */ // Search assets route app.get('/api/v1/assets/search', async (req, res) => { try { const { query, limit = '50' } = req.query; if (!query) { return res.status(400).json({ error: 'Query parameter is required' }); } const data = await coinbaseClient.searchAssets(query as string, parseInt(limit as string)); res.json({ data }); } catch (error) { logger.error('Error searching assets:', error); res.status(500).json({ error: 'Failed to search assets', message: error instanceof Error ? error.message : 'Unknown error' }); } }); app.get('/api/v1/assets', async (req, res) => { try { const { search, limit = '50' } = req.query; if (search) { const data = await coinbaseClient.searchAssets(search as string, parseInt(limit as string)); res.json({ data }); } else { const data = await coinbaseClient.getCurrencies(); const limitedData = { ...data, data: data.data.slice(0, parseInt(limit as string)) }; res.json(limitedData); } } catch (error) { logger.error('Error fetching assets:', error); res.status(500).json({ error: 'Failed to fetch assets', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/assets/{assetId}: * get: * summary: Get asset details * description: Get detailed information about a specific asset * parameters: * - in: path * name: assetId * required: true * schema: * type: string * description: Asset ID or symbol * example: BTC * responses: * 200: * description: Asset details * 404: * description: Asset not found * 500: * description: Internal server error */ app.get('/api/v1/assets/:assetId', async (req, res) => { try { const { assetId } = req.params; const data = await coinbaseClient.getAssetDetails(assetId); if (!data) { return res.status(404).json({ error: 'Asset not found' }); } res.json({ data }); } catch (error) { logger.error('Error fetching asset details:', error); res.status(500).json({ error: 'Failed to fetch asset details', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/markets/{currencyPair}/stats: * get: * summary: Get market statistics * description: Get 24-hour market statistics for a currency pair * parameters: * - in: path * name: currencyPair * required: true * schema: * type: string * description: Currency pair * example: BTC-USD * responses: * 200: * description: Market statistics * 500: * description: Internal server error */ app.get('/api/v1/markets/:currencyPair/stats', async (req, res) => { try { const { currencyPair } = req.params; const data = await coinbaseClient.getStats(currencyPair); res.json(data); } catch (error) { logger.error('Error fetching market stats:', error); res.status(500).json({ error: 'Failed to fetch market stats', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/popular-pairs: * get: * summary: Get popular trading pairs * description: Get a list of popular cryptocurrency trading pairs * responses: * 200: * description: List of popular trading pairs * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * type: string * example: "BTC-USD" */ app.get('/api/v1/popular-pairs', async (_req, res) => { try { const data = await coinbaseClient.getPopularPairs(); res.json({ data }); } catch (error) { logger.error('Error fetching popular pairs:', error); res.status(500).json({ error: 'Failed to fetch popular pairs', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/analysis/{currencyPair}: * get: * summary: Analyze price data * description: Perform technical analysis on cryptocurrency price data * parameters: * - in: path * name: currencyPair * required: true * schema: * type: string * description: Currency pair * example: BTC-USD * - in: query * name: period * schema: * type: string * enum: [1d, 7d, 30d, 1y] * default: 1d * description: Analysis period * - in: query * name: metrics * schema: * type: array * items: * type: string * enum: [volatility, trend, support_resistance, volume] * description: Analysis metrics to include * style: form * explode: false * responses: * 200: * description: Price analysis results * 500: * description: Internal server error */ app.get('/api/v1/analysis/:currencyPair', async (req, res) => { try { const { currencyPair } = req.params; const { period = '1d', metrics } = req.query; const validMetrics = ['volatility', 'trend', 'support_resistance', 'volume'] as const; const metricsArray = metrics ? (Array.isArray(metrics) ? metrics : [metrics]).filter((m): m is typeof validMetrics[number] => validMetrics.includes(m as any)) : ['volatility', 'trend'] as ('volatility' | 'trend' | 'support_resistance' | 'volume')[]; const data = await coinbaseClient.analyzePriceData( currencyPair, period as '1d' | '7d' | '30d' | '1y', metricsArray ); res.json({ data }); } catch (error) { logger.error('Error analyzing price data:', error); res.status(500).json({ error: 'Failed to analyze price data', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // 🍺₿ DEMO TRANSACTION ENDPOINTS 🍺₿ /** * @swagger * /api/v1/wallet/calculate-beer-cost: * get: * summary: Calculate beer cost in crypto * description: Calculate how much crypto you can buy with beer money * parameters: * - in: query * name: currency * schema: * type: string * default: BTC * description: Cryptocurrency to calculate * - in: query * name: beerCount * schema: * type: number * default: 1 * description: Number of beers * - in: query * name: pricePerBeer * schema: * type: number * default: 5 * description: Price per beer in USD * responses: * 200: * description: Beer cost calculation * 500: * description: Internal server error */ app.get('/api/v1/wallet/calculate-beer-cost', async (req, res) => { try { const { currency = 'BTC', beerCount = '1', pricePerBeer = '5' } = req.query; const calculation = await demoWalletClient.calculateBeerCost( currency as string, parseInt(beerCount as string), parseFloat(pricePerBeer as string) ); res.json({ data: calculation }); } catch (error) { logger.error('Error calculating beer cost:', error); res.status(500).json({ error: 'Failed to calculate beer cost', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/wallet/purchase: * post: * summary: Simulate crypto purchase * description: Simulate buying cryptocurrency with USD * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - fromCurrency * - toCurrency * - amount * properties: * fromCurrency: * type: string * example: USD * toCurrency: * type: string * example: BTC * amount: * type: number * example: 5 * description: * type: string * example: Beer money investment * responses: * 200: * description: Transaction successful * 400: * description: Invalid request * 500: * description: Internal server error */ app.post('/api/v1/wallet/purchase', async (req, res) => { try { const { fromCurrency, toCurrency, amount, description } = req.body; if (!fromCurrency || !toCurrency || !amount) { return res.status(400).json({ error: 'Missing required fields: fromCurrency, toCurrency, amount' }); } const transaction = await demoWalletClient.simulatePurchase( fromCurrency, toCurrency, parseFloat(amount), description ); res.json({ data: transaction }); } catch (error) { logger.error('Error simulating purchase:', error); res.status(500).json({ error: 'Failed to simulate purchase', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/wallet: * get: * summary: Get virtual wallet * description: Get demo wallet balance and statistics * responses: * 200: * description: Wallet information * 500: * description: Internal server error */ app.get('/api/v1/wallet', async (_req, res) => { try { const wallet = demoWalletClient.getWallet(); const stats = demoWalletClient.getWalletStats(); res.json({ data: { wallet, stats } }); } catch (error) { logger.error('Error fetching wallet:', error); res.status(500).json({ error: 'Failed to fetch wallet', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/wallet/transactions: * get: * summary: Get transaction history * description: Get demo transaction history with optional filtering * parameters: * - in: query * name: limit * schema: * type: number * default: 10 * description: Maximum number of transactions * - in: query * name: currency * schema: * type: string * description: Filter by currency * responses: * 200: * description: Transaction history * 500: * description: Internal server error */ app.get('/api/v1/wallet/transactions', async (req, res) => { try { const { limit = '10', currency } = req.query; const transactions = demoWalletClient.getTransactionHistory( parseInt(limit as string), currency as string | undefined ); res.json({ data: transactions }); } catch (error) { logger.error('Error fetching transactions:', error); res.status(500).json({ error: 'Failed to fetch transactions', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/wallet/reset: * post: * summary: Reset virtual wallet * description: Reset demo wallet to initial state * responses: * 200: * description: Wallet reset successful * 500: * description: Internal server error */ app.post('/api/v1/wallet/reset', async (_req, res) => { try { demoWalletClient.resetWallet(); const wallet = demoWalletClient.getWallet(); res.json({ data: wallet, message: 'Wallet reset to initial state' }); } catch (error) { logger.error('Error resetting wallet:', error); res.status(500).json({ error: 'Failed to reset wallet', message: error instanceof Error ? error.message : 'Unknown error' }); } }); /** * @swagger * /api/v1/wallet/buy-beer: * post: * summary: Buy virtual beer with cryptocurrency * description: Purchase virtual beer using crypto (creates circular economy - BTC → Beer) * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * quantity: * type: number * example: 1 * default: 1 * currency: * type: string * example: BTC * default: BTC * pricePerBeer: * type: number * example: 5 * default: 5 * responses: * 200: * description: Beer purchase successful or needs more crypto * 500: * description: Internal server error */ app.post('/api/v1/wallet/buy-beer', async (req, res) => { try { const { quantity = 1, currency = 'BTC', pricePerBeer = 5 } = req.body; const result = await demoWalletClient.buyVirtualBeer( quantity, currency, pricePerBeer ); if (!result.success) { return res.status(200).json({ success: false, needsMoreCrypto: result.needsMoreCrypto, suggestedAmount: result.suggestedAmount, message: result.message }); } res.json({ success: true, data: result.transaction, message: result.message }); } catch (error) { logger.error('Error buying virtual beer:', error); res.status(500).json({ error: 'Failed to buy virtual beer', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Serve static files from frontend (for Vercel deployment) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Serve static files from the frontend build app.use(express.static(path.join(__dirname, '../../frontend/dist'))); // Handle React routing - serve index.html for all non-API routes app.get('*', (req, res) => { // Don't serve index.html for API routes if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'API route not found', path: req.originalUrl }); } // Serve index.html for all other routes (React app) res.sendFile(path.join(__dirname, '../../frontend/dist/index.html')); }); // Error handling middleware app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { logger.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' }); }); // For local development, start server if (process.env.NODE_ENV !== 'production') { const server = app.listen(PORT, () => { logger.info(`API Server running on port ${PORT}`); logger.info(`API Documentation available at http://localhost:${PORT}/api-docs`); }); // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Process terminated'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Process terminated'); process.exit(0); }); }); } export default app;

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