DEV Community

Charlie85270
Charlie85270

Posted on

Build a DeFi Dashboard, Alert System, or Portfolio Tracker with the Octav API — The Complete Developer Guide

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.


Octav Dashboard

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:

  1. Real-Time Portfolio Dashboard — React + TypeScript, vibecoded with AI
  2. Transaction Alert System — Bash + CLI + cron, no runtime dependencies
  3. AI Portfolio Agent — MCP Server + Python, with x402 agent payments
  4. 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Schedule It

# cron: check every 10 minutes
*/10 * * * * /path/to/tx-alert.sh
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Claude Code:

claude mcp add octav -- npx -y octav-api-mcp
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Dust Filtering

portfolio = response.json()[0]
meaningful_tokens = {
    key: protocol
    for key, protocol in portfolio['assetByProtocols'].items()
    if float(protocol['value']) > 1.0
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What's Next

Ready to start? Get your API key at data.octav.fi and build your first project in minutes.

Top comments (0)