# Project export: Cryptoagent - CFO

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Analyzes crypto portfolios and generates smart liquidation strategies based on risk tolerance.Uses real-time Gemini Exchange data and Fetch.ai agents to help decisions about converting crypto to cash
- Devpost: https://devpost.com/software/cryptoagent-cfo
- GitHub: https://github.com/yuktaablitz/cryptoagent-cfo.git
- Video: https://www.youtube.com/embed/6Pv9YAOo3Ic?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Postman)
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 3 recognized source files, 21 KB.
- HTML (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (5 of 5)

```
.gitignore
fetch_agent/agent.py
fetch_agent/mock_portfolio.json
fetch_agent/register_agentverse.py
index.html
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Initial commit: CryptoAgent CFO - CalHacks 2025 submission

## Key source files (fetched from GitHub, selected and truncated for size)

### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CryptoAgent CFO - Smart Liquidation Advisor</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 900px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 40px;
            text-align: center;
        }
        
        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
        }
        
        .header p {
            font-size: 1.1em;
            opacity: 0.9;
        }
        
        .content {
            padding: 40px;
        }
        
        .input-section {
            margin-bottom: 30px;
        }
        
        label {
            display: block;
            font-weight: 600;
            margin-bottom: 10px;
            color: #333;
            font-size: 1.1em;
        }
        
        textarea {
            width: 100%;
            min-height: 120px;
            padding: 15px;
            border: 2px solid #e0e0e0;
            border-radius: 10px;
            font-size: 16px;
            font-family: inherit;
            resize: vertical;
            transition: border-color 0.3s;
        }
        
        textarea:focus {
            outline: none;
            border-color: #667eea;
        }
        
        .examples {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
        }
        
        .examples h4 {
            color: #666;
            font-size: 0.9em;
            margin-bottom: 8px;
        }
        
        .examples p {
            color: #888;
            font-size: 0.85em;
            margin: 5px 0;
            font-style: italic;
        }
        
        button {
            width: 100%;
            padding: 18px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 10px;
            font-size: 1.2em;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
        }
        
        button:active {
            transform: translateY(0);
        }
        
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
            transform: none;
        }
        
        .loading {
            text-align: center;
            padding: 40px;
            display: none;
        }
        
        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #667eea;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 1s linear infinite;
            margin: 0 auto 20px;
        }
        
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .result {
            margin-top: 30px;
            display: none;
        }
        
        .result-card {
            background: #f8f9fa;
            border-radius: 12px;
            padding: 25px;
            margin-bottom: 20px;
            border-left: 4px solid #667eea;
        }
        
        .result-card h3 {
            color: #667eea;
            margin-bottom: 15px;
            font-size: 1.3em;
        }
        
        .liquidation-item {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin: 10px 0;
            border: 1px solid #e0e0e0;
        }
        
        .liquidation-item strong {
            color: #d32f2f;
            font-size: 1.1em;
        }
        
        .holding-item {
            background: white;
            padding: 12px;
            border-radius: 8px;
            margin: 8px 0;
            border: 1px solid #e0e0e0;
            display: flex;
            justify-content: space-between;
        }
        
        .rationale {
            background: #e8f4fd;
            border-left: 4px solid #2196F3;
            padding: 20px;
            border-radius: 8px;
            margin: 15px 0;
        }
        
        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 15px;
            border-radius: 8px;
            margin: 10px 0;
            color: #856404;
        }
        
        .tech-badge {
            display: inline-block;
            background: #667eea;
            color: white;
            padding: 5px 12px;
            border-radius: 15px;
            font-size: 0.85em;
            margin: 5px;
        }
        
        .footer {
            text-align: center;
            padding: 30px;
            background: #f8f9fa;
            color: #666;
        }
        
        .footer a {
            color: #667eea;
            text-decoration: none;
            font-weight: 600;
        }
        
        .error {
            background: #ffebee;
            border-left: 4px solid #f44336;
            padding: 20px;
            border-radius: 8px;
            color: #c62828;
            display: none;
        }
    </style>
</head>
<body>
    <div clas
[truncated — 7829 more characters]
```

### fetch_agent/register_agentverse.py

```python
import os
from uagents_core.utils.registration import (
    register_chat_agent,
    RegistrationRequestCredentials,
)

# Set your credentials
AGENTVERSE_KEY = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE3NjE0ODIwNDcsImlhdCI6MTc2MTQ3ODQ0NywiaXNzIjoiZmV0Y2guYWkiLCJqdGkiOiI5OTU1MjZmMThkMDBhN2I2NDVhNjk3N2EiLCJzY29wZSI6IiIsInN1YiI6Ijg4Y2M1YTY4NjNhZTZkYWYxYmI5OTg2MjI5Zjg4NzdmMTEwZWUyZDk1MzQ4YjBlNSJ9.aSUSmQ0vzchSSm8rLRUXtOSj4VgbqJ_3tzCSTiLQbGBiSP0yTGrcIe7gvdRN0c9jzcIkb9QCqL9_dNeusW8oraL-RqH7LtKUePe2BNNA2oEx4I2v97SpvHQb74Mi_FWexvdHZjcoPGxB7cG-2GwfPhYmrXCbPQ8Cyn06BAmNmgrK2LUNgAnRDzeAzl6T73bqpQ4BcLQdRLeqzwjvl1wLTYLJQJCUguAoEWyxw0m6HVQ0mzLjLNNb-uycK1ctdu0l3prrLuJLfgp1yhRz2uM6fpwiWGkjghTj-Rs4AmyZTfP17TikRyXBmhGxAo7a2W4Uv742AQh84FFHdHOu2_1hSA"
AGENT_SEED_PHRASE = "cryptoagent-cfo-calhacks-2025"

# Set environment variables
os.environ["AGENTVERSE_KEY"] = AGENTVERSE_KEY
os.environ["AGENT_SEED_PHRASE"] = AGENT_SEED_PHRASE

# Register the agent
register_chat_agent(
    "CryptoAgent CFO",
    "https://jussive-lonnie-gallantly.ngrok-free.dev/chat",
    active=True,
    credentials=RegistrationRequestCredentials(
        agentverse_api_key=os.environ["AGENTVERSE_KEY"],
        agent_seed_phrase=os.environ["AGENT_SEED_PHRASE"],
    ),
)

print("✅ Agent registered on Agentverse!")
print("📍 Agent Address: agent1qfhnmhjwxjjs43970rqjhsw00ds054v8gt0d0735m5x6h3pttpcewy2wugj")
print("🌐 Endpoint: https://jussive-lonnie-gallantly.ngrok-free.dev/chat")

```

### fetch_agent/agent.py

```python
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from uagents import Agent, Context
from typing import Optional, List
import os, json, re

AGENT_SEED = os.environ.get("AGENT_SEED", "cryptoagent-cfo-calhacks-2025")
AGENT_PORT = int(os.environ.get("AGENT_PORT", "8080"))

agent = Agent(name="crypto_cfo_agent", seed=AGENT_SEED, port=8000, endpoint=[f"http://localhost:8000/submit"])

app = FastAPI(title="CryptoAgent CFO", description="AI-powered crypto portfolio liquidation advisor", version="1.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])

class AssetAction(BaseModel):
    symbol: str
    sell_usd: float
    sell_fraction: float
    reason: str

class PlanRequest(BaseModel):
    runway_usd: float
    required_expense_usd: float
    risk_tolerance: str = "medium"
    portfolio_override: Optional[dict] = None

class PlanResponse(BaseModel):
    liquidation_plan: List[AssetAction]
    keep_holdings: List[dict]
    total_freed_usd: float
    remaining_portfolio_usd: float
    rationale: str
    warnings: List[str]
    agent_address: str

class ChatMessage(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: List[ChatMessage]

def load_mock_portfolio():
    portfolio_path = os.path.join(os.path.dirname(__file__), "mock_portfolio.json")
    with open(portfolio_path) as f:
        return json.load(f)

def parse_prompt(text: str):
    nums = [float(x) for x in re.findall(r'\$?(\d+(?:\.\d+)?)', text)]
    runway = nums[0] if nums else 1000
    expense = nums[1] if len(nums) > 1 else 500
    t = text.lower()
    if any(w in t for w in ["conservative", "risk-averse", "safe", "careful", "low"]): risk = "low"
    elif any(w in t for w in ["aggressive", "risky", "growth", "yolo", "high"]): risk = "high"
    else: risk = "medium"
    return runway, expense, risk

def calculate_liquidation_plan(runway, expense, portfolio, risk):
    holdings = portfolio["holdings"].copy()
    volatility_map = {"BTC": 1, "ETH": 2, "SOL": 3}
    if risk == "low": holdings.sort(key=lambda h: volatility_map.get(h["symbol"], 99), reverse=True)
    elif risk == "high": holdings.sort(key=lambda h: volatility_map.get(h["symbol"], 99))
    else: holdings.sort(key=lambda h: h["usd_value"])
    
    to_sell, remaining, need, total_freed = [], [], float(expense), 0.0
    for h in holdings:
        if need <= 0:
            remaining.append(h)
            continue
        take_usd = min(h["usd_value"], need)
        sell_fraction = round(take_usd / h["usd_value"], 4) if h["usd_value"] > 0 else 0
        reason = "Higher volatility - preserving stable assets" if risk == "low" else "Lower volatility - preserving growth potential" if risk == "high" else "Balanced liquidation strategy"
        to_sell.append(AssetAction(symbol=h["symbol"], sell_usd=round(take_usd, 2), sell_fraction=sell_fraction, reason=reason))
        total_freed += take_usd
        need -= take_usd
        remain_val = h["usd_value"] - take_usd
        if remain_val > 1:
            remaining.append({"symbol": h["symbol"], "amount": round(h["amount"] * (1 - sell_fraction), 8), "usd_value": round(remain_val, 2)})
    
    rationale = f"Parsed runway=${runway:.2f}, expense=${expense:.2f}, risk='{risk}'. Generated liquidation plan to cover ${expense:.2f} with '{risk}' risk tolerance. "
    if risk == "low": rationale += "Sold volatile assets (SOL, ETH) first to preserve stable BTC."
    elif risk == "high": rationale += "Sold stable BTC first to preserve growth assets (SOL, ETH)."
    else: rationale += "Used balanced proportional approach."
    
    warnings = []
    if need > 0: warnings.append(f"Insufficient funds: short by ${need:.2f}")
    if total_freed > portfolio["total_usd"] * 0.8: warnings.append("Liquidating >80% of portfolio")
    
    remaining_value = sum(h.get("usd_value", 0) for h in remaining)
    return to_sell, remaining, total_freed, remaining_value, rationale, warnings

@app.get("/")
async def root():
    return {"service": "CryptoAgent CFO", "status": "active", "agent_address": agent.address}

@app.get("/health")
async def health():
    return {"status": "healthy", "agent_address": agent.address}

@app.post("/")
async def root_post(request: Request):
    try:
        body = await request.json()
    except:
        body = {}
    
    if "user_prompt" in body or "prompt" in body:
        text = body.get("user_prompt") or body.get("prompt") or ""
        runway, expense, risk = parse_prompt(text)
    else:
        runway = body.get("runway_usd", 1000)
        expense = body.get("required_expense_usd", 500)
        risk = body.get("risk_tolerance", "medium")
    
    portfolio = body.get("portfolio_override") or load_mock_portfolio()
    to_sell, remaining, total_freed, remaining_value, rationale, warnings = calculate_liquidation_plan(runway, expense, portfolio, risk)
    
    return {
        "liquidation_plan": [a.dict() for a in to_sell],
        "keep_holdings": remaining,
        "total_freed_usd": round(total_freed, 2),
        "remaining_portfolio_usd": round(remaining_value, 2),
        "rationale": rationale,
        "warnings": warnings,
        "agent_address": agent.address
    }

@app.post("/plan")
async def plan(request: Request):
    return await root_post(request)

@app.post("/chat")
async def chat(req: ChatRequest):
    user_text = next((m.content for m in req.messages if m.role == "user"), "")
    runway, expense, risk = parse_prompt(user_text)
    portfolio = load_mock_portfolio()
    to_sell, remaining, total_freed, remaining_value, rationale, warnings = calculate_liquidation_plan(runway, expense, portfolio, risk)
    
    actions = "\n".join([f"Sell ${a.sell_usd} of {a.symbol} ({a.sell_fraction*100:.1f}%) - {a.reason}" for a in to_sell])
    warns = "\n".join(warnings) if warnings else "No warnings."
    msg = f"""Liquidation Plan to Cov
[truncated — 684 more characters]
```