diff --git a/examples/crypto-butler/.env.example b/examples/crypto-butler/.env.example new file mode 100644 index 0000000..3d38419 --- /dev/null +++ b/examples/crypto-butler/.env.example @@ -0,0 +1,18 @@ +# .env.local +# Copy this file to .env.local and fill in your values + +# ── ANTHROPIC ────────────────────────────────────── +ANTHROPIC_API_KEY=sk-ant-your-key-here + +# ── WALLETCONNECT (get free projectId at cloud.walletconnect.com) ── +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here + +# ── OPTIONAL: ALCHEMY for Base Sepolia RPC ──────── +NEXT_PUBLIC_ALCHEMY_ID=your_alchemy_id_here + +# ── UTXOs.dev Project ID (server-side only) ──────── +UTXOS_PROJECT_ID=your_project_id + +# ── Blockfrost API Keys (server-side only) ──────── +BLOCKFROST_API_KEY_PREPROD=preprodxxxxxxxx +BLOCKFROST_API_KEY_MAINNET=mainnetxxxxxxxx diff --git a/examples/crypto-butler/CONTRIBUTING.md b/examples/crypto-butler/CONTRIBUTING.md new file mode 100644 index 0000000..17740a4 --- /dev/null +++ b/examples/crypto-butler/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to Crypto Butler + +Contributions are welcome. Please open an issue before submitting a pull request. + +## Development Setup +```bash +git clone https://github.com/your-username/crypto-butler +cd crypto-butler +npm install +cp .env.example .env.local +npm run dev +``` + +## Areas Where Contributions Are Needed + +- Additional Cardano protocol integrations for yield data +- Improved risk scoring logic in the Risk Agent +- M-Pesa API integration +- Mobile-responsive UI improvements +- Smart contract development for on-chain execution + +## Pull Request Guidelines + +- One feature or fix per pull request +- Include a clear description of what changed and why +- Test on Cardano Preprod testnet before submitting diff --git a/examples/crypto-butler/README.md b/examples/crypto-butler/README.md new file mode 100644 index 0000000..f9c7f46 --- /dev/null +++ b/examples/crypto-butler/README.md @@ -0,0 +1,527 @@ +# Crypto Butler + +An AI-powered DeFi wealth management agent built on Cardano. Users describe their financial goals in plain English and a multi-agent AI swarm analyzes live yield opportunities, builds a personalized strategy, and simulates execution on Cardano Preprod testnet. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Tech Stack](#tech-stack) +- [Multi-Agent System](#multi-agent-system) +- [UTXOS.dev Integration](#utxosdev-integration) +- [Wallet Connection](#wallet-connection) +- [Yield Data](#yield-data) +- [Testnet Simulation](#testnet-simulation) +- [Getting Started](#getting-started) +- [Environment Variables](#environment-variables) +- [Project Structure](#project-structure) +- [Open Source Contributions](#open-source-contributions) +- [Roadmap](#roadmap) + +--- + +## Overview + +Crypto Butler is a conversational DeFi agent. Instead of navigating complex protocol interfaces, users simply describe what they want: + +> "Grow my 5000 USDC at moderate risk over 12 months" + +The system deploys three specialized AI agents that research live yields, profile risk tolerance, and return a plain-English allocation strategy with projected returns — all simulated on Cardano Preprod testnet. + +The product targets two audiences: +- Crypto-native users who want intelligent yield optimization without manual research +- Emerging market users (starting with Kenya) who need a simple, mobile-first entry point into DeFi with M-Pesa integration planned for a future release + +--- + +## Architecture +``` +User Input (natural language goal) + | + v + Orchestrator (Next.js API Route) + | + +-----+-----+ + | | | + v v v +Research Risk CFO +Agent Agent Agent + | | | + +-----+-----+ + | + v + Recommendation + (allocation + projections) + | + v + Testnet Simulation + (Cardano Preprod) + | + v + UI Dashboard + (charts + tx log) +``` + +--- + +## Tech Stack + +| Layer | Technology | +|---|---| +| Frontend | Next.js 14, Tailwind CSS | +| AI Agents | Anthropic Claude claude-sonnet-4-20250514 | +| Wallet (Social) | UTXOS.dev Wallet-as-a-Service | +| Wallet (Native) | CIP-30 direct browser API | +| Yield Data | DeFiLlama API | +| Blockchain | Cardano Preprod Testnet | +| Blockchain Data | Blockfrost | +| Charts | Recharts | +| Deployment | Vercel | + +--- + +## Multi-Agent System + +The core of Crypto Butler is a three-agent swarm. Each agent has a single responsibility and passes structured JSON to the next. + +### Agent 1: Researcher + +Fetches live yield data from DeFiLlama and identifies the top opportunities across risk categories. +```javascript +// app/api/butler/route.js + +async function researcherAgent(yields) { + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1000, + system: `You are a DeFi yield researcher. + Analyze yield opportunities and return ONLY valid JSON.`, + messages: [{ + role: 'user', + content: `Analyze these yield pools and identify the top 6 + opportunities across risk categories. + + Pools: ${JSON.stringify(yields, null, 2)} + + Return ONLY this JSON structure: + { + "conservative": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}], + "moderate": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}], + "aggressive": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}] + }` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} +``` + +### Agent 2: Risk Analyst + +Parses the user's natural language goal to extract risk tolerance, time horizon, and target amount. Scores each opportunity for fit. +```javascript +async function riskAgent(userGoal, researcherOutput) { + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 800, + system: `You are a risk analyst for DeFi portfolios. + Return ONLY valid JSON.`, + messages: [{ + role: 'user', + content: `User goal: "${userGoal}" + Available opportunities: ${JSON.stringify(researcherOutput)} + + Extract: + 1. Risk tolerance (conservative/moderate/aggressive) + 2. Time horizon in months + 3. Target amount in USD + 4. Primary goal (grow/preserve/income) + + Score each opportunity 1-10 for fit. + + Return ONLY this JSON: + { + "riskProfile": { + "tolerance": "moderate", + "timeHorizonMonths": 12, + "targetAmountUsd": 5000, + "primaryGoal": "grow" + }, + "scoredOpportunities": [ + { + "protocol": "", + "pool": "", + "apy": 0, + "fitScore": 8, + "rationale": "" + } + ] + }` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} +``` + +### Agent 3: CFO + +Takes the scored opportunities and builds a complete portfolio allocation with projections, risks, and plain-English explanation. +```javascript +async function cfoAgent(userGoal, riskOutput) { + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1500, + system: `You are a personal CFO and DeFi wealth manager. + Explain complex finance simply. Return ONLY valid JSON.`, + messages: [{ + role: 'user', + content: `User goal: "${userGoal}" + Risk profile: ${JSON.stringify(riskOutput.riskProfile)} + Top opportunities: ${JSON.stringify(riskOutput.scoredOpportunities)} + + Return ONLY this JSON: + { + "greeting": "", + "summary": "", + "allocation": [ + { + "protocol": "", + "pool": "", + "chain": "", + "apy": 0, + "percentage": 40, + "amountUsd": 2000, + "role": "Core yield engine", + "reasoning": "" + } + ], + "projections": { + "months3": 0, + "months6": 0, + "months12": 0, + "blendedApy": 0 + }, + "risks": [], + "butlerNote": "" + }` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} +``` + +--- + +## UTXOS.dev Integration + +Crypto Butler uses UTXOS.dev to enable social login wallet creation. This removes the requirement for users to have an existing Cardano wallet or browser extension — critical for the emerging market use case. + +### The Problem It Solves + +Standard Cardano wallet onboarding requires: +- Installing a browser extension +- Writing down a 24-word seed phrase +- Funding the wallet manually + +For a first-time user in Nairobi, this is a fatal barrier. UTXOS.dev replaces it with a Google or Apple login. + +### How It Is Integrated + +All UTXOS SDK calls run server-side in a Next.js API route. This avoids the WebAssembly SSR conflict that occurs when Cardano SDKs are imported in client components. +```javascript +// app/api/wallet/enable/route.js + +import { Web3Wallet } from '@utxos/sdk'; +import { BlockfrostProvider } from '@meshsdk/core'; + +export async function POST(req) { + try { + const provider = new BlockfrostProvider('/api/blockfrost/preprod/'); + + const wallet = await Web3Wallet.enable({ + projectId: process.env.UTXOS_PROJECT_ID, + networkId: 0, // 0 = preprod, 1 = mainnet + fetcher: provider, + submitter: provider, + }); + + const address = await wallet.cardano.getChangeAddress(); + const balance = await wallet.cardano.getBalance(); + + return Response.json({ address, balance }); + } catch (err) { + return Response.json({ error: err.message }, { status: 500 }); + } +} +``` + +The Blockfrost API key is kept server-side via a proxy route: +```javascript +// app/api/blockfrost/[network]/[...path]/route.js + +export async function GET(req, { params }) { + const { network, path } = params; + + const apiKey = network === 'preprod' + ? process.env.BLOCKFROST_API_KEY_PREPROD + : process.env.BLOCKFROST_API_KEY_MAINNET; + + const url = `https://cardano-${network}.blockfrost.io/api/v0/${path.join('/')}`; + + const res = await fetch(url, { + headers: { project_id: apiKey } + }); + + const data = await res.json(); + return Response.json(data); +} +``` + +### Why Server-Side +``` +Browser Server (API Route) + | | + | POST /api/wallet/enable | + |------------------------------> | + | | UTXOS SDK runs here + | | Blockfrost called here + | | WebAssembly runs here + | { address, balance } | + | <-----------------------------| + | | +No SDK in browser No SSR conflict +``` + +--- + +## Wallet Connection + +The app supports two wallet connection methods side by side. + +### Method 1: Social Login via UTXOS.dev + +For new users with no existing Cardano wallet. Login with Google or Apple creates a non-custodial wallet automatically. No seed phrase. No browser extension. +```javascript +// components/WalletConnect.jsx + +async function connectSocial(provider) { + try { + const res = await fetch('/api/wallet/enable', { method: 'POST' }); + const { address, balance } = await res.json(); + setAddress(address.slice(0, 8) + '...' + address.slice(-4)); + setConnected(true); + onConnected?.({ address, balance }, 'social'); + } catch (err) { + console.error('Social login failed:', err); + } +} +``` + +### Method 2: CIP-30 Native Wallet + +For existing Cardano users with Eternl, Nami, Vespr, or other CIP-30 compatible wallets. Uses the standard `window.cardano` browser API directly — no SDK required. +```javascript +// components/WalletConnect.jsx + +useEffect(() => { + if (typeof window === 'undefined') return; + const knownWallets = ['eternl', 'nami', 'vespr', 'flint', 'typhon']; + const detected = knownWallets + .filter(name => window.cardano?.[name]) + .map(name => ({ + name, + icon: window.cardano[name].icon, + api: window.cardano[name] + })); + setWallets(detected); +}, []); + +async function connectWallet(wallet) { + const api = await wallet.api.enable(); + const addresses = await api.getUsedAddresses(); + const addr = addresses[0]; + setAddress(addr.slice(0, 8) + '...' + addr.slice(-4)); + setConnected(true); + onConnected?.(api, 'cip30'); +} +``` + +--- + +## Yield Data + +Live yield data is fetched from the DeFiLlama Pools API at request time. No API key required. +```javascript +// app/api/butler/route.js + +async function fetchYields() { + const res = await fetch('https://yields.llama.fi/pools', { + next: { revalidate: 300 }, // cache for 5 minutes + }); + const data = await res.json(); + + return data.data + .filter(p => + p.stablecoin && + p.tvlUsd > 1_000_000 && + p.apy > 0 && + p.apy < 100 && + ['Cardano', 'Ethereum', 'Arbitrum'].includes(p.chain) + ) + .sort((a, b) => b.tvlUsd - a.tvlUsd) + .slice(0, 20) + .map(p => ({ + protocol: p.project, + pool: p.symbol, + chain: p.chain, + apy: parseFloat(p.apy.toFixed(2)), + tvlUsd: p.tvlUsd, + })); +} +``` + +Cardano-native protocols included in the filter: Liqwid, Minswap, Indigo, Splash. + +--- + +## Testnet Simulation + +After the CFO agent builds an allocation, the system generates a simulated set of Cardano Preprod transactions. Each transaction represents one deposit step in the strategy. +```javascript +function generateTestnetSimulation(allocation) { + return { + network: 'Cardano Preprod Testnet', + transactions: allocation.map((alloc, i) => ({ + step: i + 1, + action: `Deposit ${alloc.amountUsd.toFixed(0)} USDC + into ${alloc.protocol} ${alloc.pool}`, + protocol: alloc.protocol, + chain: 'Cardano Preprod', + amount: alloc.amountUsd, + txHash: generateMockTxHash(), + status: 'simulated', + explorerUrl: `https://preprod.cardanoscan.io/transaction/${generateMockTxHash()}`, + })), + disclaimer: 'Simulated on Cardano Preprod testnet. No real funds used.', + }; +} +``` + +--- + +## Getting Started + +### Prerequisites + +- Node.js 18 or higher +- An Anthropic API key +- A UTXOS.dev project ID +- A Blockfrost API key (Preprod) +- Eternl wallet browser extension (for CIP-30 testing) + +### Installation +```bash +git clone https://github.com/your-username/crypto-butler +cd crypto-butler +npm install +cp .env.example .env.local +``` + +Fill in your environment variables then run: +```bash +npm run dev +``` + +Open http://localhost:3000 and type a goal such as: +``` +Grow my 5000 USDC at moderate risk over 12 months +``` + +--- + +## Environment Variables +```bash +# Anthropic +ANTHROPIC_API_KEY=sk-ant-your-key-here + +# UTXOS.dev +UTXOS_PROJECT_ID=your_project_id + +# Blockfrost +BLOCKFROST_API_KEY_PREPROD=preprodxxxxxxxx +BLOCKFROST_API_KEY_MAINNET=mainnetxxxxxxxx + +# WalletConnect (optional for CIP-30) +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id +``` + +--- + +## Project Structure +``` +crypto-butler/ + app/ + api/ + butler/ + route.js - Multi-agent orchestration + wallet/ + enable/ + route.js - UTXOS.dev wallet initialization + blockfrost/ + [network]/ + [...path]/ + route.js - Blockfrost proxy + globals.css + layout.jsx + page.jsx - Main chat interface + providers.jsx + components/ + AgentPanel.jsx - Live agent activity display + Charts.jsx - Allocation pie + projection chart + SimulationPanel.jsx - Testnet transaction log + WalletConnect.jsx - Dual wallet connection UI + .env.example + next.config.js + README.md +``` + +--- + +## Open Source Contributions + +During development we encountered a critical incompatibility between Mesh SDK and Next.js 15. Mesh SDK's dependency on `libsodium-wrappers-sumo` (WebAssembly cryptography) causes build failures when imported in any client component, even with `ssr: false` and dynamic imports. + +The root cause is that Next.js 15 with Turbopack attempts to bundle WebAssembly modules server-side during the compilation step, before the `ssr: false` directive can take effect. + +Our solution was to move all Cardano SDK usage to server-side API routes where WebAssembly runs without conflict, and implement a direct CIP-30 wallet connection for the client that requires zero SDK dependencies. + +We have documented this issue and the fix in a pull request to the MeshJS repository at github.com/MeshJS/mesh. + +--- + +## Roadmap + +**Post-Hackathon Phase 1** +- M-Pesa onramp and offramp integration (Kenya) +- Real transaction execution on Cardano mainnet +- Performance fee smart contract (10-20% of yield) + +**Phase 2** +- Autonomous rebalancing agent +- Multi-chain expansion (Base, Solana) +- Mobile application + +**Phase 3** +- TEE-based key management +- DAO governance for strategy parameters +- Expansion to Nigeria, Ghana, Tanzania diff --git a/examples/crypto-butler/app/api/blockfrost/[network]/[...path]/route.js b/examples/crypto-butler/app/api/blockfrost/[network]/[...path]/route.js new file mode 100644 index 0000000..b52bba1 --- /dev/null +++ b/examples/crypto-butler/app/api/blockfrost/[network]/[...path]/route.js @@ -0,0 +1,35 @@ +export async function GET(req, { params }) { + const { network, path } = params; + const apiKey = network === 'preprod' + ? process.env.BLOCKFROST_API_KEY_PREPROD + : process.env.BLOCKFROST_API_KEY_MAINNET; + + const url = `https://cardano-${network}.blockfrost.io/api/v0/${path.join('/')}`; + + const res = await fetch(url, { + headers: { project_id: apiKey } + }); + + const data = await res.json(); + return Response.json(data); +} + +export async function POST(req) { + try { + const { token } = await req.json(); + + const res = await fetch('https://api.utxos.dev/v1/wallet/cardano/address', { + headers: { + 'Authorization': `Bearer ${token}`, + 'x-project-id': process.env.UTXOS_PROJECT_ID, + } + }); + + const data = await res.json(); + return Response.json({ address: data.address, balance: data.balance }); + + } catch (err) { + console.error('Wallet error:', err.message); + return Response.json({ error: err.message }, { status: 500 }); + } +} diff --git a/examples/crypto-butler/app/api/butler/.route.js.swp b/examples/crypto-butler/app/api/butler/.route.js.swp new file mode 100644 index 0000000..498e0d9 Binary files /dev/null and b/examples/crypto-butler/app/api/butler/.route.js.swp differ diff --git a/examples/crypto-butler/app/api/butler/route.js b/examples/crypto-butler/app/api/butler/route.js new file mode 100644 index 0000000..d4e171c --- /dev/null +++ b/examples/crypto-butler/app/api/butler/route.js @@ -0,0 +1,248 @@ +// app/api/butler/route.js +// Multi-agent swarm: Researcher → Risk Agent → CFO Agent + +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +async function fetchYields() { + try { + const res = await fetch('https://yields.llama.fi/pools', { + next: { revalidate: 300 }, + }); + const data = await res.json(); + + return data.data + .filter(p => + p.stablecoin && + p.tvlUsd > 1_000_000 && + p.apy > 0 && + p.apy < 100 && + ['Cardano', 'Ethereum', 'Arbitrum'].includes(p.chain) + ) + .sort((a, b) => b.tvlUsd - a.tvlUsd) + .slice(0, 20) + .map(p => ({ + protocol: p.project, + pool: p.symbol, + chain: p.chain, + apy: parseFloat(p.apy.toFixed(2)), + tvlUsd: p.tvlUsd, + })); + } catch (err) { + console.error('Yield fetch failed:', err); + return FALLBACK_YIELDS; + } +} + +function categorize(apy) { + if (apy < 5) return 'conservative'; + if (apy < 12) return 'moderate'; + return 'aggressive'; +} + +async function researcherAgent(yields) { + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1000, + system: `You are a DeFi yield researcher. + Analyze yield opportunities and return ONLY valid JSON.`, + messages: [{ + role: 'user', + content: `Analyze these yield pools and identify the top 6 + opportunities across risk categories. + + Pools: ${JSON.stringify(yields, null, 2)} + + Return ONLY this JSON structure: + { + "conservative": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}], + "moderate": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}], + "aggressive": [{"protocol": "", "pool": "", "chain": "", + "apy": 0, "reasoning": ""}] + }` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} + +async function riskAgent(userGoal, researcherOutput) { + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 800, + system: `You are a risk analyst for DeFi portfolios. + Return ONLY valid JSON.`, + messages: [{ + role: 'user', + content: `User goal: "${userGoal}" + Available opportunities: ${JSON.stringify(researcherOutput)} + + Extract: + 1. Risk tolerance (conservative/moderate/aggressive) + 2. Time horizon in months + 3. Target amount in USD + 4. Primary goal (grow/preserve/income) + + Score each opportunity 1-10 for fit. + + Return ONLY this JSON: + { + "riskProfile": { + "tolerance": "moderate", + "timeHorizonMonths": 12, + "targetAmountUsd": 5000, + "primaryGoal": "grow" + }, + "scoredOpportunities": [ + { + "protocol": "", + "pool": "", + "apy": 0, + "fitScore": 8, + "rationale": "" + } + ] + }` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} + +async function cfoAgent(userGoal, riskOutput) { + const { riskProfile, scoredOpportunities } = riskOutput; + const amount = riskProfile.targetAmountUsd; + + const res = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1500, + system: `You are a personal CFO and DeFi wealth manager. You explain complex finance simply, like a trusted friend who happens to be an expert. Preferred stablecoins on Cardano are iUSD, DJED, and USDM instead of USDC. Return ONLY valid JSON, no markdown.`, + messages: [{ + role: 'user', + content: `User goal: "${userGoal}" +Risk profile: ${JSON.stringify(riskProfile)} +Top opportunities (scored): ${JSON.stringify(scoredOpportunities.slice(0, 6))} + +Build an optimal portfolio allocation for $${amount}. + +Return ONLY this JSON: +{ + "greeting": "A warm 1-sentence greeting addressing their specific goal", + "summary": "2-3 sentence plain-English explanation of the strategy", + "allocation": [ + { + "protocol": "", + "pool": "", + "chain": "", + "apy": 0, + "percentage": 40, + "amountUsd": 2000, + "role": "Core yield engine", + "reasoning": "Why this fits their goal" + } + ], + "projections": { + "months3": 0, + "months6": 0, + "months12": 0, + "blendedApy": 0 + }, + "risks": ["Risk 1", "Risk 2"], + "nextSteps": "One clear action sentence", + "butlerNote": "A charming, butler-style closing remark" +}` + }] + }); + + const text = res.content[0].text.replace(/```json|```/g, '').trim(); + return JSON.parse(text); +} + +// ── MAIN ROUTE ────────────────────────────────────────────────── +export async function POST(req) { + try { + const { message, conversationHistory = [] } = await req.json(); + + if (!message) { + return Response.json({ error: 'Message required' }, { status: 400 }); + } + + // Stream agent status updates via a simple object + const agentLog = []; + + // Step 1: Fetch live yields + agentLog.push({ agent: 'researcher', status: 'fetching live yields from DeFiLlama...' }); + const yields = await fetchYields(); + + // Step 2: Researcher agent analyzes yields + agentLog.push({ agent: 'researcher', status: 'analyzing 20+ yield opportunities...' }); + const researcherOutput = await researcherAgent(yields); + + // Step 3: Risk agent scores against user goal + agentLog.push({ agent: 'risk', status: 'profiling your risk tolerance...' }); + const riskOutput = await riskAgent(message, researcherOutput); + + // Step 4: CFO builds the recommendation + agentLog.push({ agent: 'cfo', status: 'building your personalized strategy...' }); + const cfoOutput = await cfoAgent(message, riskOutput); + + // Step 5: Generate testnet simulation + const simulation = generateTestnetSimulation(cfoOutput.allocation, riskOutput.riskProfile); + + return Response.json({ + success: true, + agentLog, + recommendation: cfoOutput, + riskProfile: riskOutput.riskProfile, + simulation, + yieldsAnalyzed: yields.length, + }); + + } catch (error) { + console.error('Butler error:', error); + return Response.json({ error: error.message }, { status: 500 }); + } +} + +function generateTestnetSimulation(allocation, riskProfile) { + const txs = allocation.map((alloc, i) => ({ + step: i + 1, + action: `Deposit ${alloc.amountUsd.toFixed(0)} USDC → ${alloc.protocol} ${alloc.pool}`, + protocol: alloc.protocol, + chain: 'Cardano Preprod', + amount: alloc.amountUsd, + txHash: `0x${randomHex(64)}`, + status: 'simulated', + gasEstimate: `~$${(Math.random() * 0.05 + 0.01).toFixed(3)}`, + explorerUrl: `https://preprod.cardanoscan.io/transaction/${randomHex(64)}`, + })); + + return { + network: 'Cardano Preprod Testnet', + totalTransactions: txs.length, + totalGasEstimate: `~$${(txs.length * 0.02).toFixed(3)}`, + transactions: txs, + disclaimer: 'Simulated on Cardano Preprod testnet. No real funds used.', + }; +} + +function randomHex(len) { + return Array.from({ length: len }, () => + Math.floor(Math.random() * 16).toString(16) + ).join(''); +} + +// ── FALLBACK YIELDS (if DeFiLlama is down) ───────────────────── +const FALLBACK_YIELDS = [ + { protocol: 'liqwid', pool: 'USDC', chain: 'Cardano', apy: 7.2, tvlUsd: 45_000_000, category: 'moderate' }, + { protocol: 'minswap', pool: 'ADA/USDC', chain: 'Cardano', apy: 9.1, tvlUsd: 22_000_000, category: 'moderate' }, + { protocol: 'indigo', pool: 'iUSD', chain: 'Cardano', apy: 5.8, tvlUsd: 18_000_000, category: 'conservative' }, + { protocol: 'splash', pool: 'USDC', chain: 'Cardano', apy: 8.4, tvlUsd: 15_000_000, category: 'moderate' }, + { protocol: 'wingriders', pool: 'USDC', chain: 'Cardano', apy: 6.5, tvlUsd: 12_000_000, category: 'moderate' }, + { protocol: 'sundae', pool: 'USDC', chain: 'Cardano', apy: 7.8, tvlUsd: 10_000_000, category: 'moderate' }, +]; diff --git a/examples/crypto-butler/app/globals.css b/examples/crypto-butler/app/globals.css new file mode 100644 index 0000000..f16495c --- /dev/null +++ b/examples/crypto-butler/app/globals.css @@ -0,0 +1,95 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --navy-900: #0a0e27; + --navy-800: #1a1f3a; + --navy-700: #2d3561; + --gold-500: #fbbf24; + --gold-400: #f59e0b; + --success: #10b981; + --danger: #ef4444; +} + +* { + font-family: 'Inter', sans-serif; +} + +body { + background: linear-gradient(135deg, var(--navy-900) 0%, var(--navy-800) 100%); + color: #e2e8f0; + min-height: 100vh; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--navy-800); +} + +::-webkit-scrollbar-thumb { + background: var(--navy-700); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--navy-600); +} + +/* Animations */ +@keyframes pulse-gold { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.animate-pulse-gold { + animation: pulse-gold 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* Glass morphism effect */ +.glass { + background: rgba(26, 31, 58, 0.8); + backdrop-filter: blur(10px); + border: 1px solid rgba(251, 191, 36, 0.1); +} + +/* Custom button styles */ +.btn-primary { + background: linear-gradient(135deg, var(--gold-500), var(--gold-400)); + color: var(--navy-900); + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(251, 191, 36, 0.3); +} + +/* Chat message styling */ +.message-user { + background: linear-gradient(135deg, var(--navy-700), var(--navy-600)); + border-left: 3px solid var(--gold-500); +} + +.message-butler { + background: rgba(26, 31, 58, 0.6); + border: 1px solid rgba(251, 191, 36, 0.2); +} + +/* Agent status indicators */ +.agent-active { + background: linear-gradient(135deg, var(--gold-500), var(--gold-400)); + color: var(--navy-900); +} + +.agent-idle { + background: var(--navy-700); + color: #94a3b8; +} diff --git a/examples/crypto-butler/app/layout.jsx b/examples/crypto-butler/app/layout.jsx new file mode 100644 index 0000000..c8d3a01 --- /dev/null +++ b/examples/crypto-butler/app/layout.jsx @@ -0,0 +1,37 @@ +// app/layout.jsx +import { Playfair_Display, DM_Sans, DM_Mono } from 'next/font/google'; +import './globals.css'; +import { Providers } from './providers'; + +const display = Playfair_Display({ + subsets: ['latin'], + variable: '--font-display', + weight: ['400', '600', '700'], +}); + +const body = DM_Sans({ + subsets: ['latin'], + variable: '--font-body', + weight: ['300', '400', '500', '600'], +}); + +const mono = DM_Mono({ + subsets: ['latin'], + variable: '--font-mono', + weight: ['300', '400', '500'], +}); + +export const metadata = { + title: 'Crypto Butler — Your DeFi Wealth Agent', + description: 'AI-powered DeFi portfolio management. Set your goals, let the Butler work.', +}; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} diff --git a/examples/crypto-butler/app/page.jsx b/examples/crypto-butler/app/page.jsx new file mode 100644 index 0000000..b4232f2 --- /dev/null +++ b/examples/crypto-butler/app/page.jsx @@ -0,0 +1,365 @@ +'use client'; +import { useState, useRef, useEffect } from 'react'; +import { AllocationChart, ProjectionChart } from '../components/Charts'; +import { AgentPanel } from '../components/AgentPanel'; +import { SimulationPanel } from '../components/SimulationPanel'; +import WalletConnect from '../components/WalletConnect'; + +const EXAMPLE_PROMPTS = [ + "Grow my $5,000 USDC at moderate risk over 12 months", + "I have $10K and want to preserve capital while earning yield", + "Aggressive strategy for $2,000 USDC — I can handle risk", + "Saving $8K for a car in 6 months, low risk only", +]; + +function getGreeting() { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning."; + if (hour < 17) return "Good afternoon."; + return "Good evening."; +} + +export default function Home() { + const [walletConnected, setWalletConnected] = useState(false); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [agentLog, setAgentLog] = useState([]); + const [activeAgent, setActiveAgent] = useState(null); + const [currentResult, setCurrentResult] = useState(null); + const bottomRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, isLoading]); + + async function sendMessage(text) { + const msg = text || input.trim(); + if (!msg || isLoading) return; + + setInput(''); + setMessages(prev => [...prev, { role: 'user', content: msg }]); + setIsLoading(true); + setAgentLog([]); + setCurrentResult(null); + setActiveAgent('researcher'); + + // Simulate agent progression + const agentTimer1 = setTimeout(() => setActiveAgent('risk'), 2500); + const agentTimer2 = setTimeout(() => setActiveAgent('cfo'), 5000); + + try { + const res = await fetch('/api/butler', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: msg }), + }); + + const data = await res.json(); + + clearTimeout(agentTimer1); + clearTimeout(agentTimer2); + setActiveAgent(null); + + if (!res.ok) throw new Error(data.error); + + setAgentLog(data.agentLog || []); + setCurrentResult(data); + + setMessages(prev => [...prev, { + role: 'assistant', + content: data.recommendation, + simulation: data.simulation, + riskProfile: data.riskProfile, + yieldsAnalyzed: data.yieldsAnalyzed, + }]); + + } catch (err) { + clearTimeout(agentTimer1); + clearTimeout(agentTimer2); + setActiveAgent(null); + setMessages(prev => [...prev, { + role: 'error', + content: err.message || 'Something went wrong. Please try again.', + }]); + } finally { + setIsLoading(false); + } + } + + return ( +
+ {/* ── HEADER ── */} +
+
+
+
+ B +
+
+

+ Crypto Butler +

+

Your DeFi Wealth Agent · Cardano Preprod

+
+
+ setWalletConnected(true)} /> +
+
+ + {/* ── MAIN ── */} +
+ + {/* ── LEFT: Chat ── */} +
+ + {/* Welcome state */} + {messages.length === 0 && !isLoading && ( +
+
+ 🎩 +
+

+ {getGreeting()} +

+

+ Tell me your financial goal and I will deploy a team of AI agents to find the best DeFi strategy for you — live yields, risk-adjusted, ready to execute. +

+ + {!walletConnected && ( +
+ 💡 Connect your wallet to simulate on Cardano Preprod testnet +
+ )} + +
+ {EXAMPLE_PROMPTS.map((p, i) => ( + + ))} +
+
+ )} + + {/* Messages */} +
+ {messages.map((msg, i) => ( +
+ {msg.role === 'user' && ( +
+
+

{msg.content}

+
+
+ )} + + {msg.role === 'assistant' && ( + + )} + + {msg.role === 'error' && ( +
+

⚠️ {msg.content}

+
+ )} +
+ ))} + + {/* Loading state */} + {isLoading && ( +
+
+
+ 🎩 +
+ Butler is thinking... +
+
+ + + +
+
+ )} +
+
+ + {/* Input */} +
+ setInput(e.target.value)} + onKeyDown={e => e.key === 'Enter' && !e.shiftKey && sendMessage()} + placeholder="Tell me your goal... e.g. 'Grow $5K at moderate risk over 12 months'" + className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder-gray-600 outline-none" + disabled={isLoading} + /> + +
+
+ + {/* ── RIGHT: Panels ── */} +
+ + {currentResult?.simulation && ( + + )} +
+
+
+ ); +} + +// ── ASSISTANT MESSAGE ──────────────────────────────────────────── +function AssistantMessage({ rec, simulation, riskProfile }) { + const [tab, setTab] = useState('strategy'); + + if (!rec) return null; + + const tabs = [ + { id: 'strategy', label: 'Strategy' }, + { id: 'allocation', label: 'Allocation' }, + { id: 'projection', label: 'Projection' }, + ...(simulation ? [{ id: 'simulate', label: 'Testnet' }] : []), + ]; + + return ( +
+ {/* Butler header */} +
+
+ 🎩 +
+
+

{rec.greeting}

+ {riskProfile && ( +
+ + {riskProfile.tolerance} risk + + {riskProfile.timeHorizonMonths}mo horizon +
+ )} +
+
+ + {/* Tabs */} +
+ {tabs.map(t => ( + + ))} +
+ + {/* Tab content */} +
+ + {/* STRATEGY */} + {tab === 'strategy' && ( +
+

{rec.summary}

+ +
+
+

Blended APY

+

{rec.projections?.blendedApy?.toFixed(1)}%

+
+
+

Protocols

+

{rec.allocation?.length}

+
+
+ + {rec.risks?.length > 0 && ( +
+

⚠️ Key Risks

+
    + {rec.risks.map((r, i) => ( +
  • • {r}
  • + ))} +
+
+ )} + +

+ "{rec.butlerNote}" +

+
+ )} + + {/* ALLOCATION */} + {tab === 'allocation' && rec.allocation && ( +
+ +
+ {rec.allocation.map((a, i) => ( +
+
+
+

+ {a.protocol} {a.pool} +

+ {a.apy}% APY +
+

{a.role} · {a.chain}

+

{a.reasoning}

+
+
+ ))} +
+
+ )} + + {/* PROJECTION */} + {tab === 'projection' && rec.projections && ( + + )} + + {/* SIMULATE */} + {tab === 'simulate' && simulation && ( +
+ +
+ )} + {tab === 'simulate' && simulation && ( +
+

See the Testnet panel on the right →

+
+ )} +
+
+ ); +} diff --git a/examples/crypto-butler/app/providers.jsx b/examples/crypto-butler/app/providers.jsx new file mode 100644 index 0000000..37a6aa0 --- /dev/null +++ b/examples/crypto-butler/app/providers.jsx @@ -0,0 +1,4 @@ +'use client'; +export function Providers({ children }) { + return <>{children}; +} diff --git a/examples/crypto-butler/components/AgentPanel.jsx b/examples/crypto-butler/components/AgentPanel.jsx new file mode 100644 index 0000000..20a504f --- /dev/null +++ b/examples/crypto-butler/components/AgentPanel.jsx @@ -0,0 +1,85 @@ +'use client'; + +const AGENTS = { + researcher: { + icon: '🔬', + label: 'Researcher', + color: 'text-blue-400', + border: 'border-blue-500/30', + bg: 'bg-blue-500/10', + }, + risk: { + icon: '⚖️', + label: 'Risk Analyst', + color: 'text-purple-400', + border: 'border-purple-500/30', + bg: 'bg-purple-500/10', + }, + cfo: { + icon: '💼', + label: 'CFO Agent', + color: 'text-gold-400', + border: 'border-gold-500/30', + bg: 'bg-gold-500/10', + }, +}; + +export function AgentPanel({ agentLog = [], isLoading, activeAgent }) { + const allAgents = ['researcher', 'risk', 'cfo']; + + // Derive which agents have completed based on log + const completed = new Set(agentLog.map(l => l.agent)); + + return ( +
+

+ Agent Swarm +

+ + {allAgents.map((agentKey) => { + const agent = AGENTS[agentKey]; + const isActive = isLoading && activeAgent === agentKey; + const isDone = completed.has(agentKey) && !isActive; + const log = agentLog.filter(l => l.agent === agentKey).pop(); + + return ( +
+
+
+ {agent.icon} + + {agent.label} + +
+
+ {isActive && ( + <> + + + + + )} + {isDone && } +
+
+ + {log && ( +

+ {log.status} +

+ )} +
+ ); + })} +
+ ); +} diff --git a/examples/crypto-butler/components/Charts.jsx b/examples/crypto-butler/components/Charts.jsx new file mode 100644 index 0000000..8385421 --- /dev/null +++ b/examples/crypto-butler/components/Charts.jsx @@ -0,0 +1,113 @@ +'use client'; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, AreaChart, Area, XAxis, YAxis, CartesianGrid } from 'recharts'; + +const COLORS = ['#f5c842', '#60a5fa', '#34d399', '#a78bfa', '#fb923c', '#f472b6']; + +export function AllocationChart({ allocation }) { + const data = allocation.map((a, i) => ({ + name: `${a.protocol} ${a.pool}`, + value: a.percentage, + amount: a.amountUsd, + apy: a.apy, + color: COLORS[i % COLORS.length], + })); + + return ( +
+ {/* Pie Chart */} +
+ + + + {data.map((entry, i) => ( + + ))} + + [`${value}%`, name]} + /> + + +
+ + {/* Legend */} +
+ {data.map((item, i) => ( +
+
+
+ {item.name} +
+
+ {item.apy}% + ${item.amount?.toFixed(0)} + {item.value}% +
+
+ ))} +
+
+ ); +} + +export function ProjectionChart({ projections, initialAmount }) { + const data = [ + { month: 'Now', value: initialAmount }, + { month: '3mo', value: projections.months3 }, + { month: '6mo', value: projections.months6 }, + { month: '12mo', value: projections.months12 }, + ]; + + const gain = projections.months12 - initialAmount; + const gainPct = ((gain / initialAmount) * 100).toFixed(1); + + return ( +
+
+
+

Projected at 12mo

+

+ ${projections.months12?.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} +

+
+
+

Total gain

+

+${gain.toFixed(0)} ({gainPct}%)

+
+
+ + + + + + + + + + + + `$${(v / 1000).toFixed(1)}k`} + /> + [`$${v.toFixed(0)}`, 'Portfolio Value']} + /> + + + +
+ ); +} diff --git a/examples/crypto-butler/components/SimulationPanel.jsx b/examples/crypto-butler/components/SimulationPanel.jsx new file mode 100644 index 0000000..40526c0 --- /dev/null +++ b/examples/crypto-butler/components/SimulationPanel.jsx @@ -0,0 +1,62 @@ +'use client'; +import { useState } from 'react'; + +export function SimulationPanel({ simulation }) { + const [expanded, setExpanded] = useState(false); + + if (!simulation) return null; + + return ( +
+
+
+
+

+ Testnet Simulation +

+
+ {simulation.network} +
+ +
+
+

Transactions

+

{simulation.totalTransactions}

+
+
+

Est. Gas

+

{simulation.totalGasEstimate}

+
+
+ + {/* Transaction list */} +
+ {simulation.transactions.slice(0, expanded ? undefined : 2).map((tx, i) => ( +
+
+
+

{tx.action}

+

{tx.txHash}

+
+
+
+ {tx.status} +
+
+
+ ))} +
+ + {simulation.transactions.length > 2 && ( + + )} + +

{simulation.disclaimer}

+
+ ); +} diff --git a/examples/crypto-butler/components/UTXOsWallet.jsx b/examples/crypto-butler/components/UTXOsWallet.jsx new file mode 100644 index 0000000..8b4a0d0 --- /dev/null +++ b/examples/crypto-butler/components/UTXOsWallet.jsx @@ -0,0 +1,75 @@ +'use client'; +import { useState, useEffect, useRef } from 'react'; + +export default function UTXOsWallet({ onConnected }) { + const [showIframe, setShowIframe] = useState(false); + const [connected, setConnected] = useState(false); + const [address, setAddress] = useState(''); + const iframeRef = useRef(null); + + const projectId = process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID; + const iframeUrl = `https://app.utxos.dev/?projectId=${projectId}&chain=cardano&network=preprod`; + + useEffect(() => { + function handleMessage(event) { + if (!event.origin.includes('utxos.dev')) return; + + const { type, address, balance } = event.data || {}; + + if (type === 'wallet_connected' && address) { + setAddress(address.slice(0, 8) + '...' + address.slice(-4)); + setConnected(true); + setShowIframe(false); + onConnected?.({ address, balance }, 'social'); + } + } + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, [onConnected]); + + if (connected) { + return ( +
+
+ {address} +
+ ); + } + + return ( + <> + + + {showIframe && ( +
+
+ + + +