Learn how to build DeFi dashboards, portfolio trackers, and AI-powered crypto agents using the Octav API, MCP Server, CLI, and x402 payments. Complete guide with code examples.
Building crypto apps normally means stitching together 10+ APIs, normalizing data across chains, and maintaining a patchwork of indexers. Octav replaces all of that with a single API covering 65+ blockchains — portfolio data, transactions, DeFi positions, NFTs, and historical snapshots from one endpoint.
In this guide, you'll build four real projects:
- Real-Time Portfolio Dashboard — React + TypeScript, vibecoded with AI
- Transaction Alert System — Bash + CLI + cron, no runtime dependencies
- AI Portfolio Agent — MCP Server + Python, with x402 agent payments
- Tax & Accounting Export — Python CSV generation with full pagination
Each project uses a different tool from the Octav developer ecosystem. By the end, you'll know which tool to reach for in any situation.
Getting Started (3 Minutes)
Step 1: Sign up at data.octav.fi and generate an API key.
Step 2: Purchase credits. The Starter pack (400 credits / $10) is plenty for testing. Most API calls cost 1 credit ($0.025). Credits never expire.
Step 3: Make your first request.
cURL:
curl -X GET "https://api.octav.fi/v1/nav?addresses=0x6426af179aabebe47666f345d69fd9079673f6cd" \
-H "Authorization: Bearer YOUR_API_KEY"
JavaScript:
const response = await fetch(
'https://api.octav.fi/v1/nav?addresses=0x6426af179aabebe47666f345d69fd9079673f6cd',
{ headers: { 'Authorization': `Bearer ${process.env.OCTAV_API_KEY}` } }
);
const data = await response.json();
console.log(`Net Worth: $${data.nav}`);
// => Net Worth: $1,235,564.43
Python:
import requests
response = requests.get(
'https://api.octav.fi/v1/nav',
params={'addresses': '0x6426af179aabebe47666f345d69fd9079673f6cd'},
headers={'Authorization': f'Bearer {api_key}'}
)
data = response.json()
print(f"Net Worth: ${data['nav']:,.2f}")
# => Net Worth: $1,235,564.43
Core Endpoints
| Endpoint | What It Returns | Cost |
|---|---|---|
/v1/portfolio |
Full portfolio with DeFi positions | 1 credit |
/v1/nav |
Net asset value in any currency | 1 credit |
/v1/wallet |
Token balances (no DeFi) | 1 credit |
/v1/transactions |
Transaction history with filters | 1 credit |
/v1/token-overview |
Token distribution across chains | 1 credit |
/v1/historical |
Portfolio snapshot at a past date | 1 credit |
/v1/credits |
Your remaining credits | Free |
/v1/status |
Sync status for addresses | Free |
Project 1: Real-Time Portfolio Dashboard (Vibecoded)
The fastest way to build a dashboard: give your AI assistant the right prompt and let it generate the code.
The Prompt
Using the Octav API (docs: https://api-docs.octav.fi/llms.txt), build a React + TypeScript
portfolio dashboard with TailwindCSS.
Features:
- Input field for wallet address (EVM 0x... or Solana base58)
- Net worth display using GET /v1/nav?addresses={addr}
- DeFi positions grouped by protocol using GET /v1/portfolio?addresses={addr}
- Token distribution pie chart using GET /v1/token-overview?addresses={addr}&date={today}
- Loading states and error handling
Auth: Bearer token via OCTAV_API_KEY env var, proxied through a Next.js API route.
All Octav endpoints return JSON. Portfolio response includes networth, chains, and assetByProtocols.
Pro tip: Install the Octav MCP Server (
npx octav-api-mcp) in Claude Desktop or Cursor so your AI assistant can query live portfolio data while building the dashboard.
The Dashboard Component
import { useState, useEffect } from 'react';
interface Portfolio {
networth: string;
chains: Record<string, { name: string; value: string }>;
assetByProtocols: Record<string, {
name: string;
value: string;
protocolImage: string;
positions: Array<{
type: string;
assets: Array<{ symbol: string; value: string; balance: string }>;
}>;
}>;
}
interface NavData {
nav: number;
currency: string;
}
export function PortfolioDashboard({ address }: { address: string }) {
const [portfolio, setPortfolio] = useState<Portfolio | null>(null);
const [nav, setNav] = useState<NavData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!address) return;
async function fetchData() {
setLoading(true);
setError(null);
try {
const headers = { 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OCTAV_API_KEY}` };
const [portfolioRes, navRes] = await Promise.all([
fetch(`https://api.octav.fi/v1/portfolio?addresses=${address}`, { headers }),
fetch(`https://api.octav.fi/v1/nav?addresses=${address}`, { headers }),
]);
if (!portfolioRes.ok || !navRes.ok) {
throw new Error('Failed to fetch portfolio data');
}
const [portfolioData, navData] = await Promise.all([
portfolioRes.json(),
navRes.json(),
]);
setPortfolio(portfolioData[0]);
setNav(navData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchData();
}, [address]);
if (loading) return <div className="animate-pulse">Loading portfolio...</div>;
if (error) return <div className="text-red-500">Error: {error}</div>;
if (!portfolio || !nav) return null;
return (
<div className="space-y-6">
{/* Net Worth */}
<div className="bg-gray-900 rounded-xl p-6">
<p className="text-gray-400 text-sm">Net Worth</p>
<p className="text-4xl font-bold text-white">
${nav.nav.toLocaleString(undefined, { maximumFractionDigits: 2 })}
</p>
</div>
{/* Chain Distribution */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.values(portfolio.chains).map((chain) => (
<div key={chain.name} className="bg-gray-800 rounded-lg p-4">
<p className="text-gray-400 text-sm">{chain.name}</p>
<p className="text-white font-semibold">
${parseFloat(chain.value).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</p>
</div>
))}
</div>
{/* DeFi Positions by Protocol */}
<div className="space-y-4">
<h2 className="text-xl font-bold text-white">DeFi Positions</h2>
{Object.values(portfolio.assetByProtocols).map((protocol) => (
<div key={protocol.name} className="bg-gray-800 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<span className="text-white font-semibold">{protocol.name}</span>
<span className="text-gray-400">
${parseFloat(protocol.value).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</span>
</div>
{protocol.positions.map((position, i) => (
<div key={i} className="ml-4 text-sm text-gray-400">
<span className="uppercase text-xs text-gray-500">{position.type}</span>
{position.assets.map((asset, j) => (
<div key={j} className="flex justify-between">
<span>{asset.symbol}</span>
<span>${parseFloat(asset.value).toLocaleString()}</span>
</div>
))}
</div>
))}
</div>
))}
</div>
</div>
);
}
Security note: In production, proxy API calls through your backend. Never expose your API key in client-side code.
Project 2: Transaction Alert System (CLI + Cron)
No Node.js, no Python, no dependencies — just bash and the Octav CLI.
Install the CLI
curl -sSf https://raw.githubusercontent.com/Octav-Labs/octav-cli/main/install.sh | sh
octav auth set-key YOUR_API_KEY
The Alert Script
#!/bin/bash
# tx-alert.sh — Monitor wallets for new transactions, alert on large ones
ADDRESSES="0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68"
THRESHOLD_USD=1000
STATE_DIR="$HOME/.octav/state"
LOG_FILE="$HOME/.octav/logs/tx-alert.log"
mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"
for ADDR in $(echo "$ADDRESSES" | tr ',' '\n'); do
STATE_FILE="$STATE_DIR/last-tx-${ADDR:0:8}.txt"
LAST_SEEN=""
[ -f "$STATE_FILE" ] && LAST_SEEN=$(cat "$STATE_FILE")
# Fetch recent transactions
RESULT=$(octav transactions get --addresses "$ADDR" --limit 20 --raw 2>&1)
if [ $? -ne 0 ]; then
echo "[$(date)] ERROR fetching $ADDR: $RESULT" >> "$LOG_FILE"
continue
fi
LATEST_TX=$(echo "$RESULT" | jq -r '.transactions[0].hash // empty')
[ -z "$LATEST_TX" ] && continue
# Skip if no new transactions
[ "$LATEST_TX" = "$LAST_SEEN" ] && continue
# Process new transactions
echo "$RESULT" | jq -r --arg last "$LAST_SEEN" --argjson threshold "$THRESHOLD_USD" '
.transactions
| if $last == "" then .[:5] else [limit(20; .[] | select(.hash != $last))] end
| .[]
| select(
[.assets[]? | .value // 0 | tonumber] | add > $threshold
)
| "[\(.date)] \(.txType) $\([.assets[]? | .value // 0 | tonumber] | add | floor) on \(.chainKey) — \(.hash[:16])..."
' | while read -r line; do
echo "$line" >> "$LOG_FILE"
# macOS notification (remove this line on Linux)
osascript -e "display notification \"$line\" with title \"Octav Alert\"" 2>/dev/null
done
# Update state
echo "$LATEST_TX" > "$STATE_FILE"
done
Schedule It
# cron: check every 10 minutes
*/10 * * * * /path/to/tx-alert.sh
On macOS, use launchd for better sleep/wake handling — see the CLI Automations docs for full plist templates.
Project 3: AI Portfolio Agent (MCP + Python)
Set Up MCP
Add the Octav MCP server to your AI assistant:
Claude Desktop — edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"octav": {
"command": "npx",
"args": ["-y", "octav-api-mcp"],
"env": {
"OCTAV_API_KEY": "your-api-key-here"
}
}
}
}
Cursor — go to Cursor Settings > MCP and add the same config.
VS Code — add to settings.json:
{
"mcp": {
"servers": {
"octav": {
"command": "npx",
"args": ["-y", "octav-api-mcp"],
"env": {
"OCTAV_API_KEY": "your-api-key-here"
}
}
}
}
}
Claude Code:
claude mcp add octav -- npx -y octav-api-mcp
Once connected, you can ask questions like:
- "What's my total exposure to Aave across all chains for 0xABC...?"
- "Show me all swap transactions over $1,000 on Arbitrum in the last 30 days"
- "Compare my portfolio value today vs. 30 days ago"
Python Monitoring Agent
import requests
import time
import os
from datetime import datetime
class OctavPortfolioAgent:
"""Autonomous portfolio monitor using the Octav API"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = 'https://api.octav.fi/v1'
self.headers = {'Authorization': f'Bearer {api_key}'}
self.snapshots: dict[str, float] = {}
def get_nav(self, address: str, currency: str = 'USD') -> dict:
resp = requests.get(
f'{self.base_url}/nav',
params={'addresses': address, 'currency': currency},
headers=self.headers,
timeout=30,
)
resp.raise_for_status()
return resp.json()
def get_portfolio(self, address: str) -> dict:
resp = requests.get(
f'{self.base_url}/portfolio',
params={'addresses': address},
headers=self.headers,
timeout=30,
)
resp.raise_for_status()
return resp.json()[0]
def get_transactions(self, address: str, **filters) -> list:
params = {'addresses': address, **filters}
resp = requests.get(
f'{self.base_url}/transactions',
params=params,
headers=self.headers,
timeout=30,
)
resp.raise_for_status()
return resp.json()
def check_credits(self) -> int:
resp = requests.get(f'{self.base_url}/credits', headers=self.headers)
return resp.json().get('credits', 0)
def monitor(self, addresses: list[str], interval: int = 300, threshold_pct: float = 5.0):
"""Main monitoring loop — alerts on significant portfolio changes"""
print(f"Monitoring {len(addresses)} address(es) every {interval}s")
print(f"Alert threshold: {threshold_pct}% change")
print(f"Credits remaining: {self.check_credits()}")
while True:
for addr in addresses:
try:
nav = self.get_nav(addr)
current = nav['nav']
previous = self.snapshots.get(addr)
if previous:
change_pct = ((current - previous) / previous) * 100
if abs(change_pct) >= threshold_pct:
print(f"\n{'='*50}")
print(f"ALERT: {addr[:10]}... changed {change_pct:+.2f}%")
print(f" ${previous:,.2f} -> ${current:,.2f}")
print(f" {datetime.now().isoformat()}")
print(f"{'='*50}\n")
self.snapshots[addr] = current
time.sleep(2)
except requests.RequestException as e:
print(f"Error checking {addr[:10]}...: {e}")
time.sleep(interval)
if __name__ == '__main__':
agent = OctavPortfolioAgent(os.environ['OCTAV_API_KEY'])
agent.monitor(
addresses=['0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68'],
interval=300,
threshold_pct=3.0,
)
x402 Agent Payments
For autonomous AI agents that don't need API keys, use the x402 payment protocol — the agent pays per request with USDC:
# No API key needed — pays with x402
octav agent wallet --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68
octav agent portfolio --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68
Project 4: Tax & Accounting Export Tool
import requests
import csv
import os
class TaxExporter:
"""Export transaction history to CSV for tax reporting"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = 'https://api.octav.fi/v1'
self.headers = {'Authorization': f'Bearer {api_key}'}
TAX_CATEGORIES = {
'SWAP': 'Trade',
'TRANSFERIN': 'Receive',
'TRANSFEROUT': 'Send',
'CLAIM': 'Income',
'AIRDROP': 'Income',
'STAKE': 'DeFi',
'UNSTAKE': 'DeFi',
'DEPOSIT': 'DeFi',
'WITHDRAW': 'DeFi',
'BORROW': 'DeFi',
'REPAY': 'DeFi',
'APPROVE': 'Other',
}
def fetch_all_transactions(self, address: str, start_date: str, end_date: str) -> list:
"""Fetch all transactions with pagination"""
all_txs = []
offset = 0
limit = 250
while True:
resp = requests.get(
f'{self.base_url}/transactions',
params={
'addresses': address,
'startDate': start_date,
'endDate': end_date,
'limit': limit,
'offset': offset,
'sort': 'ASC',
},
headers=self.headers,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
txs = data if isinstance(data, list) else data.get('transactions', [])
if not txs:
break
all_txs.extend(txs)
if len(txs) < limit:
break
offset += limit
return all_txs
def export_csv(self, address: str, year: int, output_path: str):
"""Export a full year of transactions to CSV"""
start_date = f'{year}-01-01'
end_date = f'{year}-12-31'
print(f"Fetching transactions for {address[:10]}... ({start_date} to {end_date})")
transactions = self.fetch_all_transactions(address, start_date, end_date)
print(f"Found {len(transactions)} transactions")
with open(output_path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'Date', 'Type', 'Tax Category', 'Chain',
'Asset', 'Amount', 'Value (USD)',
'Fee (USD)', 'Transaction Hash',
])
for tx in transactions:
tx_type = tx.get('txType', 'UNKNOWN')
tax_category = self.TAX_CATEGORIES.get(tx_type, 'Other')
fee_usd = sum(float(f.get('value', 0)) for f in tx.get('fees', []))
for asset in tx.get('assets', []):
writer.writerow([
tx.get('date', ''),
tx_type,
tax_category,
tx.get('chainKey', ''),
asset.get('symbol', ''),
asset.get('balance', ''),
asset.get('value', ''),
f'{fee_usd:.2f}',
tx.get('hash', ''),
])
print(f"Exported to {output_path}")
if __name__ == '__main__':
exporter = TaxExporter(os.environ['OCTAV_API_KEY'])
exporter.export_csv(
address='0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68',
year=2025,
output_path='crypto-taxes-2025.csv',
)
For automated daily recording, use the subscribe-snapshot endpoint:
# One-time setup: subscribe to daily snapshots
octav historical subscribe-snapshot \
--addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68 \
--description "Tax reporting - main wallet"
# Pull year-end snapshot for tax filing
octav historical get \
--addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68 \
--date 2025-12-31
Developer Toolkit Overview
| Tool | Best For | Setup | Auth |
|---|---|---|---|
| REST API | Web apps, backends, integrations | Any HTTP client | API key |
| MCP Server | AI assistants (Claude, Cursor, VS Code) | npx octav-api-mcp |
API key |
| CLI | Shell scripts, cron jobs, terminal workflows | `curl \ | sh or cargo install octav` |
| x402 Payments | Autonomous AI agents | No setup | Agent wallet (USDC) |
| Agent Skill | Claude Code, Codex, ChatGPT | npx skills add Octav-Labs/octav-api-skill |
API key |
| llms.txt | Feed docs to any LLM | Point to https://api-docs.octav.fi/llms.txt
|
N/A |
Advanced Patterns
Rate Limit Handling
The API allows 360 requests per minute. Implement exponential backoff:
import time
from requests.exceptions import RequestException
def fetch_with_retry(url: str, headers: dict, max_retries: int = 3):
for attempt in range(max_retries):
try:
resp = requests.get(url, headers=headers, timeout=30)
if resp.status_code == 429:
wait = int(resp.headers.get('Retry-After', 2 ** attempt))
time.sleep(wait)
continue
resp.raise_for_status()
return resp.json()
except RequestException:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
Dust Filtering
portfolio = response.json()[0]
meaningful_tokens = {
key: protocol
for key, protocol in portfolio['assetByProtocols'].items()
if float(protocol['value']) > 1.0
}
Webhook-Style Polling with Cron
# Check every 5 minutes for transaction changes
*/5 * * * * /path/to/portfolio-monitor.sh
# Daily snapshot at 9am
0 9 * * * /path/to/daily-snapshot.sh
# Weekly report on Sundays
0 10 * * 0 /path/to/weekly-report.sh
What's Next
- Full API Documentation — Interactive playground for every endpoint
- MCP Server — 14 tools for AI assistants
- CLI Automations — Production-ready cron scripts
- Pricing — Starting at $10 for 400 credits (never expire)
- Discord — Get help from the community
Ready to start? Get your API key at data.octav.fi and build your first project in minutes.

Top comments (0)