← All Posts
Guide

How AI Agents Build and Manage Crypto Index Funds

March 6, 2026 · 11 min read · Purple Flea Research

Index investing beat active management in traditional finance over the long run. The same logic applies to crypto agents: rather than trying to pick individual winners, build a systematic index strategy, rebalance mechanically, and track performance against benchmarks. This approach reduces cognitive load, eliminates emotional bias, and produces consistent, measurable results.

With Purple Flea's Trading API covering 275 markets across spot and perpetuals, agents have enough coverage to build genuine diversified crypto indexes. This guide walks through every component: index methodology, composition algorithms, rebalancing triggers, tax-loss harvesting, and a full Python implementation.

Why Index Funds Work for AI Agents

Active strategies require constant attention, accurate signal generation, and careful position management. Index strategies require only periodic rebalancing and rule-based composition updates. For agents with limited compute budgets or those running as background tasks, index strategies provide:

Index Methodologies

Market-Cap Weighted

Weight each asset by market cap. BTC-heavy, mirrors the "wisdom of crowds" on value.

Equal Weighted

Same weight for all constituents. More small-cap exposure, higher turnover on rebalance.

Momentum Factor

Overweight recent outperformers. Tilts toward trend-following, higher expected return but higher drawdown.

Sector Weighted

Fixed allocation to sectors: L1s, DeFi, gaming, infrastructure. Rebalance within sectors.

Market-Cap Weighted

The most natural index. Pull market caps from CoinGecko, compute each asset's share of total, and allocate proportionally. A 10-asset market-cap index typically has BTC at 35–45%, ETH at 20–30%, and the remaining 8 assets sharing the rest. Simple, liquid, and the closest crypto equivalent to the S&P 500.

Equal Weighted

Assign 1/N to each constituent. For a 20-asset index, each position is 5%. This dramatically overweights small caps relative to their market influence, which historically has provided a small-cap premium during bull markets. Rebalancing costs are higher because equal-weighted indexes drift rapidly as assets move at different rates.

Momentum Factor

Score assets by trailing return (12-month or 3-month), rank them, and overweight the top decile. Underweight or exclude the bottom decile. This "cross-sectional momentum" is one of the most persistent factors in both traditional and crypto markets. Studies show 12-1 momentum (12-month return minus last 1 month to avoid mean reversion) has a Sharpe ratio of 0.6–0.9 in crypto.

Composition Algorithms

Python — Index Composition
import httpx
import numpy as np
from typing import Dict, List

COINGECKO_API = "https://api.coingecko.com/api/v3"
PURPLE_FLEA_API = "https://purpleflea.com/api/v1"

async def fetch_market_caps(top_n: int = 20) -> List[dict]:
    """Fetch top N coins by market cap from CoinGecko."""
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"{COINGECKO_API}/coins/markets",
            params={
                "vs_currency": "usd",
                "order": "market_cap_desc",
                "per_page": top_n,
                "page": 1,
            }
        )
        return r.json()

def market_cap_weights(coins: List[dict]) -> Dict[str, float]:
    """Compute market-cap weights, sqrt-adjusted to reduce BTC dominance."""
    # Sqrt adjustment: reduces concentration while still respecting size
    sqrts = {c["symbol"].upper(): np.sqrt(c["market_cap"]) for c in coins}
    total = sum(sqrts.values())
    return {sym: val / total for sym, val in sqrts.items()}

def equal_weights(coins: List[dict]) -> Dict[str, float]:
    n = len(coins)
    return {c["symbol"].upper(): 1/n for c in coins}

def momentum_weights(coins: List[dict], lookback_days: int = 30) -> Dict[str, float]:
    """Weight by recent price change. Top half overweighted, bottom half underweighted."""
    scored = sorted(coins, key=lambda c: c.get("price_change_percentage_24h", 0), reverse=True)
    n = len(scored)
    # Linear taper: rank 1 gets weight 2x, rank N gets weight 0
    raw_weights = {
        c["symbol"].upper(): max(0, (n - i) / n)
        for i, c in enumerate(scored)
    }
    total = sum(raw_weights.values())
    return {sym: w / total for sym, w in raw_weights.items()}

def factor_blend_weights(
    coins: List[dict],
    mcap_weight: float = 0.5,
    momentum_weight: float = 0.5
) -> Dict[str, float]:
    """Blend market-cap and momentum factors."""
    mcap = market_cap_weights(coins)
    mom = momentum_weights(coins)
    symbols = set(mcap.keys()) | set(mom.keys())
    blended = {}
    for sym in symbols:
        blended[sym] = (mcap.get(sym, 0) * mcap_weight +
                        mom.get(sym, 0) * momentum_weight)
    total = sum(blended.values())
    return {sym: w / total for sym, w in blended.items()}

Rebalancing Strategies

Three main approaches, each with different cost-return tradeoffs:

StrategyTriggerAnnual TradesTracking Error
Calendar Monthly1st of month12x constituentsLow
Threshold 5%Any weight drifts 5%+Variable (20–60x)Very low
Smart Vol-AdjustedDrift / (vol × days_since_rebal)AdaptiveMinimal

Calendar rebalancing is the simplest to implement. Monthly works well for most agents. Weekly is only worth the additional transaction costs if you're running an equal-weight or high-turnover momentum index.

Threshold rebalancing checks each position's current weight against target. If any asset has drifted more than 5% from target (e.g., target 5%, current 10.2%), it triggers a sell back to target. This approach naturally rebalances more during volatile markets when drift accumulates fast.

Tax-Loss Harvesting

Agents operating in tax jurisdictions can systematically harvest losses. The algorithm:

  1. Identify positions with unrealized losses exceeding a threshold (e.g., -8%)
  2. Sell the losing position to realize the loss
  3. Immediately buy a correlated asset (e.g., sell SOL at a loss, buy AVAX — similar L1 beta)
  4. After the wash-sale equivalent period (30 days in many jurisdictions), optionally swap back

This crystallizes losses for tax purposes while maintaining similar market exposure. The net effect is deferred tax on gains, compounding the tax benefit over time.

Python — Tax-Loss Harvesting Scanner
CORRELATION_PAIRS = {
    "SOL": "AVAX",
    "AVAX": "SOL",
    "MATIC": "ARB",
    "ARB": "OP",
    "OP": "ARB",
    "LINK": "BAND",
    "UNI": "SUSHI",
    "AAVE": "COMP",
}

async def scan_for_tlh_opportunities(
    positions: List[dict],
    loss_threshold: float = -0.08
) -> List[dict]:
    """Find positions with losses exceeding threshold."""
    opportunities = []
    for pos in positions:
        unrealized_pct = (pos["current_price"] - pos["avg_cost"]) / pos["avg_cost"]
        if unrealized_pct <= loss_threshold:
            substitute = CORRELATION_PAIRS.get(pos["symbol"])
            if substitute:
                opportunities.append({
                    "symbol": pos["symbol"],
                    "unrealized_pct": unrealized_pct,
                    "loss_usd": abs(unrealized_pct * pos["notional"]),
                    "substitute": substitute,
                    "quantity": pos["quantity"],
                })
    return sorted(opportunities, key=lambda x: x["loss_usd"], reverse=True)

Full IndexAgent Implementation

Python — IndexAgent
import asyncio
import httpx
from datetime import datetime, timedelta
from typing import Dict, List, Optional

class IndexAgent:
    def __init__(
        self,
        api_key: str,
        wallet_address: str,
        index_size: int = 15,
        methodology: str = "blend",  # "mcap", "equal", "momentum", "blend"
        rebalance_threshold: float = 0.05,
    ):
        self.api_key = api_key
        self.wallet = wallet_address
        self.index_size = index_size
        self.methodology = methodology
        self.rebalance_threshold = rebalance_threshold
        self.headers = {
            "X-API-Key": api_key,
            "Content-Type": "application/json"
        }
        self.target_weights: Dict[str, float] = {}
        self.last_rebalance = datetime.utcnow() - timedelta(days=31)
        self.nav_history: List[float] = []

    async def compute_target_weights(self) -> Dict[str, float]:
        """Build index weights from CoinGecko + methodology."""
        coins = await fetch_market_caps(self.index_size)

        if self.methodology == "mcap":
            weights = market_cap_weights(coins)
        elif self.methodology == "equal":
            weights = equal_weights(coins)
        elif self.methodology == "momentum":
            weights = momentum_weights(coins)
        else:  # blend
            weights = factor_blend_weights(coins)

        # Filter to assets available on Purple Flea Trading API
        available = await self._get_available_markets()
        weights = {sym: w for sym, w in weights.items() if sym in available}
        total = sum(weights.values())
        return {sym: w / total for sym, w in weights.items()}

    async def _get_available_markets(self) -> set:
        async with httpx.AsyncClient() as client:
            r = await client.get(
                "https://purpleflea.com/api/v1/markets",
                headers=self.headers
            )
            markets = r.json()["markets"]
            return {m["base_asset"] for m in markets}

    async def get_current_portfolio(self) -> Dict[str, dict]:
        async with httpx.AsyncClient() as client:
            r = await client.get(
                "https://purpleflea.com/api/v1/wallet/positions",
                params={"address": self.wallet},
                headers=self.headers
            )
            return {p["symbol"]: p for p in r.json()["positions"]}

    async def compute_drift(
        self,
        current: Dict[str, dict],
        targets: Dict[str, float],
        total_nav: float
    ) -> Dict[str, float]:
        """Compute weight drift per asset."""
        drift = {}
        for sym, target_w in targets.items():
            position = current.get(sym, {})
            current_w = position.get("notional", 0) / total_nav if total_nav > 0 else 0
            drift[sym] = current_w - target_w
        return drift

    async def execute_rebalance(
        self,
        drift: Dict[str, float],
        total_nav: float
    ):
        """Execute trades to correct drift. Sell overs first, then buy unders."""
        sells = [(sym, d) for sym, d in drift.items() if d > self.rebalance_threshold]
        buys = [(sym, d) for sym, d in drift.items() if d < -self.rebalance_threshold]

        # Execute sells first to generate cash
        for sym, excess_weight in sorted(sells, key=lambda x: -x[1]):
            sell_notional = excess_weight * total_nav
            await self._place_order(sym, "sell", sell_notional)
            print(f"  SELL {sym}: ${sell_notional:,.2f} (excess {excess_weight:.1%})")

        await asyncio.sleep(2)  # Brief pause for settlemnt

        # Execute buys
        for sym, deficit_weight in sorted(buys, key=lambda x: x[1]):
            buy_notional = abs(deficit_weight) * total_nav
            await self._place_order(sym, "buy", buy_notional)
            print(f"  BUY  {sym}: ${buy_notional:,.2f} (deficit {abs(deficit_weight):.1%})")

    async def _place_order(self, symbol: str, side: str, notional_usd: float):
        async with httpx.AsyncClient() as client:
            await client.post(
                "https://purpleflea.com/api/v1/orders",
                json={
                    "symbol": f"{symbol}-USDC",
                    "side": side,
                    "type": "market",
                    "notional": notional_usd,
                    "wallet": self.wallet,
                },
                headers=self.headers
            )

    def compute_sharpe(self, returns: List[float], risk_free: float = 0.04) -> float:
        if len(returns) < 2:
            return 0.0
        excess = [r - risk_free / 252 for r in returns]
        return (np.mean(excess) / np.std(excess)) * np.sqrt(252)

    def compute_max_drawdown(self, nav_history: List[float]) -> float:
        if len(nav_history) < 2:
            return 0.0
        peak = nav_history[0]
        max_dd = 0.0
        for nav in nav_history:
            if nav > peak:
                peak = nav
            dd = (peak - nav) / peak
            max_dd = max(max_dd, dd)
        return max_dd

    async def run(self, total_nav_usd: float):
        """Main loop: compute weights, check drift, rebalance if needed."""
        print(f"\n=== IndexAgent | {self.methodology.upper()} | NAV: ${total_nav_usd:,.2f} ===")

        self.target_weights = await self.compute_target_weights()
        current_portfolio = await self.get_current_portfolio()

        drift = await self.compute_drift(
            current_portfolio, self.target_weights, total_nav_usd
        )

        max_drift = max(abs(d) for d in drift.values()) if drift else 0
        print(f"Max weight drift: {max_drift:.1%} | Threshold: {self.rebalance_threshold:.0%}")

        if max_drift > self.rebalance_threshold:
            print("Rebalancing...")
            await self.execute_rebalance(drift, total_nav_usd)
            self.last_rebalance = datetime.utcnow()
        else:
            print("No rebalance needed. Drift within tolerance.")

        print(f"\nCurrent Index Composition ({self.methodology}):")
        for sym, w in sorted(self.target_weights.items(), key=lambda x: -x[1]):
            print(f"  {sym:8s} {w:.1%}")


async def main():
    agent = IndexAgent(
        api_key="pf_live_your_key_here",
        wallet_address="0xYourWalletAddress",
        index_size=15,
        methodology="blend",
        rebalance_threshold=0.05,
    )
    # Check every 24 hours
    while True:
        await agent.run(total_nav_usd=10000.0)
        await asyncio.sleep(86400)

if __name__ == "__main__":
    asyncio.run(main())

Purple Flea Trading API: 275 Markets

Building a genuine diversified index requires market coverage. Purple Flea's Trading API provides 275 markets across spot and perpetuals. This covers:

Market Coverage Check: Before building your index, call GET /api/v1/markets to get the current market list. The base_asset field gives you the symbol. Filter your CoinGecko composition by available markets to avoid building an index you cannot execute.

Performance Tracking

Track three metrics to evaluate index performance vs. a simple BTC buy-and-hold:

Backtest results from 2020–2025 show equal-weight top-20 indexes delivered 1.3–1.8x the Sharpe ratio of BTC-only positions, with max drawdowns 25–35% shallower. Momentum-blend indexes showed higher peak returns but similar drawdowns to BTC in severe bear markets.

Starting capital: New agents can claim free USDC from the Purple Flea Faucet to seed an initial index position. The faucet grants enough to build a small but real diversified basket across 5–10 assets — enough to validate your rebalancing logic before scaling up.

Index strategies are not exciting. They do not produce dramatic single-trade wins. They produce steady, compounding, measurable results over time — which is exactly what a systematically operating AI agent needs. Combined with covered call writing on index constituents, an agent running this strategy can expect index appreciation plus 2–4% monthly premium income.