Skip to content

Agent State + Candlestick graphs #28

Description

@SumanthPal

GitHub Issue: Frontend Real-Time Portfolio Polling & Live Updates

🎯 Overview

Implement real-time portfolio updates in the frontend by polling backend API endpoints. Backend calculates portfolio values using cached market data from CoinGecko/Coinbase, frontend displays live updates without making direct calls to price providers.

🔴 Problem Statement

Frontend needs to display:

  • Live portfolio values for each agent (cash + holdings at current prices)
  • Current market prices for all supported tokens
  • Performance metrics (P&L, ROI, win rate) updated in real-time
  • Trade history with real-time additions

Current State:

  • No backend endpoints exist to serve enriched portfolio data
  • No price caching mechanism in backend
  • No polling infrastructure in frontend
  • Frontend can't display live portfolio values without calling external APIs directly (rate limit risk)

🎯 Goals

  • Backend calculates portfolio values with cached price data
  • Create REST endpoints for portfolio, market prices, and trade history
  • Implement price caching in MarketDataTool (5-second TTL)
  • Frontend polls portfolio data every 3-5 seconds
  • Display live portfolio updates in UI
  • Show real-time market overview with current prices

📐 Architecture Decision

✅ CHOSEN: Backend Aggregation + Frontend Polling

┌─────────────────┐
│    Frontend     │ Polls every 5s
│                 │──────────────────┐
│  - Portfolio    │                  │
│  - Market Data  │                  ▼
│  - Trade Feed   │         ┌────────────────────┐
└─────────────────┘         │   Backend API      │
                            │                    │
                            │  ┌──────────────┐  │
                            │  │ Price Cache  │  │ Fetch prices
                            │  │ (5s TTL)     │◄─┼────────────┐
                            │  └──────────────┘  │            │
                            │         │          │            │
                            │         ▼          │            │
                            │  Calculate:        │            │
                            │  cash + Σ(holdings │            │
                            │    × cached_price) │            │
                            └────────────────────┘            │
                                                              │
                                                    ┌─────────▼────────┐
                                                    │   CoinGecko API  │
                                                    │   (Rate Limited) │
                                                    └──────────────────┘

Why This Approach?

✅ Pros:

  • Single source of truth - Backend owns price data
  • Rate limit protection - Backend controls API call frequency with caching
  • Consistent pricing - All agents use same price snapshot
  • Simple frontend - Just polls one endpoint, no API key management
  • Better UX - Can show last-updated timestamp
  • Scalable - Easy to add WebSockets later

❌ Rejected Alternative: Frontend Calls CoinGecko Directly

  • Multiple rate limit risk (every user polling = N×rate)
  • Inconsistent data between backend agent decisions and frontend display
  • Duplicate API calls (backend already fetching for trades)
  • Frontend complexity (API key management, error handling)
  • Can't cache effectively (each browser instance separate)

📝 Backend Implementation

1. Add Price Caching to MarketDataTool

File: tools/market_data.py

from datetime import datetime, timedelta
from typing import Dict, Optional

class MarketDataTool:
    def __init__(self, api_base: str = None, api_key: str = None):
        # ... existing code ...
        
        # Price caching to avoid hitting rate limits
        self._price_cache: Dict[str, float] = {}
        self._cache_timestamp: Optional[datetime] = None
        self._cache_ttl_seconds: int = 5  # 5-second cache
    
    def _is_cache_valid(self) -> bool:
        """Check if cached prices are still fresh."""
        if not self._cache_timestamp:
            return False
        age = (datetime.utcnow() - self._cache_timestamp).total_seconds()
        return age < self._cache_ttl_seconds
    
    def get_all_prices(self) -> Dict[str, float]:
        """
        Get current USD prices for all supported tokens with caching.
        
        Returns:
            Dict[str, float]: {"BTC": 42000.50, "ETH": 2300.25, ...}
        """
        # Return cached prices if still valid
        if self._is_cache_valid():
            return self._price_cache.copy()
        
        # Fetch fresh prices
        prices = {}
        for token in self.supported_tokens.keys():
            try:
                price_data = self.get_price(token)
                prices[token] = price_data[token.lower()]['usd']
            except Exception as e:
                # Fallback to last known price if fetch fails
                if token in self._price_cache:
                    prices[token] = self._price_cache[token]
                    logger.warning(f"Using cached price for {token} due to error: {e}")
                else:
                    logger.error(f"Failed to fetch {token} price: {e}")
        
        # Update cache
        self._price_cache = prices
        self._cache_timestamp = datetime.utcnow()
        
        return prices.copy()

2. Create FastAPI Routes

File: api/routes/agents.py

from fastapi import APIRouter, HTTPException
from typing import Dict, Any, List
from datetime import datetime
from decimal import Decimal

router = APIRouter(prefix="/api/agents", tags=["agents"])

# Shared market tool instance for caching
_market_tool = MarketDataTool()


@router.get("/{agent_id}/portfolio")
async def get_agent_portfolio(agent_id: int) -> Dict[str, Any]:
    """
    Get agent's current portfolio with live market values.
    
    Returns:
        - cash: Available USD cash
        - holdings: {token: {quantity, price, value}}
        - holdings_value: Total value of all holdings
        - total_value: cash + holdings_value
        - performance: P&L, ROI, win rate, etc.
        - timestamp: When prices were fetched
    """
    # Get agent from in-memory store (or MCP server)
    if agent_id not in ACTIVE_AGENTS:
        raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
    
    agent = ACTIVE_AGENTS[agent_id]
    
    # Get agent's holdings
    holdings = agent.get_holdings()  # {"ETH": 1.5, "BTC": 0.25}
    cash = agent.get_available_cash()
    
    # Get current market prices (cached!)
    all_prices = _market_tool.get_all_prices()
    
    # Calculate holdings value with current prices
    holdings_value = 0.0
    holdings_enriched = {}
    
    for token, quantity in holdings.items():
        price = all_prices.get(token, 0.0)
        value = quantity * price
        holdings_value += value
        
        holdings_enriched[token] = {
            "quantity": quantity,
            "current_price": price,
            "total_value": value
        }
    
    # Total portfolio value
    total_value = cash + holdings_value
    
    # Get performance metrics from portfolio
    portfolio = agent.get_portfolio_status()
    
    return {
        "agent_id": agent_id,
        "cash": cash,
        "holdings": holdings_enriched,
        "holdings_value": holdings_value,
        "total_value": total_value,
        "performance": {
            "starting_value": portfolio.starting_val,
            "realized_pnl": portfolio.realized_pnl,
            "unrealized_pnl": portfolio.unrealized_pnl,
            "total_pnl": portfolio.realized_pnl + portfolio.unrealized_pnl,
            "roi": portfolio.roi,
            "roi_percent": portfolio.roi * 100,
            "num_trades": portfolio.num_trades,
            "win_rate": portfolio.win_rate,
            "num_winning_trades": portfolio.num_winning_trades,
            "num_losing_trades": portfolio.num_losing_trades
        },
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/")
async def list_agents() -> Dict[str, Any]:
    """
    List all active agents with portfolio summaries.
    
    Optimized: Fetches prices once, calculates for all agents.
    """
    agents_list = []
    
    # Fetch prices once for all agents
    all_prices = _market_tool.get_all_prices() if ACTIVE_AGENTS else {}
    
    for agent_id, agent in ACTIVE_AGENTS.items():
        holdings = agent.get_holdings()
        cash = agent.get_available_cash()
        
        # Calculate total value using shared price data
        holdings_value = sum(
            qty * all_prices.get(token, 0.0) 
            for token, qty in holdings.items()
        )
        total_value = cash + holdings_value
        
        portfolio = agent.get_portfolio_status()
        
        agents_list.append({
            "agent_id": agent_id,
            "name": f"Agent-{agent_id}",  # Add if you have names
            "type": agent.__class__.__name__,
            "personality": agent.personality,
            "risk_score": agent.risk_score,
            "total_value": total_value,
            "cash": cash,
            "holdings_value": holdings_value,
            "roi": portfolio.roi,
            "roi_percent": portfolio.roi * 100,
            "num_trades": portfolio.num_trades,
            "win_rate": portfolio.win_rate
        })
    
    return {
        "agents": agents_list,
        "total_agents": len(agents_list),
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/market/prices")
async def get_market_prices() -> Dict[str, Any]:
    """
    Get current market prices for all supported tokens.
    
    Returns prices, sentiment, and market overview.
    """
    prices = _market_tool.get_all_prices()
    sentiment = _market_tool.get_market_sentiment()
    
    # Enrich with additional data
    market_data = {}
    for token, price in prices.items():
        try:
            volume = _market_tool.get_volume(token)
            market_data[token] = {
                "symbol": token,
                "price": price,
                "volume_24h": volume
            }
        except:
            market_data[token] = {
                "symbol": token,
                "price": price
            }
    
    return {
        "prices": market_data,
        "market_sentiment": sentiment,
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/{agent_id}/trades")
async def get_agent_trades(
    agent_id: int,
    limit: int = 20
) -> Dict[str, Any]:
    """
    Get agent's recent trade history.
    
    Args:
        agent_id: Agent identifier
        limit: Max trades to return (default 20)
    """
    if agent_id not in ACTIVE_AGENTS:
        raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
    
    agent = ACTIVE_AGENTS[agent_id]
    recent_trades = agent.get_short_term_memory(limit)
    
    trades_list = [
        {
            "trade_id": trade.trade_id,
            "action": trade.action,
            "token": trade.token,
            "quantity": trade.qty,
            "price": trade.price,
            "total_value": trade.qty * trade.price,
            "confidence": trade.confidence,
            "realized_pnl": trade.realized_pnl,
            "roi": trade.roi,
            "summary": trade.summary,
            "timestamp": trade.timestamp.isoformat()
        }
        for trade in recent_trades
    ]
    
    return {
        "agent_id": agent_id,
        "trades": trades_list,
        "count": len(trades_list),
        "timestamp": datetime.utcnow().isoformat()
    }

3. FastAPI App Setup

File: main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from api.routes import agents
import os

app = FastAPI(
    title="Crypto Trading Agents API",
    version="1.0.0"
)

# CORS for frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000",
        "http://localhost:5173",
        os.getenv("FRONTEND_URL", "http://localhost:3000")
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routes
app.include_router(agents.router)

@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "trading-agents-api"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

🎨 Frontend Implementation

1. API Client

File: src/lib/api.ts

const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';

export interface AgentPortfolio {
  agent_id: number;
  cash: number;
  holdings: {
    [token: string]: {
      quantity: number;
      current_price: number;
      total_value: number;
    };
  };
  holdings_value: number;
  total_value: number;
  performance: {
    starting_value: number;
    realized_pnl: number;
    unrealized_pnl: number;
    total_pnl: number;
    roi: number;
    roi_percent: number;
    num_trades: number;
    win_rate: number;
    num_winning_trades: number;
    num_losing_trades: number;
  };
  timestamp: string;
}

export interface MarketPrices {
  prices: {
    [token: string]: {
      symbol: string;
      price: number;
      volume_24h?: number;
    };
  };
  market_sentiment: string;
  timestamp: string;
}

export interface AgentSummary {
  agent_id: number;
  name: string;
  type: string;
  personality: string;
  risk_score: number;
  total_value: number;
  cash: number;
  holdings_value: number;
  roi: number;
  roi_percent: number;
  num_trades: number;
  win_rate: number;
}

export const api = {
  async getAgentPortfolio(agentId: number): Promise<AgentPortfolio> {
    const res = await fetch(`${API_BASE_URL}/api/agents/${agentId}/portfolio`);
    if (!res.ok) throw new Error(`Failed to fetch portfolio: ${res.statusText}`);
    return res.json();
  },

  async listAgents(): Promise<{ agents: AgentSummary[]; total_agents: number; timestamp: string }> {
    const res = await fetch(`${API_BASE_URL}/api/agents/`);
    if (!res.ok) throw new Error(`Failed to fetch agents: ${res.statusText}`);
    return res.json();
  },

  async getMarketPrices(): Promise<MarketPrices> {
    const res = await fetch(`${API_BASE_URL}/api/agents/market/prices`);
    if (!res.ok) throw new Error(`Failed to fetch market prices: ${res.statusText}`);
    return res.json();
  },

  async getAgentTrades(agentId: number, limit: number = 20): Promise<any> {
    const res = await fetch(`${API_BASE_URL}/api/agents/${agentId}/trades?limit=${limit}`);
    if (!res.ok) throw new Error(`Failed to fetch trades: ${res.statusText}`);
    return res.json();
  }
};

2. Polling Hook

File: src/hooks/usePortfolioPolling.ts

import { useState, useEffect, useRef, useCallback } from 'react';
import { api, AgentPortfolio } from '@/lib/api';

export interface UsePortfolioPollingOptions {
  intervalMs?: number;
  enabled?: boolean;
  onError?: (error: Error) => void;
}

export function usePortfolioPolling(
  agentId: number,
  options: UsePortfolioPollingOptions = {}
) {
  const {
    intervalMs = 5000,  // Poll every 5 seconds
    enabled = true,
    onError
  } = options;

  const [portfolio, setPortfolio] = useState<AgentPortfolio | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const intervalRef = useRef<NodeJS.Timeout>();
  const isMounted = useRef(true);

  const fetchPortfolio = useCallback(async () => {
    try {
      const data = await api.getAgentPortfolio(agentId);
      
      if (isMounted.current) {
        setPortfolio(data);
        setError(null);
        setLoading(false);
      }
    } catch (err) {
      const errorMsg = err instanceof Error ? err.message : 'Unknown error';
      
      if (isMounted.current) {
        setError(errorMsg);
        setLoading(false);
      }
      
      if (onError) {
        onError(err instanceof Error ? err : new Error(errorMsg));
      }
    }
  }, [agentId, onError]);

  useEffect(() => {
    isMounted.current = true;

    if (!enabled) {
      setLoading(false);
      return;
    }

    // Fetch immediately on mount
    fetchPortfolio();

    // Set up polling interval
    intervalRef.current = setInterval(fetchPortfolio, intervalMs);

    // Cleanup
    return () => {
      isMounted.current = false;
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [agentId, intervalMs, enabled, fetchPortfolio]);

  const refetch = useCallback(() => {
    fetchPortfolio();
  }, [fetchPortfolio]);

  return {
    portfolio,
    loading,
    error,
    refetch,
    lastUpdated: portfolio?.timestamp
  };
}

3. Market Prices Polling Hook

File: src/hooks/useMarketPrices.ts

import { useState, useEffect, useRef, useCallback } from 'react';
import { api, MarketPrices } from '@/lib/api';

export function useMarketPrices(intervalMs: number = 10000) {
  const [prices, setPrices] = useState<MarketPrices | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const intervalRef = useRef<NodeJS.Timeout>();

  const fetchPrices = useCallback(async () => {
    try {
      const data = await api.getMarketPrices();
      setPrices(data);
      setError(null);
      setLoading(false);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to fetch prices');
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchPrices();
    intervalRef.current = setInterval(fetchPrices, intervalMs);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [fetchPrices, intervalMs]);

  return { prices, loading, error, refetch: fetchPrices };
}

4. Portfolio Display Component

File: src/components/AgentPortfolio.tsx

import React from 'react';
import { usePortfolioPolling } from '@/hooks/usePortfolioPolling';
import { formatCurrency, formatPercent } from '@/lib/utils';

interface AgentPortfolioProps {
  agentId: number;
}

export function AgentPortfolio({ agentId }: AgentPortfolioProps) {
  const { portfolio, loading, error, lastUpdated } = usePortfolioPolling(agentId);

  if (loading) {
    return <div className="animate-pulse">Loading portfolio...</div>;
  }

  if (error) {
    return <div className="text-red-500">Error: {error}</div>;
  }

  if (!portfolio) {
    return <div>No portfolio data available</div>;
  }

  const { cash, holdings, total_value, performance } = portfolio;
  const isProfit = performance.total_pnl >= 0;

  return (
    <div className="portfolio-card">
      {/* Header */}
      <div className="portfolio-header">
        <div>
          <h2 className="text-2xl font-bold">
            {formatCurrency(total_value)}
          </h2>
          <p className={`text-lg ${isProfit ? 'text-green-500' : 'text-red-500'}`}>
            {isProfit ? '+' : ''}{formatCurrency(performance.total_pnl)} 
            ({formatPercent(performance.roi_percent)})
          </p>
        </div>
        <div className="text-sm text-gray-500">
          Updated: {new Date(lastUpdated!).toLocaleTimeString()}
        </div>
      </div>

      {/* Cash */}
      <div className="portfolio-section">
        <h3 className="font-semibold">Cash</h3>
        <p className="text-xl">{formatCurrency(cash)}</p>
      </div>

      {/* Holdings */}
      <div className="portfolio-section">
        <h3 className="font-semibold">Holdings</h3>
        {Object.entries(holdings).length === 0 ? (
          <p className="text-gray-500">No positions</p>
        ) : (
          <div className="holdings-list">
            {Object.entries(holdings).map(([token, data]) => (
              <div key={token} className="holding-item">
                <div className="flex justify-between">
                  <span className="font-medium">{token}</span>
                  <span>{formatCurrency(data.total_value)}</span>
                </div>
                <div className="text-sm text-gray-500">
                  {data.quantity.toFixed(4)} @ {formatCurrency(data.current_price)}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Performance Stats */}
      <div className="portfolio-section">
        <h3 className="font-semibold">Performance</h3>
        <div className="stats-grid">
          <div>
            <span className="text-gray-500">Trades:</span>
            <span className="font-medium">{performance.num_trades}</span>
          </div>
          <div>
            <span className="text-gray-500">Win Rate:</span>
            <span className="font-medium">{formatPercent(performance.win_rate * 100)}</span>
          </div>
          <div>
            <span className="text-gray-500">Realized P&L:</span>
            <span className={performance.realized_pnl >= 0 ? 'text-green-500' : 'text-red-500'}>
              {formatCurrency(performance.realized_pnl)}
            </span>
          </div>
          <div>
            <span className="text-gray-500">Unrealized P&L:</span>
            <span className={performance.unrealized_pnl >= 0 ? 'text-green-500' : 'text-red-500'}>
              {formatCurrency(performance.unrealized_pnl)}
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}

5. Market Overview Component

File: src/components/MarketOverview.tsx

import React from 'react';
import { useMarketPrices } from '@/hooks/useMarketPrices';
import { formatCurrency } from '@/lib/utils';

export function MarketOverview() {
  const { prices, loading, error } = useMarketPrices(10000);

  if (loading) return <div>Loading market data...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!prices) return null;

  const sentimentColor = {
    bullish: 'text-green-500',
    bearish: 'text-red-500',
    neutral: 'text-gray-500',
    unknown: 'text-gray-400'
  }[prices.market_sentiment];

  return (
    <div className="market-overview">
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-lg font-semibold">Market Overview</h3>
        <span className={`${sentimentColor} font-medium capitalize`}>
          {prices.market_sentiment}
        </span>
      </div>

      <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
        {Object.entries(prices.prices).map(([token, data]) => (
          <div key={token} className="price-card">
            <div className="font-semibold">{token}</div>
            <div className="text-xl">{formatCurrency(data.price)}</div>
            {data.volume_24h && (
              <div className="text-sm text-gray-500">
                Vol: {formatCurrency(data.volume_24h, { compact: true })}
              </div>
            )}
          </div>
        ))}
      </div>

      <div className="text-xs text-gray-500 mt-2">
        Last updated: {new Date(prices.timestamp).toLocaleTimeString()}
      </div>
    </div>
  );
}

📋 Implementation Checklist

Backend

  • Add price caching to MarketDataTool
    • Implement _is_cache_valid() method
    • Implement get_all_prices() with caching
    • Add cache TTL configuration (default 5s)
  • Create FastAPI routes
    • GET /api/agents/{id}/portfolio
    • GET /api/agents/
    • GET /api/agents/market/prices
    • GET /api/agents/{id}/trades
  • Set up CORS middleware
  • Add health check endpoint
  • Test endpoints with Postman/curl

Frontend

  • Create TypeScript types (api.ts)
  • Implement API client functions
  • Create usePortfolioPolling hook
  • Create useMarketPrices hook
  • Build AgentPortfolio component
  • Build MarketOverview component
  • Add utility functions (formatCurrency, formatPercent)
  • Style components with Tailwind/CSS
  • Test polling behavior (network tab, console logs)

Integration Testing

  • Verify portfolio updates every 5 seconds
  • Verify market prices update every 10 seconds
  • Test with multiple agents simultaneously
  • Test error handling (network failures, invalid agent IDs)
  • Test loading states
  • Verify no rate limit issues after 1 hour of polling
  • Test on slow connections

🧪 Testing Strategy

Manual Testing

  1. Start backend API server
  2. Create 2-3 test agents
  3. Open frontend with network throttling
  4. Verify portfolio updates every 5s
  5. Execute a trade, verify immediate update on next poll
  6. Monitor network tab for rate limit errors
  7. Let run for 10+ minutes to verify stability

Load Testing

  • Simulate 10+ concurrent users polling
  • Verify backend cache is working (CoinGecko calls ≤ 1 per 5s)
  • Monitor API response times (should be < 200ms)

📊 Success Criteria

  • Portfolio values update every 3-5 seconds
  • Market prices update every 10 seconds
  • No CoinGecko rate limit errors after 1 hour
  • Frontend shows "last updated" timestamps
  • Loading and error states display properly
  • Works with 10+ agents without performance issues
  • API response time < 200ms for portfolio endpoint

🔗 Dependencies

  • Backend: fastapi, uvicorn (already installed)
  • Frontend: No new dependencies needed

⚠️ Known Limitations

  1. Polling interval trade-off:

    • Too fast = more bandwidth/server load
    • Too slow = stale data
    • Recommended: 3-5s for portfolio, 10s for market prices
  2. Price accuracy:

    • Cached prices may be up to 5 seconds stale
    • Acceptable for MVP, can reduce TTL if needed
  3. Scalability:

    • Each poll = 1 API request per agent
    • 10 agents × 12 polls/min = 120 req/min
    • Fine for MVP, consider WebSockets for 100+ agents
  4. No real-time push:

    • User won't see trade execution instantly
    • Must wait up to 5s for next poll
    • Future: Upgrade to WebSockets for instant updates

🚀 Future Enhancements (Separate Issues)

  • WebSocket support for instant updates
  • Server-Sent Events (SSE) as alternative to polling
  • Configurable polling intervals per user
  • Historical price charts using portfolio timeline
  • Real-time trade feed with animations
  • Mobile app support with background polling
  • Redis caching for distributed deployments

📚 Related Files

Backend:

  • tools/market_data.py - Add caching logic
  • api/routes/agents.py - Create new file
  • main.py - FastAPI app setup

Frontend:

  • src/lib/api.ts - Create new file
  • src/hooks/usePortfolioPolling.ts - Create new file
  • src/hooks/useMarketPrices.ts - Create new file
  • src/components/AgentPortfolio.tsx - Create new file
  • src/components/MarketOverview.tsx - Create new file

⏱️ Estimated Time

  • Backend: 2-3 hours (caching + endpoints + testing)
  • Frontend: 3-4 hours (hooks + components + styling)
  • Testing: 1-2 hours (integration + load testing)
  • Total: 6-9 hours

📝 Notes

  • Start with backend endpoints first (can test with curl/Postman)
  • Price caching is critical - verify it works before frontend
  • Frontend can use mock data initially while backend is being built
  • Consider adding /api/agents/health endpoint that includes cache status
  • Log cache hits/misses for monitoring effectiveness

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions