Build an AI Trading Terminal with LunarCrush MCP + Remix + Gemini in 20 Minutes
Transform social intelligence into trading insights using Model Context Protocol (MCP) and AI-powered analysis
Why MCP Changes Everything for AI Development
Traditional API integrations require developers to manually orchestrate multiple endpoints, handle complex error scenarios, and build custom data formatting logic. This creates significant overhead and maintenance burden.
Model Context Protocol (MCP) revolutionizes how AI systems access real-time data. Instead of manual API integration, MCP creates secure, standardized connections between AI models and data sources.
This means your AI can intelligently orchestrate multiple data tools, make complex decisions, and generate insights that would take hours to code manually.
What You'll Build
In this tutorial, you'll create a production-ready AI Trading Terminal that:
β’ β
MCP Integration - Direct connection between Google Gemini AI and LunarCrush tools
β’ β
AI Orchestration - Gemini intelligently selects and combines multiple data sources
β’ β
Real-time Analysis - Live progress tracking through a 6-step AI pipeline
β’ β
Interactive Charts - Visual price history with responsive design
β’ β
Smart Caching - Instant retrieval for previously analyzed cryptocurrencies
Time Investment: 20 minutes
Skill Level: Beginner to Intermediate
What You'll Learn: Remix, TypeScript, MCP integration, AI orchestration, real-time data processing
π‘ Pro Tip: By the end, you'll have a portfolio-worthy project that demonstrates modern AI development patterns with MCP!
Live Example: View the deployed version β
Before We Start
You'll Need:
β’ Node.js 18+ installed
β’ Basic knowledge of React/TypeScript
β’ A code editor (VS Code recommended)
β’ 2 API keys from different services (we'll walk through signup below)
Two Ways to Experience This Tutorial:
- π¨βπ» Build It Yourself - Follow along step-by-step with your own API keys
- π Try the Live Demo - View the deployed version and explore the code
Quick Project Setup:
# We'll build this step-by-step, but here's the final structure:
npx create-remix@latest lunarcrush-mcp --template remix-run/remix/templates/remix --typescript
cd lunarcrush-mcp
npm install @google/generative-ai @modelcontextprotocol/sdk @heroui/react recharts
π¨ Common Issue: Make sure you have Node.js 18+ installed. Check with node --version
Account Setup Guide
We need 2 services for this project. Both have generous free tiers!
Sign Up For LunarCrush API
LunarCrush provides social sentiment data that most traders don't have access to.
Use my discount referral code JAMAALBUILDS to receive 15% off your plan.
- Visit LunarCrush Signup
- Enter your email address and click "Continue"
- Check your email for verification code and enter it
- Complete the onboarding steps: β’ Select your favorite categories (or keep defaults) β’ Create your profile (add photo and nickname if desired) β’ Important: Select a subscription plan (you'll need it to generate an API key)
Generate Your API Key:
Once you've subscribed, navigate to the API authentication page and generate an API key.
Save this API key - you'll add it to your environment variables later.
Set Up Google Gemini AI
Google's Gemini AI will orchestrate the LunarCrush tools and generate trading recommendations.
- Sign up: Visit aistudio.google.com and click "Get API key"
- Choose authentication: Sign in with your Google account
-
Create API key:
β’ Click "Create API key"
β’ Choose "Create API key in new project" or select existing project
β’ Copy your API key (starts with
AIza...
)
Project Setup
Create Remix Project
# Create new Remix project with TypeScript
npx create-remix@latest lunarcrush-mcp --template remix-run/remix/templates/remix --typescript
cd lunarcrush-mcp
# Install required dependencies
npm install @google/generative-ai @modelcontextprotocol/sdk @heroui/react recharts clsx tailwind-merge
# Install development dependencies
npm install @types/node --save-dev
# Create environment file
touch .env.local
Set Up Environment Variables
Add your API keys to .env.local
:
# .env.local
LUNARCRUSH_API_KEY=lc_your_key_here
GOOGLE_GEMINI_API_KEY=your_gemini_key_here
Create Project Structure (Copy/Paste Terminal Commands)
# Create directory structure (some may already exist)
mkdir -p components hooks config types
# Create TypeScript interfaces
cat > types/index.ts << 'EOF'
import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number;
};
// Gemini AI Types
export interface GeminiResponse {
candidates?: Array<{
content?: {
parts?: Array<{
text?: string;
}>;
};
}>;
}
// Trading Analysis Types
export interface TradingAnalysis {
symbol: string;
recommendation: 'BUY' | 'SELL' | 'HOLD';
confidence: number;
reasoning: string;
social_sentiment: 'bullish' | 'bearish' | 'neutral';
key_metrics: Record<string, string | number | unknown>;
ai_analysis: {
summary?: string;
pros?: string[];
cons?: string[];
key_factors?: string[];
} | string;
timestamp: string;
chart_data: Array<{ date: string; price: number }>;
success: boolean;
error?: string;
processingTime?: number;
}
// Tool Call Types
export interface ToolCall {
tool: string;
args: Record<string, unknown>;
reason: string;
}
// MCP Tool Types
export interface McpTool {
name: string;
inputSchema: {
type: string;
properties?: Record<string, unknown>;
required?: string[];
};
title?: string;
description?: string;
outputSchema?: Record<string, unknown>;
annotations?: Record<string, unknown>;
}
// Tool Result Types
export interface ToolResult {
tool: string;
args: Record<string, unknown>;
reason: string;
result?: unknown;
error?: string;
}
EOF
Core Implementation (Copy/Paste Terminal Commands)
Create the MCP Client Hook
# Create MCP server hook
cat > hooks/useMcpServer.js << 'EOF'
// src/hooks/useGeminiMcp.js
import { useMcp } from 'use-mcp/react';
const useMcpServer = ({ url, clientName, autoReconnect = true, config }) => {
// MCP connection via use-mcp
const {
state, // Connection state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
tools, // Available tools from MCP server
callTool, // Function to call tools on the MCP server
} = useMcp({
url,
clientName,
autoReconnect,
...config,
});
return {
tools,
callTool,
state,
};
};
export default useMcpServer;
EOF
Create the Advanced Crypto Chart Component
# Create advanced crypto chart component
cat > app/components/CryptoChart.tsx << 'EOF'
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
interface ChartData {
date: string;
close: number;
}
interface CryptoChartProps {
data: ChartData[];
symbol: string;
}
export default function CryptoChart({ data, symbol }: CryptoChartProps) {
if (!data || data.length === 0) {
return (
<div className='flex items-center justify-center h-64 bg-gray-50 dark:bg-gray-800 rounded-lg'>
<p className='text-gray-500 dark:text-gray-400'>
No chart data available
</p>
</div>
);
}
// Format the date for display
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
};
// Format price for display
const formatPrice = (price: number) => {
if (price < 0.01) {
return `$${price.toFixed(6)}`;
} else if (price < 1) {
return `$${price.toFixed(4)}`;
} else {
return `$${price.toLocaleString()}`;
}
};
return (
<div className='w-full'>
<div className='mb-4'>
<h3 className='text-lg font-semibold text-gray-800 dark:text-white'>
{symbol} Price History (Last Week)
</h3>
<p className='text-sm text-gray-600 dark:text-gray-400'>
Historical price data from LunarCrush Topic_Time_Series
</p>
</div>
<div className='h-64 w-full'>
<ResponsiveContainer width='100%' height='100%'>
<LineChart
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}>
<CartesianGrid
strokeDasharray='3 3'
stroke='#374151'
opacity={0.3}
/>
<XAxis
dataKey='date'
tickFormatter={formatDate}
stroke='#6B7280'
fontSize={12}
/>
<YAxis tickFormatter={formatPrice} stroke='#6B7280' fontSize={12} />
<Tooltip
labelFormatter={(label) => `Date: ${formatDate(label)}`}
formatter={(value: number) => [formatPrice(value), 'Price']}
contentStyle={{
backgroundColor: '#1F2937',
border: '1px solid #374151',
borderRadius: '6px',
color: '#F9FAFB',
}}
/>
<Line
type='monotone'
dataKey='close'
stroke='#3B82F6'
strokeWidth={2}
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3B82F6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
Data points: {data.length} | Range:{' '}
{formatPrice(Math.min(...data.map((d) => d.close)))} -{' '}
{formatPrice(Math.max(...data.map((d) => d.close)))}
</div>
</div>
);
}
EOF
Create the Main Trading Interface
# Create main trading page
cat > app/routes/_index.tsx << 'EOF'
import { useState, useEffect, lazy } from 'react';
import { useLoaderData } from '@remix-run/react';
import { json, type MetaFunction } from '@remix-run/node';
import { Input } from '@heroui/input';
import { Button } from '@heroui/button';
import { Card, CardBody } from '@heroui/card';
import { Chip } from '@heroui/chip';
import type { TradingAnalysis } from '../../types';
// Client-side cache using sessionStorage for persistence across page reloads
const getCache = () => {
if (typeof window === 'undefined') return new Map(); // Server-side fallback
try {
const cached = sessionStorage.getItem('trading-cache');
return cached ? new Map(JSON.parse(cached)) : new Map();
} catch {
return new Map();
}
};
const setCache = (key: string, value: TradingAnalysis) => {
if (typeof window === 'undefined') return; // Server-side fallback
try {
const cache = getCache();
cache.set(key, value);
sessionStorage.setItem('trading-cache', JSON.stringify([...cache]));
} catch {
// Ignore storage errors
}
};
const ChartComponent = lazy(() => import('../../components/PriceChart'));
export const meta: MetaFunction = () => {
return [
{ title: 'LunarCrush AI Trading Terminal | MCP Powered' },
{
name: 'description',
content:
'Professional AI-powered trading terminal with real-time social intelligence',
},
];
};
export async function loader() {
return json({
message: 'Trading terminal loaded successfully',
env: {
hasGeminiKey: !!process.env.GOOGLE_GEMINI_API_KEY,
hasLunarCrushKey: !!process.env.LUNARCRUSH_API_KEY,
},
});
}
export default function TradingIndex() {
const { env } = useLoaderData<typeof loader>();
const [isClient, setIsClient] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [progressStep, setProgressStep] = useState(0);
const [progressPercent, setProgressPercent] = useState(0);
const [subStepMessage, setSubStepMessage] = useState('');
// State for analysis results
const [analysis, setAnalysis] = useState<TradingAnalysis | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
// Ensure we're on the client side to prevent hydration mismatch
useEffect(() => {
setIsClient(true);
}, []);
// Check if we have the required environment indicators
const hasRequiredKeys = env.hasGeminiKey && env.hasLunarCrushKey;
const progressSteps = [
{
label: 'Connecting to LunarCrush MCP',
description: 'Establishing secure connection to LunarCrush API...',
},
{
label: 'Fetching social & market data',
description: 'Retrieving real-time social sentiment and market data...',
},
{
label: 'Processing social metrics',
description: 'Analyzing social metrics and engagement patterns...',
},
{
label: 'Running Gemini AI analysis',
description: 'Google Gemini AI processing complex data patterns...',
},
{
label: 'Analyzing market patterns',
description: 'Deep learning analysis of price movements and trends...',
},
{
label: 'Generating insights',
description: 'Creating personalized trading recommendations...',
},
{
label: 'Finalizing recommendations',
description: 'Compiling comprehensive analysis report...',
},
{
label: 'Preparing results',
description: 'Formatting and validating final output...',
},
];
async function analyzeSymbol(symbol = 'BTC'): Promise<TradingAnalysis> {
console.log(`οΏ½ Starting server-side analysis for ${symbol}`);
const formData = new FormData();
formData.append('symbol', symbol);
const response = await fetch('/api/analyze', {
method: 'POST',
body: formData,
});
// Check if the response is ok
console.log(
`οΏ½ Received response for ${symbol}. Response OK: ${response.ok}${response.status}`
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Analysis failed');
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Analysis failed');
}
console.log(
`β
Analysis completed for ${symbol} ${data.success} ${data.data}`
);
return data.data;
}
// Handle progress animation when loading starts
useEffect(() => {
let stepInterval: NodeJS.Timeout;
let subStepInterval: NodeJS.Timeout;
// Sub-step messages for the final phases (steps 3-7)
const aiAnalysisMessages = [
'Analyzing sentiment correlations...',
'Processing market volatility patterns...',
'Evaluating social momentum indicators...',
'Cross-referencing technical signals...',
'Calculating risk-adjusted probabilities...',
'Generating confidence intervals...',
'Optimizing recommendation logic...',
'Validating analysis accuracy...',
'Processing social engagement metrics...',
'Analyzing price-volume relationships...',
'Evaluating market maker behavior...',
'Scanning for whale activity patterns...',
'Computing social sentiment scores...',
'Analyzing influencer impact metrics...',
'Processing fear & greed indicators...',
'Evaluating community growth trends...',
'Calculating momentum divergences...',
'Analyzing support & resistance levels...',
'Processing order book dynamics...',
'Evaluating liquidity pool data...',
'Computing volatility projections...',
'Analyzing institutional flow patterns...',
'Processing news sentiment impact...',
'Evaluating correlation matrices...',
'Computing risk-reward ratios...',
'Analyzing market cycle positioning...',
'Processing fundamental indicators...',
'Evaluating adoption metrics...',
'Computing probability distributions...',
'Finalizing recommendation synthesis...',
'Preparing comprehensive report...',
'Validating output consistency...',
'Optimizing confidence scoring...',
'Formatting analysis results...',
];
if (isAnalyzing) {
// Reset states immediately
setProgressStep(0);
setProgressPercent(0);
setSubStepMessage('');
// Step progression - advance every 2.5 seconds and update progress
stepInterval = setInterval(() => {
setProgressStep((prev) => {
const next = prev + 1;
const finalStep =
next >= progressSteps.length - 1 ? progressSteps.length - 1 : next;
// Update progress based on step completion
// Each step represents equal progress up to 90%
const progressByStep = Math.round(
(finalStep + 1) * (90 / progressSteps.length)
);
setProgressPercent(progressByStep);
return finalStep;
});
}, 3000); // Change step every 3 seconds
// Sub-step messaging for AI analysis phases (starts after step 3)
subStepInterval = setInterval(() => {
setProgressStep((currentStep) => {
// Only show sub-messages during AI analysis phases (steps 3-7)
if (currentStep >= 4) {
const randomMessage =
aiAnalysisMessages[
Math.floor(Math.random() * aiAnalysisMessages.length)
];
setSubStepMessage(randomMessage);
} else {
setSubStepMessage('');
}
return currentStep;
});
}, 4000); // Rotate sub-messages every 1.5 seconds
} else {
// Loading finished - complete the progress
setProgressPercent(100);
setProgressStep(progressSteps.length - 1);
setSubStepMessage('Analysis complete!');
// Reset after showing completion
const resetTimeout = setTimeout(() => {
setProgressPercent(0);
setProgressStep(0);
setSubStepMessage('');
}, 2000);
return () => clearTimeout(resetTimeout);
}
return () => {
if (stepInterval) clearInterval(stepInterval);
if (subStepInterval) clearInterval(subStepInterval);
};
}, [isAnalyzing, progressSteps.length]); // Only depend on analyzing state
const handleSearch = async () => {
if (!searchTerm.trim()) return;
setIsAnalyzing(true);
setAnalysis(null);
setProgressStep(0);
setProgressPercent(0);
setSubStepMessage('');
try {
// Step 1: Initialize server-side analysis
setProgressStep(1);
setProgressPercent(12.5);
setSubStepMessage('π Initializing server-side analysis...');
await new Promise((resolve) => setTimeout(resolve, 500));
// Step 2: Connecting to APIs
setProgressStep(2);
setProgressPercent(25);
setSubStepMessage('π Connecting to LunarCrush and Gemini APIs...');
await new Promise((resolve) => setTimeout(resolve, 300));
// Step 3: Start the analysis
setProgressStep(3);
setProgressPercent(50);
setSubStepMessage('β‘ Gemini orchestrating analysis...');
let analysisResponse;
// Only use cache on client side to prevent hydration mismatch
if (isClient) {
const cache = getCache();
if (cache.has(searchTerm.toUpperCase())) {
console.log(`π Cache hit for ${searchTerm.toUpperCase()}`);
analysisResponse = cache.get(searchTerm.toUpperCase());
} else {
analysisResponse = await analyzeSymbol(searchTerm.toUpperCase());
setCache(searchTerm.toUpperCase(), analysisResponse);
}
} else {
analysisResponse = await analyzeSymbol(searchTerm.toUpperCase());
}
// Step 4: Finalize results
setProgressStep(4);
setProgressPercent(100);
setSubStepMessage('β
Analysis complete!');
// Set the final analysis
setAnalysis({
...analysisResponse,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('β Analysis failed:', error);
setSubStepMessage(
`β Error: ${
error instanceof Error ? error.message : 'Analysis failed'
}`
);
// Set error analysis
setAnalysis({
success: false,
error: error instanceof Error ? error.message : 'Analysis failed',
symbol: searchTerm.toUpperCase(),
recommendation: 'HOLD',
confidence: 0,
reasoning: 'Analysis could not be completed due to technical issues.',
social_sentiment: 'neutral',
key_metrics: {},
ai_analysis: {
summary: 'Analysis failed due to technical issues.',
pros: [],
cons: ['Technical error occurred'],
key_factors: [],
},
timestamp: new Date().toISOString(),
chart_data: [],
});
} finally {
// Always stop analyzing state
setTimeout(() => {
setIsAnalyzing(false);
}, 1000);
}
};
return (
<div className='min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-indigo-900'>
{/* Header */}
<header className='border-b border-slate-700/50 bg-slate-900/50 backdrop-blur-xl'>
<div className='max-w-7xl mx-auto px-6 py-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<div className='w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center'>
<span className='text-white font-bold text-sm'>LC</span>
</div>
<div>
<h1 className='text-xl font-bold text-white'>
AI Trading Agent
</h1>
<p className='text-xs text-slate-400'>
Powered by the LunarCrush MCP & Google Gemini
</p>
</div>
</div>
<div className='flex items-center gap-2'>
<div
className={`w-2 h-2 rounded-full ${
hasRequiredKeys ? 'bg-green-500 animate-pulse' : 'bg-red-500'
}`}></div>
<span className='text-sm text-slate-300'>
{hasRequiredKeys ? 'API Ready' : 'Missing API Keys'}
</span>
{hasRequiredKeys && (
<Chip
size='sm'
variant='flat'
className='bg-green-500/20 text-green-300 text-xs'>
Server-Side Analysis
</Chip>
)}
</div>
</div>
</div>
</header>
<div className='max-w-7xl mx-auto px-6 py-8'>
{/* Search Section */}
<div className='mb-8'>
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl'>
<CardBody className='p-6'>
<div className='flex items-center gap-4 mb-4'>
<div className='flex-1'>
<Input
placeholder='Enter symbol (BTC, ETH, DOGE, etc.)'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
size='lg'
variant='bordered'
classNames={{
input: 'text-white placeholder:text-slate-400',
inputWrapper:
'border-slate-600 bg-slate-700/50 hover:border-slate-500',
}}
/>
</div>
<Button
color='primary'
size='lg'
onPress={() => handleSearch()}
isLoading={isAnalyzing}
className='px-8 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700'>
{isAnalyzing ? 'Analyzing...' : 'Analyze'}
</Button>
</div>
{/* Quick Select Coins */}
<div className='flex items-center gap-3'>
<span className='text-sm text-slate-400'>Quick select:</span>
<div className='flex gap-2'>
{['BTC', 'ETH', 'SOL', 'ADA', 'DOGE'].map((coin) => (
<Chip
key={coin}
variant='flat'
className='cursor-pointer bg-slate-700/50 text-slate-300 hover:bg-slate-600/50 border-slate-600'
onClick={() => setSearchTerm(coin)}>
{coin}
</Chip>
))}
</div>
</div>
</CardBody>
</Card>
</div>
{/* Enhanced Loading State with Progress Bar */}
{isAnalyzing && (
<Card className='mb-10 bg-slate-800/40 border border-slate-700/30 backdrop-blur-2xl shadow-2xl overflow-hidden'>
<CardBody className='p-10'>
<div className='text-center'>
<div className='flex justify-center mb-8'>
<div className='relative'>
{/* Outer rotating ring */}
<div className='w-20 h-20 border-4 border-slate-700/50 rounded-full'></div>
{/* Main spinning ring */}
<div className='absolute top-0 left-0 w-20 h-20 border-4 border-transparent border-t-blue-500 border-r-purple-500 rounded-full animate-spin'></div>
{/* Inner pulsing dot */}
<div className='absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full animate-pulse'></div>
</div>
</div>
<h3 className='text-2xl font-bold bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent mb-3'>
Analyzing {searchTerm}
</h3>
<p className='text-slate-400 mb-8'>
Gathering comprehensive market intelligence...
</p>
{/* Progress Bar */}
<div className='max-w-lg mx-auto mb-8'>
<div className='flex items-center justify-between mb-3'>
<span className='text-sm text-slate-400'>Progress</span>
<span className='text-sm font-medium text-blue-400'>
{Math.round(progressPercent)}%
</span>
</div>
<div className='w-full bg-slate-700/50 rounded-full h-3 overflow-hidden'>
<div
className='h-full bg-gradient-to-r from-blue-500 via-purple-500 to-cyan-500 rounded-full transition-all duration-300 ease-out relative'
style={{ width: `${progressPercent}%` }}>
<div className='absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse'></div>
</div>
</div>
</div>
{/* Current Step Indicator */}
<div className='bg-slate-700/30 rounded-xl p-6 mb-6'>
<div className='flex items-center justify-center gap-3 mb-4'>
<div className='w-3 h-3 bg-blue-500 rounded-full animate-bounce'></div>
<h4 className='text-blue-400 font-bold'>
{progressStep < progressSteps.length
? progressSteps[progressStep].label
: 'Finalizing results'}
</h4>
</div>
<p className='text-slate-400 text-sm mb-3'>
{progressStep < progressSteps.length
? progressSteps[progressStep].description
: 'Preparing comprehensive analysis report...'}
</p>
{/* Sub-step messaging for AI analysis phases */}
{subStepMessage && (
<div className='flex items-center justify-center gap-2 mt-4 p-3 bg-slate-600/20 rounded-lg border border-slate-600/30'>
<div className='w-2 h-2 bg-cyan-400 rounded-full animate-pulse'></div>
<span className='text-cyan-300 text-sm font-medium'>
{subStepMessage}
</span>
</div>
)}
</div>
{/* Step Progress Indicators */}
<div className='grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3 max-w-6xl mx-auto'>
{[
{ icon: 'π', label: 'Connect', step: 0 },
{ icon: 'π', label: 'Fetch Data', step: 1 },
{ icon: 'β‘', label: 'Process', step: 2 },
{ icon: 'π§ ', label: 'AI Analysis', step: 3 },
{ icon: 'οΏ½', label: 'Patterns', step: 4 },
{ icon: 'π‘', label: 'Insights', step: 5 },
{ icon: 'π―', label: 'Recommend', step: 6 },
{ icon: 'β
', label: 'Complete', step: 7 },
].map((item, index) => (
<div
key={index}
className={`flex flex-col items-center gap-2 p-3 rounded-xl border transition-all duration-300 ${
progressStep >= item.step
? 'bg-blue-500/20 border-blue-500/30 text-blue-400'
: 'bg-slate-700/20 border-slate-600/20 text-slate-500'
}`}>
<div
className={`text-xl ${
progressStep >= item.step ? 'animate-bounce' : ''
}`}>
{item.icon}
</div>
<div className='text-xs font-medium text-center'>
{item.label}
</div>
{progressStep === item.step && (
<div className='w-1 h-1 bg-blue-400 rounded-full animate-ping'></div>
)}
</div>
))}
</div>
</div>
</CardBody>
</Card>
)}
{/* Results Display */}
{analysis && analysis.success && (
<div className='grid lg:grid-cols-3 gap-6'>
{/* Left Column - Main Analysis */}
<div className='lg:col-span-2 space-y-6'>
{/* Signal Card */}
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl'>
<CardBody className='p-6'>
<div className='flex items-center justify-between mb-6'>
<div>
<h2 className='text-2xl font-bold text-white mb-1'>
{analysis.symbol}
</h2>
<p className='text-slate-400 text-sm'>
{new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
<div className='text-right'>
<Chip
size='lg'
className={`font-bold text-white ${
analysis.recommendation === 'BUY'
? 'bg-gradient-to-r from-green-500 to-emerald-600'
: analysis.recommendation === 'SELL'
? 'bg-gradient-to-r from-red-500 to-rose-600'
: 'bg-gradient-to-r from-yellow-500 to-orange-600'
}`}>
{analysis.recommendation}
</Chip>
<div className='text-slate-300 text-sm mt-2'>
{analysis.confidence}% Confidence
</div>
</div>
</div>
{/* Reasoning */}
<div className='bg-slate-700/30 rounded-lg p-4 mb-6'>
<h4 className='text-white font-semibold mb-2'>
Analysis Reasoning
</h4>
<p className='text-slate-300 text-sm leading-relaxed'>
{analysis.reasoning}
</p>
</div>
{/* Social Sentiment */}
<div className='flex items-center gap-3'>
<span className='text-slate-400 text-sm'>
Social Sentiment:
</span>
<Chip
variant='flat'
className={`${
analysis.social_sentiment === 'bullish'
? 'bg-green-500/20 text-green-400 border-green-500/30'
: analysis.social_sentiment === 'bearish'
? 'bg-red-500/20 text-red-400 border-red-500/30'
: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
}`}>
{analysis?.social_sentiment?.toUpperCase()}
</Chip>
</div>
</CardBody>
</Card>
{/* Key Metrics - Show on mobile above chart */}
{analysis.key_metrics && (
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl lg:hidden'>
<CardBody className='p-6'>
<h4 className='text-white font-semibold mb-4'>
Market Metrics
</h4>
<div className='space-y-3'>
{Object.entries(analysis.key_metrics)
.filter(
([, value]) => value !== null && value !== undefined
)
.map(([key, value]) => {
const formatValue = (
key: string,
value: string | number
) => {
if (typeof value !== 'number') return value;
// Price formatting
if (key.includes('price')) {
return `$${value.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
// Market cap and volume formatting
if (key.includes('cap') || key.includes('volume')) {
if (value >= 1e12)
return `$${(value / 1e12).toFixed(2)}T`;
if (value >= 1e9)
return `$${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6)
return `$${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3)
return `$${(value / 1e3).toFixed(2)}K`;
return `$${value.toLocaleString()}`;
}
// Score/rank formatting (no decimals)
if (
key.includes('score') ||
key.includes('rank') ||
key.includes('dominance')
) {
return value.toFixed(0);
}
// Social metrics formatting
if (
key.includes('mentions') ||
key.includes('engagements') ||
key.includes('creators')
) {
if (value >= 1e6)
return `${(value / 1e6).toFixed(1)}M`;
if (value >= 1e3)
return `${(value / 1e3).toFixed(1)}K`;
return value.toLocaleString();
}
return value.toLocaleString();
};
return (
<div
key={key}
className='flex items-center justify-between py-2'>
<span className='text-slate-400 text-sm capitalize'>
{key.replace(/_/g, ' ')}
</span>
<span className='text-white font-semibold text-sm'>
{formatValue(key, value as string | number)}
</span>
</div>
);
})}
</div>
</CardBody>
</Card>
)}
{/* Price Chart */}
{analysis.chart_data && analysis.chart_data.length > 0 && (
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl'>
<CardBody className='p-6'>
<ChartComponent
data={analysis.chart_data}
symbol={analysis.symbol}
/>
</CardBody>
</Card>
)}
{/* AI Analysis */}
{analysis.ai_analysis &&
typeof analysis.ai_analysis === 'object' && (
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl'>
<CardBody className='p-6'>
<h4 className='text-white font-semibold mb-4'>
AI Deep Analysis
</h4>
{/* Summary */}
{analysis.ai_analysis.summary && (
<div className='mb-6 p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg'>
<h5 className='text-blue-400 font-medium mb-2'>
Executive Summary
</h5>
<p className='text-slate-300 text-sm leading-relaxed'>
{analysis.ai_analysis.summary}
</p>
</div>
)}
{/* Pros and Cons */}
<div className='grid md:grid-cols-2 gap-4'>
{/* Pros */}
{analysis.ai_analysis.pros && (
<div className='p-4 bg-green-500/10 border border-green-500/20 rounded-lg'>
<h5 className='text-green-400 font-medium mb-3 flex items-center gap-2'>
<span className='w-2 h-2 bg-green-400 rounded-full'></span>
Bullish Factors
</h5>
<ul className='space-y-2'>
{analysis.ai_analysis.pros.map(
(pro: string, index: number) => (
<li
key={index}
className='text-slate-300 text-sm flex items-start gap-2'>
<span className='text-green-400 mt-1'>
β’
</span>
<span>{pro}</span>
</li>
)
)}
</ul>
</div>
)}
{/* Cons */}
{analysis.ai_analysis.cons && (
<div className='p-4 bg-red-500/10 border border-red-500/20 rounded-lg'>
<h5 className='text-red-400 font-medium mb-3 flex items-center gap-2'>
<span className='w-2 h-2 bg-red-400 rounded-full'></span>
Risk Factors
</h5>
<ul className='space-y-2'>
{analysis.ai_analysis.cons.map(
(con: string, index: number) => (
<li
key={index}
className='text-slate-300 text-sm flex items-start gap-2'>
<span className='text-red-400 mt-1'>β’</span>
<span>{con}</span>
</li>
)
)}
</ul>
</div>
)}
</div>
{/* Key Factors */}
{analysis.ai_analysis.key_factors && (
<div className='mt-4 p-4 bg-purple-500/10 border border-purple-500/20 rounded-lg'>
<h5 className='text-purple-400 font-medium mb-3'>
Key Factors to Monitor
</h5>
<div className='flex flex-wrap gap-2'>
{analysis.ai_analysis.key_factors.map(
(factor: string, index: number) => (
<Chip
key={index}
variant='flat'
className='bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs text-wrap h-12'>
{factor}
</Chip>
)
)}
</div>
</div>
)}
</CardBody>
</Card>
)}
</div>
{/* Right Column - Metrics */}
<div className='space-y-6'>
{/* Key Metrics - Hidden on mobile, shown on desktop */}
{analysis.key_metrics && (
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl hidden lg:block'>
<CardBody className='p-6'>
<h4 className='text-white font-semibold mb-4'>
Market Metrics
</h4>
<div className='space-y-3'>
{Object.entries(analysis.key_metrics)
.filter(
([, value]) => value !== null && value !== undefined
)
.map(([key, value]) => {
const formatValue = (
key: string,
value: string | number
) => {
if (typeof value !== 'number') return value;
// Price formatting
if (key.includes('price')) {
return `$${value.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
// Market cap and volume formatting
if (key.includes('cap') || key.includes('volume')) {
if (value >= 1e12)
return `$${(value / 1e12).toFixed(2)}T`;
if (value >= 1e9)
return `$${(value / 1e9).toFixed(2)}B`;
if (value >= 1e6)
return `$${(value / 1e6).toFixed(2)}M`;
if (value >= 1e3)
return `$${(value / 1e3).toFixed(2)}K`;
return `$${value.toLocaleString()}`;
}
// Score/rank formatting (no decimals)
if (
key.includes('score') ||
key.includes('rank') ||
key.includes('dominance')
) {
return value.toFixed(0);
}
// Social metrics formatting
if (
key.includes('mentions') ||
key.includes('engagements') ||
key.includes('creators')
) {
if (value >= 1e6)
return `${(value / 1e6).toFixed(1)}M`;
if (value >= 1e3)
return `${(value / 1e3).toFixed(1)}K`;
return value.toLocaleString();
}
return value.toLocaleString();
};
return (
<div
key={key}
className='flex items-center justify-between py-2'>
<span className='text-slate-400 text-sm capitalize'>
{key.replace(/_/g, ' ')}
</span>
<span className='text-white font-semibold text-sm'>
{formatValue(key, value as string | number)}
</span>
</div>
);
})}
</div>
</CardBody>
</Card>
)}
{/* MCP Status */}
<Card className='bg-slate-800/50 border-slate-700/50 backdrop-blur-xl'>
<CardBody className='p-6'>
<h4 className='text-white font-semibold mb-4'>
Data Sources
</h4>
<div className='space-y-3'>
<div className='flex items-center gap-3'>
<div className='w-2 h-2 bg-green-500 rounded-full'></div>
<span className='text-slate-300 text-sm'>
LunarCrush MCP
</span>
</div>
<div className='flex items-center gap-3'>
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
<span className='text-slate-300 text-sm'>
Google Gemini AI
</span>
</div>
<div className='flex items-center gap-3'>
<div className='w-2 h-2 bg-purple-500 rounded-full'></div>
<span className='text-slate-300 text-sm'>
Real-time Analysis
</span>
</div>
</div>
</CardBody>
</Card>
{/* Disclaimer */}
<Card className='bg-amber-500/10 border-amber-500/20 backdrop-blur-xl'>
<CardBody className='p-4'>
<h5 className='text-amber-400 font-medium text-sm mb-2'>
β οΈ Disclaimer
</h5>
<p className='text-amber-300/80 text-xs leading-relaxed'>
This analysis is for informational purposes only and should
not be considered financial advice. Always do your own
research before making investment decisions.
</p>
</CardBody>
</Card>
</div>
</div>
)}
{/* Error State */}
{analysis && !analysis.success && (
<Card className='bg-red-500/10 border-red-500/20 backdrop-blur-xl'>
<CardBody className='p-6'>
<h3 className='text-red-400 font-semibold mb-2'>
Analysis Error
</h3>
<p className='text-red-300 text-sm'>
{analysis.error ||
'An error occurred during analysis. Please try again.'}
</p>
</CardBody>
</Card>
)}
</div>
{/* Footer */}
<footer className='relative mt-20 border-t border-slate-700/30 bg-slate-900/80 backdrop-blur-2xl'>
<div className='max-w-7xl mx-auto px-6 py-12'>
<div className='grid md:grid-cols-4 gap-8 mb-8'>
{/* Main Info */}
<div className='md:col-span-1'>
<div className='flex items-center gap-3 mb-4'>
<div className='w-10 h-10 bg-gradient-to-br from-blue-500 via-purple-500 to-cyan-500 rounded-xl flex items-center justify-center'>
<span className='text-white font-bold text-sm'>LC</span>
</div>
<div>
<h3 className='text-lg font-bold text-white'>
LunarCrush AI
</h3>
<p className='text-xs text-slate-400'>Trading Terminal</p>
</div>
</div>
<p className='text-slate-400 text-sm leading-relaxed mb-4'>
Intelligent trading signals powered by social sentiment analysis
and Google Gemini AI.
</p>
<a
href='https://github.com/danilobatson/lunarcrush_mcp'
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-2 text-blue-400 hover:text-blue-300 text-sm font-medium transition-colors'>
<span>π</span>
View Source Code
</a>
</div>
{/* Built With */}
<div>
<h4 className='text-white font-bold mb-4'>Built With</h4>
<div className='space-y-3'>
<a
href='https://remix.run/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-blue-400 rounded-full'></span>
Remix - Full Stack Framework
</a>
<a
href='https://www.typescriptlang.org/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-blue-400 rounded-full'></span>
TypeScript - Type Safety
</a>
<a
href='https://tailwindcss.com/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-cyan-400 rounded-full'></span>
Tailwind CSS - Styling
</a>
<a
href='https://heroui.com/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-purple-400 rounded-full'></span>
HeroUI - Components
</a>
</div>
</div>
{/* Powered By */}
<div>
<h4 className='text-white font-bold mb-4'>Powered By</h4>
<div className='space-y-3'>
<a
href='https://lunarcrush.com/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-green-400 rounded-full'></span>
LunarCrush - Social Analytics
</a>
<a
href='https://ai.google.dev/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-yellow-400 rounded-full'></span>
Google Gemini - AI Analysis
</a>
<a
href='https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#writing-mcp-clients'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-purple-400 rounded-full'></span>
MCP TypeScript SDK
</a>
<a
href='https://vite.dev/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-orange-400 rounded-full'></span>
Vite - Build Tool
</a>
</div>
</div>
{/* Resources */}
<div>
<h4 className='text-white font-bold mb-4'>Resources</h4>
<div className='space-y-3'>
<a
href='https://lunarcrush.com/about/api'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-blue-400 rounded-full'></span>
LunarCrush API Docs
</a>
<a
href='https://ai.google.dev/docs'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-yellow-400 rounded-full'></span>
Gemini AI Documentation
</a>
<a
href='https://vercel.com/'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-purple-400 rounded-full'></span>
Vercel - Deployment
</a>
<a
href='https://remix.run/docs'
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors'>
<span className='w-2 h-2 bg-cyan-400 rounded-full'></span>
Remix Documentation
</a>
</div>
</div>
</div>
{/* Bottom Section */}
<div className='pt-8 border-t border-slate-700/30'>
<div className='flex flex-col md:flex-row items-center justify-between gap-4'>
<div className='text-slate-400 text-sm'>
Β© 2025 LunarCrush MCP Powered AI Trading Agent. Built for
demonstration purposes.
</div>
<div className='flex items-center gap-1 text-sm text-slate-400'>
<span>Powered by:</span>
<a
href='https://lunarcrush.com/'
target='_blank'
rel='noopener noreferrer'
className='text-blue-400 hover:text-blue-300 transition-colors'>
LunarCrush
</a>
<span>β’</span>
<a
href='https://ai.google.dev/'
target='_blank'
rel='noopener noreferrer'
className='text-yellow-400 hover:text-yellow-300 transition-colors'>
Gemini AI
</a>
<span>β’</span>
<a
href='https://danilobatson.github.io/'
target='_blank'
rel='noopener noreferrer'
className='text-purple-400 hover:text-purple-300 transition-colors'>
Developer Portfolio
</a>
</div>
</div>
</div>
</div>
</footer>
</div>
);
}
EOF
Create the Analysis API Endpoint
Server-Side MCP Integration
# Create the analysis API route
cat > app/routes/api.analyze.ts << 'EOF'
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { GoogleGenAI } from '@google/genai';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import type {
TradingAnalysis,
GeminiResponse,
McpTool,
ToolCall,
ToolResult
} from '../../types';
async function geminiFlashResponse(
ai: GoogleGenAI,
contents: string
): Promise<GeminiResponse> {
return await ai.models.generateContent({
model: 'gemini-2.0-flash-lite',
contents: contents,
});
}
function createOrchestrationPrompt(
symbol: string,
availableTools: Record<string, McpTool>
): string {
return `
You are a cryptocurrency analyst. I need you to analyze ${symbol.toUpperCase()} using the available LunarCrush MCP tools. Use a MAX of four tools.
AVAILABLE MCP TOOLS:
${JSON.stringify(availableTools, null, 2)}
TASK: Create a plan to gather comprehensive data for ${symbol.toUpperCase()} trading analysis.
Based on the available tools, decide which tools to call and with what parameters to get:
1. Current price and market data
2. Social sentiment metrics
3. Historical performance data
4. Ranking and positioning data
5. Get one week price historical time series data for charting purposes. Look only for the price metrics.
Prioritize getting data for the one week price chart. The price chart is important. If you don't get data back in the response try a few different solutions to get the data (e.g. try the name of the coin FIRST then try the symbol). When using the Cryptocurrencies tool it should have no limit but sorted by market cap.
Respond with a JSON array of tool calls in this exact format:
[
{
"tool": "tool_name",
"args": {"param": "value"},
"reason": "Short reason why this tool call is needed"
}
]
Be specific with parameters. For example, if you need to find ${symbol} in a list first, plan that step.
`;
}
async function executeGeminiToolChoices(
symbol: string,
orchestrationResponse: GeminiResponse,
client: Client
): Promise<Record<string, unknown>> {
console.time(`ToolExecution for ${symbol}`);
try {
const responseText =
orchestrationResponse.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
console.log('π€ Gemini orchestration response:', responseText);
// Extract JSON array from response
const jsonMatch = responseText?.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
console.log('β οΈ No JSON array found, using fallback');
console.timeEnd(`ToolExecution for ${symbol}`);
return {
symbol: symbol.toUpperCase(),
toolResults: [],
error: 'No tool calls found in response',
};
}
const toolCalls: ToolCall[] = JSON.parse(jsonMatch[0]);
const gatheredData: {
symbol: string;
toolResults: Array<{
tool: string;
args: Record<string, unknown>;
reason: string;
result?: unknown;
error?: string;
}>;
} = {
symbol: symbol.toUpperCase(),
toolResults: [],
};
// Execute tool calls concurrently with Promise.all
const toolPromises = toolCalls.map(async (toolCall: ToolCall) => {
try {
// Check if tool is Cryptocurrencies and cached
console.log(`π οΈ Executing: ${toolCall.tool} - ${toolCall.reason}`);
const result = await client.callTool({
name: toolCall.tool,
arguments: toolCall.args,
});
return {
tool: toolCall.tool,
args: toolCall.args,
reason: toolCall.reason,
result,
};
} catch (error) {
console.error(`β Tool ${toolCall.tool} failed:`, error);
return {
tool: toolCall.tool,
args: toolCall.args,
reason: toolCall.reason,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
});
gatheredData.toolResults = (await Promise.all(
toolPromises
)) as ToolResult[];
console.timeEnd(`ToolExecution for ${symbol}`);
return gatheredData;
} catch (error) {
console.error('β Error executing tool choices:', error);
console.timeEnd(`ToolExecution for ${symbol}`);
return {
symbol: symbol.toUpperCase(),
toolResults: [],
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
function createAnalysisPrompt(
symbol: string,
gatheredData: Record<string, unknown>
): string {
return `
You are an expert cryptocurrency analyst. Analyze the following data for ${symbol.toUpperCase()} gathered from LunarCrush MCP tools and provide a comprehensive trading recommendation. Keep it short for faster response times.
GATHERED DATA FROM MCP TOOLS:
${JSON.stringify(gatheredData, null, 2)}
ANALYSIS REQUIREMENTS:
Based on the above data from the MCP tools, provide a detailed trading analysis. Look for:
1. CURRENT MARKET DATA:
- Real current price (not demo data)
- Market cap and volume
- Recent performance metrics
2. SOCIAL SENTIMENT:
- Social mentions and engagement
- Galaxy Score and health indicators
- Community sentiment trends
3. POSITIONING DATA:
- AltRank and market positioning
- Relative performance vs other cryptocurrencies
4. CHART DATA:
- Price trends over the last week
- Could be used to create a chart
- Could be under close instead of price
- If you find price/time series data, include ONLY 12AM and 12PM time data points
- Format as: [{"time": "2025-06-10 07:00", "close": 2675.51}, ...]
- Keep chart_data small to prevent response truncation
Respond with a JSON array of tool calls in this exact format:
{
"recommendation": "BUY|SELL|HOLD",
"confidence": 0-100,
"reasoning": "Brief explanation of the recommendation",
"social_sentiment": "bullish|bearish|neutral",
"key_metrics": {
"price": "actual price from MCP data",
"galaxy_score": "score from data",
"alt_rank": "rank from data",
"social_dominance": "dominance from data",
"market_cap": "cap from data",
"volume_24h": "volume from data",
"mentions": "mentions from data",
"engagements": "engagements/interactions from data",
"creators": "creators from data"
},
"ai_analysis": {
"summary": "1-2 sentence overview of the analysis",
"pros": ["Positive factor 1", "Positive factor 2", "etc"],
"cons": ["Risk factor 1", "Risk factor 2", "etc"],
"key_factors": ["Important factor to monitor 1", "Important factor 2", "etc"]
},
"chart_data": [{"time": "2025-06-10 07:00", "close": 2675.51}],
"miscellaneous": "Any other relevant insights"
}
IMPORTANT:
- Use ONLY actual data from the MCP tools, not placeholder values
- Make the analysis beginner-friendly, concise, and educational
- Focus on explaining WHY the recommendation is made
- Extract real metrics from the gathered data
- Do not break JSON response format instructed above
`;
}
function parseAnalysisResponse(
response: GeminiResponse,
symbol: string,
cryptoData: Record<string, unknown>
): TradingAnalysis {
try {
const text = response.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
if (!text) {
throw new Error('No response from Gemini');
}
console.log('π€ Gemini raw response:', text);
// Extract JSON from response with better handling
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in Gemini response');
}
let jsonText = jsonMatch[0];
// Handle truncated JSON by trying to fix common issues
if (!jsonText.endsWith('}')) {
// Find the last complete field before truncation
const lastCompleteField = jsonText.lastIndexOf('"}');
if (lastCompleteField > 0) {
jsonText = jsonText.substring(0, lastCompleteField + 2) + '}';
}
}
const analysis = JSON.parse(jsonText);
// Validate and format response
return {
symbol: symbol.toUpperCase(),
recommendation: analysis.recommendation || 'HOLD',
confidence: analysis.confidence || 50,
reasoning: analysis.reasoning || 'Analysis completed',
social_sentiment: analysis.social_sentiment || 'neutral',
key_metrics: analysis.key_metrics,
ai_analysis:
analysis.ai_analysis || analysis.reasoning || 'AI analysis completed',
timestamp: new Date().toISOString(),
chart_data: transformChartData(analysis.chart_data || []),
success: true, // Add success property
};
} catch (error) {
console.error('β Error parsing Gemini response:', error);
// Fallback response
return {
symbol: symbol.toUpperCase(),
recommendation: 'HOLD',
confidence: 50,
reasoning: 'Analysis completed with limited data',
social_sentiment: 'neutral',
key_metrics: cryptoData || {}, // Fix: remove 'this.' and use fallback
ai_analysis: {
summary: 'Unable to complete full AI analysis. Please try again.',
pros: [],
cons: ['Analysis parsing failed'],
key_factors: [],
},
timestamp: new Date().toISOString(),
chart_data: [],
success: true, // Add success property even for fallback
};
}
}
function transformChartData(
chartData: Array<{
time?: string;
date?: string;
close?: number;
price?: number;
}>
): Array<{ date: string; price: number }> {
if (!Array.isArray(chartData) || chartData.length === 0) {
return [];
}
// Transform and filter valid data points
const transformedData = chartData
.map((item) => ({
date: item.time || item.date || '',
price: item.close || item.price || 0,
}))
.filter((item) => item.date && item.price > 0)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
return transformedData;
}
// Initialize Gemini AI with server-side API key
const getGeminiAI = () => {
const apiKey = process.env.GOOGLE_GEMINI_API_KEY;
if (!apiKey) {
throw new Error('GOOGLE_GEMINI_API_KEY environment variable is required');
}
return new GoogleGenAI({ apiKey });
};
// Create a server-side MCP client using official SDK
async function createMCPClient(apiKey: string): Promise<Client> {
console.log('π Initializing MCP client with official SDK...');
// Create SSE transport for LunarCrush MCP server
const transport = new SSEClientTransport(
new URL(`https://lunarcrush.ai/sse?key=${apiKey}`)
);
// Create MCP client
const client = new Client(
{
name: 'lunarcrush-mcp-trading',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Connect to the server
await client.connect(transport);
console.log('β
MCP client connected successfully');
return client;
}
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
let client: Client | null = null;
try {
const formData = await request.formData();
const symbol = formData.get('symbol') as string;
if (!symbol) {
return json({ error: 'Symbol is required' }, { status: 400 });
}
console.log(`π Starting server-side MCP analysis for ${symbol}`);
// Step 1: Create and connect MCP client
const apiKey = process.env.LUNARCRUSH_API_KEY;
if (!apiKey) {
throw new Error('LUNARCRUSH_API_KEY environment variable is required');
}
client = await createMCPClient(apiKey);
const ai = getGeminiAI();
console.log('π MCP client initialized successfully');
console.log(`π Fetching available MCP tools...`);
const { tools } = await client.listTools();
console.log(
`π Available MCP tools: ${tools.map((t) => t.name).join(', ')}`
);
const chooseToolsPrompt = createOrchestrationPrompt(symbol, tools as unknown as Record<string, McpTool>);
console.log(`π€ Letting Gemini choose tools for ${symbol} analysis...`);
const chooseToolsResponse = await geminiFlashResponse(
ai,
chooseToolsPrompt
);
// Step 2: Execute Gemini's tool choices to gather data
console.log('π€ Gemini orchestrator response:', chooseToolsResponse);
const gatheredData = await executeGeminiToolChoices(
symbol,
chooseToolsResponse,
client
);
const analysisPrompt = createAnalysisPrompt(symbol, gatheredData);
console.log('π§ Generating final analysis...');
// Step 3: Let Gemini analyze the collected data
const analysisResponse = await geminiFlashResponse(ai, analysisPrompt);
console.log('π€ Gemini final analysis response:', analysisResponse);
return json({
data: parseAnalysisResponse(analysisResponse, symbol, gatheredData),
success: true,
});
} catch (error) {
console.error('Server-side MCP analysis error:', error);
return json(
{
success: false,
error: error instanceof Error ? error.message : 'Analysis failed',
},
{ status: 500 }
);
} finally {
// Clean up MCP client connection
if (client) {
try {
await client.close();
console.log('π§Ή MCP client connection closed');
} catch (cleanupError) {
console.warn('Warning: MCP client cleanup failed:', cleanupError);
}
}
}
}
EOF
Testing & Deployment
Local Testing
# Start your development server
npm run dev # Remix app (localhost:5173)
Verify Everything Works:
- π Dashboard loads: Visit localhost:5173
- π MCP Analysis triggers: Enter a symbol like "BTC" and click "Analyze"
- π Progress tracking works: Watch the 6-step progress complete
- πΎ Quick select works: Try the BTC, ETH, SOL buttons
- π Charts render: Toggle between Simple and Advanced chart views
- π Cache works: Re-analyze the same symbol for instant results
π Pro Debugging Tip: Use browser dev tools Network tab to monitor API calls and check the MCP connection status indicator in the header.
Deploy to Production
Deploy on Vercel (Recommended):
- Push to GitHub:
git init
git add .
git commit -m "LunarCrush MCP AI Trading Terminal with Remix"
git branch -M main
git remote add origin https://github.com/danilobatson/lunarcrush_mcp.git
git push -u origin main
Connect to Vercel:
β’ Visit vercel.com and sign up
β’ Import your GitHub repository
β’ Configure project settings for RemixAdd Environment Variables in Vercel:
β’ Go to Settings β Environment Variables
β’ Add all variables from your.env.local
:
LUNARCRUSH_API_KEY=lc_your_api_key_here
GOOGLE_GEMINI_API_KEY=your_gemini_key_here
- Deploy: β’ Click "Deploy" in Vercel β’ Wait for deployment to complete β’ Test your live application
Live Example: You can see a deployed version at https://lunarcrush-mcp.vercel.app/
Troubleshooting Common Issues
Here are solutions to the most common problems:
Issue | Symptoms | Solution |
---|---|---|
MCP Connection Status Red | Red indicator in header, analysis fails | Check environment variables and API key validity. Restart dev server. |
Charts Not Loading | "Loading chart..." never completes | Verify recharts dependency installed: npm install recharts
|
Quick Select Not Working | Buttons don't trigger analysis | Check browser console for JavaScript errors, ensure React state updates |
Environment Variables Missing | "API key not configured" errors | Ensure both LUNARCRUSH_API_KEY and GOOGLE_GEMINI_API_KEY in .env.local
|
Gemini API Errors | "AI analysis failed" messages | Check Google AI quota at aistudio.google.com, verify API key format |
Cache Issues | Stale analysis results | Clear browser sessionStorage: sessionStorage.clear() in dev tools console |
Build Errors | TypeScript or import errors | Run npm install and check all imports match the actual file structure |
Level Up: Adding Advanced Features
Want to make this even more impressive? Here are some powerful extensions:
AI Enhancement Prompts
Real-time WebSocket Integration:
"Add WebSocket integration to this Remix trading terminal for real-time price updates. Connect to the existing MCP infrastructure and add live price streaming with connection status indicators and automatic reconnection logic."
Portfolio Management Dashboard:
"Extend this MCP trading terminal with a portfolio tracking feature. Allow users to save multiple cryptocurrency positions, track profit/loss calculations, and get AI-powered portfolio recommendations using the existing analysis structure."
Multi-Asset Analysis:
"Add batch analysis capabilities to analyze multiple cryptocurrencies simultaneously using MCP tool orchestration. Include comparison views, correlation analysis, and portfolio optimization recommendations."
Ready to Scale?
LunarCrush MCP Server Benefits:
β’ 11+ Specialized Tools for comprehensive social intelligence
β’ Real-time Data Streaming with protocol-level optimization
β’ Enterprise-Grade Rate Limiting built into the MCP layer
β’ AI-Native Integration designed for intelligent orchestration
MCP Ecosystem Expansion:
β’ Explore other MCP servers for different data sources (news, social media, market data)
β’ Build custom MCP tools for proprietary data sources
β’ Integrate multiple MCP servers for comprehensive multi-source analysis
β’ Create MCP tool chains for complex multi-step analysis workflows
Conclusion
Congratulations! You've successfully built a production-ready AI Trading Terminal that demonstrates cutting-edge development patterns with Model Context Protocol.
What You've Accomplished
β’ β
MCP Integration - Secure AI-to-data connections with standardized protocols
β’ β
AI Orchestration - Gemini intelligently selecting and combining data tools
β’ β
Real-time Processing - Live progress tracking through complex workflows
β’ β
Modern Architecture - Remix SSR with advanced React patterns and hooks
β’ β
Professional UI/UX - Beautiful, responsive interface with dual chart modes
β’ β
Production Ready - Comprehensive error handling, caching, and deployment configuration
β’ β
Performance Optimized - Smart caching, lazy loading, and efficient state management
What's Next?
Extend Your Trading Terminal:
β’ Add multi-cryptocurrency comparison and correlation analysis
β’ Implement advanced portfolio management and position tracking
β’ Create custom alert systems with email/SMS notifications via MCP tools
β’ Build sophisticated technical analysis with multiple chart indicators
Production Optimizations:
β’ Set up comprehensive monitoring with Sentry or LogRocket
β’ Implement advanced rate limiting and request validation
β’ Add Redis for distributed server-side caching
Advanced MCP Features:
β’ Multi-server MCP orchestration with different data providers
β’ Custom MCP tool development for proprietary data sources
β’ Real-time data streaming with enhanced MCP protocol features
β’ Complex AI agent workflows with multiple AI providers and decision trees
Key Technical Insights
MCP Protocol Advantages Demonstrated:
- Tool Discovery: Dynamic detection of available data sources
- Intelligent Orchestration: AI-driven selection of optimal tools
- Protocol-Level Error Handling: Robust connection management
- Standardized Responses: Consistent data formats across tools
- Performance Optimization: Efficient data retrieval and caching
React/Remix Patterns Showcased:
- Lazy Loading: Charts loaded on-demand for better performance
- Smart Caching: SessionStorage integration for instant re-analysis
- Real-time State Management: Progress tracking with useEffect hooks
- Error Boundaries: Graceful degradation without page crashes
- Responsive Design: Mobile-first approach with HeroUI components
π Take Action
Get Started Now:
- Subscribe To LunarCrush API - Get access to unique social metrics and MCP tools
- Fork the Repository - Start building your own enhanced version
- Join the Developer Community - Share your enhancements and improvements
Share Your Success:
β’ Tweet your deployed app with #LunarCrush #MCP #AI #Remix
β’ Write about your MCP development experience on LinkedIn
β’ Submit to developer showcases and tech community platforms
β’ Create video walkthroughs demonstrating the MCP integration
Learn More:
β’ Dive deeper into MCP protocol documentation and best practices
β’ Explore advanced Remix patterns for SSR and performance optimization
β’ Experiment with other AI providers (Claude, OpenAI) for comparison
β’ Build custom MCP servers for your own data sources
Resources & Documentation
β’ LunarCrush API Documentation - Complete API reference
β’ Google Gemini AI Documentation - AI model capabilities and limits
β’ Remix Framework Documentation - Full-stack web development
β’ HeroUI Component Library - React component system
π Complete GitHub Repository
Built with β€οΈ using LunarCrush β’ Google Gemini β’ Remix β’ HeroUI
Questions or Issues? Drop them below! I respond to every comment and love helping fellow developers build amazing applications with MCP and AI integration. π
Ready to revolutionize how AI accesses real-time data? Start building your MCP-powered AI trading terminal today and join the next generation of intelligent application development!
Use my discount referral code JAMAALBUILDS to receive 15% off your plan.
Top comments (0)