Skip to main content
Build a DeFi dashboard, alert system, or portfolio tracker with the Octav API. This guide walks through four real projects — from a vibecoded React dashboard to an autonomous AI agent — using every tool in the Octav developer ecosystem.
What you’ll build: A real-time portfolio dashboard, a transaction alert system, an AI portfolio agent, and a tax export tool — all powered by the same API.

Why Octav?

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.

One API, 65+ Chains

Portfolio, transactions, DeFi positions, NFTs, and historical data — all from one endpoint

Multiple Access Methods

REST API, MCP Server, CLI, x402 payments, Agent Skill, and llms.txt — pick the tool that fits your workflow

1-Credit Calls

Most calls cost 1 credit ($0.025). Credits never expire. No subscriptions.

AI-Native

Built for vibecoding — structured JSON output, LLM-friendly docs, and MCP integration

Architecture

Octav sits between the blockchain data layer and your application:
65+ Blockchains (EVM + Solana)
        |
    [Octav API]  <-- indexing, normalization, caching
        |
   +---------+---------+---------+
   |         |         |         |
REST API   MCP     CLI       x402
   |       Server    |       Payments
   |         |       |         |
Your App  Claude  Shell     AI Agents
          Cursor  Scripts   (no API key)
          VS Code  Cron

Getting Started

Get your API key and make your first request in under 3 minutes.

Get an API Key

Sign up at data.octav.fi and generate an API key. Purchase a credit package — the Starter pack (4,000 credits / $100) is plenty for testing.

Make Your First Request

Fetch the net worth of any wallet address:
curl -X GET "https://api.octav.fi/v1/nav?addresses=0x6426af179aabebe47666f345d69fd9079673f6cd" \
  -H "Authorization: Bearer YOUR_API_KEY"

Explore the Endpoints

The core endpoints you’ll use across all four projects:
EndpointWhat It ReturnsCost
/v1/portfolioFull portfolio with DeFi positions1 credit
/v1/navNet asset value in any currency1 credit
/v1/walletToken balances (no DeFi)1 credit
/v1/transactionsTransaction history with filters1 credit
/v1/token-overviewToken distribution across chains1 credit
/v1/historicalPortfolio snapshot at a past date1 credit
/v1/creditsYour remaining creditsFree
/v1/statusSync status for addressesFree

Project 1: Real-Time Portfolio Dashboard

Build a React dashboard that shows net worth, DeFi positions, and token distribution — using a prompt-first approach.

The Vibecoding Prompt

Give this prompt to Claude, Cursor, or your AI coding assistant of choice:
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.
MCP superpower: Install the MCP Server and your AI assistant can query live portfolio data while building the dashboard. It sees real response shapes, not just docs.

The Dashboard Component

Here’s what the AI generates (and what you’d refine):
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;
}

interface TokenOverview {
  tokens: Array<{ symbol: string; value: number; percentage: number }>;
}

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: In production, proxy API calls through your backend. Never expose OCTAV_API_KEY in client-side code. Use a Next.js API route or similar server-side proxy.

Project 2: Transaction Alert System

Build a whale-alert style notification system using the Octav CLI and cron. No Node.js runtime, no dependencies — just bash.

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
    osascript -e "display notification \"$line\" with title \"Octav Alert\"" 2>/dev/null
  done

  # Update state
  echo "$LATEST_TX" > "$STATE_FILE"
done

Schedule It

# Check every 10 minutes
*/10 * * * * /path/to/tx-alert.sh
Add with:
(crontab -l 2>/dev/null; echo "*/10 * * * * $HOME/scripts/tx-alert.sh") | crontab -

Customize with AI

Give Claude Code this prompt to extend the script:
I have a bash script that monitors crypto wallets using the octav CLI.
It currently sends macOS notifications. Modify it to:
1. Send alerts to a Slack webhook (URL in SLACK_WEBHOOK env var)
2. Filter to only swap and transfer transactions
3. Add a daily summary at midnight with total transaction count and volume

Project 3: AI Portfolio Agent

Connect the Octav MCP Server to your AI assistant and build an autonomous monitoring agent.

Set Up MCP

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"
      }
    }
  }
}

Natural Language Queries

Once connected, ask your AI assistant 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 and tell me which chains gained the most"
The MCP server exposes 14 tools — your AI assistant can call octav_get_portfolio, octav_get_transactions, octav_get_nav, and more directly from conversation.

Python Monitoring Agent

For autonomous monitoring, build a Python agent:
import requests
import time
import os
from datetime import datetime, timedelta

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:
        """Get net asset value"""
        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:
        """Get full portfolio with DeFi positions"""
        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:
        """Get transaction history with optional filters"""
        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:
        """Check remaining API credits"""
        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 — checks portfolio and alerts on 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)  # rate limit courtesy

                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 have API keys, use the x402 payment protocol. The agent pays per request with USDC — no key management needed.
# x402 agent call — no API key required
# The agent's wallet pays via HTTP 402 payment protocol
import requests

response = requests.get(
    'https://api.octav.fi/v1/agent/wallet?addresses=0x742d35Cc...',
    # No Authorization header needed — payment handled via x402
)

# If 402 is returned, the agent's x402-compatible HTTP client
# automatically handles the micropayment and retries
data = response.json()
Via the CLI:
# No API key needed — pays with x402
octav agent wallet --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68
octav agent portfolio --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68

Project 4: Tax & Accounting Export Tool

Export categorized transaction history to CSV for tax reporting.
import requests
import csv
import os
from datetime import datetime

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}'}

    # Transaction type categories for tax purposes
    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',
    )

Historical Snapshots

For year-end portfolio valuations, use the historical endpoint with subscribe-snapshot for automatic daily recording:
# Subscribe to daily snapshots (one-time setup)
octav historical subscribe-snapshot \
  --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68 \
  --description "Tax reporting - main wallet"

# Later, pull a specific date's snapshot
octav historical get \
  --addresses 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD68 \
  --date 2025-12-31

Developer Toolkit Overview

Choose the right tool for your use case:
ToolBest ForSetupAuth
REST APIWeb apps, backends, integrationsAny HTTP clientAPI key
MCP ServerAI assistants (Claude, Cursor, VS Code)npx octav-api-mcpAPI key
CLIShell scripts, cron jobs, terminal workflowscurl | sh or cargo install octavAPI key or env var
x402 PaymentsAutonomous AI agentsNo setupAgent wallet (USDC)
Agent SkillClaude Code, Codex, ChatGPTnpx skills add Octav-Labs/octav-api-skillAPI key
llms.txtFeed docs to any LLMPoint to URLN/A
Direct HTTP calls to https://api.octav.fi/v1/. Works with any language or framework. 65+ chains, 1-credit calls, structured JSON responses.
curl -X GET "https://api.octav.fi/v1/portfolio?addresses=0xABC..." \
  -H "Authorization: Bearer YOUR_API_KEY"

Advanced Patterns

Multi-Wallet Aggregation

Query up to 10 addresses in a single API call (still just 1 credit per address):
addresses = '0xABC...,0xDEF...,0x123...'
response = requests.get(
    f'https://api.octav.fi/v1/portfolio?addresses={addresses}',
    headers={'Authorization': f'Bearer {api_key}'}
)

# Returns an array — one portfolio object per address
portfolios = response.json()
total_nav = sum(float(p['networth']) for p in portfolios)

Rate Limit Handling

The API allows 360 requests per minute. Implement exponential backoff for reliability:
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

Filter out small token balances that clutter portfolio views:
portfolio = response.json()[0]

# Filter tokens worth less than $1
meaningful_tokens = {
    key: protocol
    for key, protocol in portfolio['assetByProtocols'].items()
    if float(protocol['value']) > 1.0
}

Webhook-Style Polling

Combine the CLI with cron for webhook-like behavior without running a server:
# Check every 5 minutes, alert on 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
See CLI Automations for complete, production-ready scripts.

What’s Next

API Reference

Complete endpoint documentation with interactive playground

MCP Server

Connect AI assistants to live portfolio data

CLI Automations

Production-ready cron scripts for monitoring and exports

Pricing

Credit packages starting at $100 for 4,000 calls
Ready to start? Get your API key at data.octav.fi and build your first project in minutes. Join the Discord if you need help.