> ## Documentation Index
> Fetch the complete documentation index at: https://docs.circuit.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Index Agent

> An equal-weight token index that deploys USDC across a basket and rebalances on a weekly cadence.

<Tip>
  Scaffold this example locally and test it with the CLI:

  ```bash theme={null}
  circuit new --name my-index-agent --language python --template index
  cd my-index-agent
  circuit run    # execute a run cycle
  circuit unwind # test the unwind logic
  ```
</Tip>

### circuit.toml

```toml circuit.toml theme={null}
name = "Example Index Agent"
tagline = "Weekly equal-weight token index"
category = "index"
imageUrl = "https://cdn.circuit.org/agents/default"
walletType = "ethereum"
allowedExecutionModes = ["auto", "manual"]
runtimeIntervalMinutes = 10080 # weekly (7 * 24 * 60)

[startingAsset]
network = "ethereum:8453" # Base
address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC
minimumAmount = "10000000" # 10 USDC (6 decimals)
```

### Example

<CodeGroup>
  ```python Python theme={null}
  from circuit_sdk import AgentContext, SessionPortfolio

  NETWORK = "ethereum:8453"  # Base
  USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  # 6 decimals, ~$1

  # The basket this index tracks — equal weight across every entry. Swap these
  # for the tokens you want exposure to (e.g. the top AI tokens); each must be a
  # valid, liquid ERC-20 on NETWORK.
  INDEX_TOKENS = [
      {"symbol": "WETH", "address": "0x4200000000000000000000000000000000000006"},
      {"symbol": "cbBTC", "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf"},
  ]

  # Skip drift smaller than this so we don't churn fees on tiny moves.
  REBALANCE_BAND_USD = 1

  # One same-chain swap, quoted then executed. Used both ways: USDC -> token to
  # build the basket, token -> USDC to unwind it.
  def execute_swap(agent: AgentContext, from_token: str, to_token: str, amount_raw: int, label: str) -> None:
      quote = agent.swap.quote({
          "from": {"network": NETWORK, "address": agent.sessionWalletAddress},
          "to": {"network": NETWORK, "address": agent.sessionWalletAddress},
          "amount": str(amount_raw),
          "fromToken": from_token,
          "toToken": to_token,
      })
      if not quote.success or not quote.data:
          agent.log(f"{label} quote failed: {quote.error}", error=True)
          return

      result = agent.swap.execute(quote.data)
      if result.success:
          agent.log(f"{label}: ~{quote.data.asset_receive.amount_formatted} expected")
      else:
          agent.log(f"{label} failed: {result.error}", error=True)

  def run(agent: AgentContext) -> None:
      # Rebalance the basket back to equal weight: mark each leg's value, split the
      # portfolio's total evenly, then trim the overweight legs and top up the
      # underweight ones. The first run (all USDC, no tokens yet) is just the
      # all-underweight case, so this one path both builds and rebalances the basket.
      def balance(addr: str):
          return next(
              (b for b in agent.portfolio.balances if b.token_address.lower() == addr.lower()),
              None,
          )

      usdc = balance(USDC)
      cash_usd = int(usdc.amount_raw) / 1e6 if usdc else 0  # USDC is ~$1 — value it from raw, no oracle

      legs = []
      for token in INDEX_TOKENS:
          held = balance(token["address"])
          legs.append({
              "token": token,
              "raw": int(held.amount_raw) if held else 0,
              # Marked value from the portfolio; bail rather than guess if a held leg is unpriced.
              "value_usd": float(held.value_usd) if held and held.value_usd is not None else None,
          })

      if any(leg["raw"] > 0 and leg["value_usd"] is None for leg in legs):
          agent.log("A basket token is unpriced — skipping rebalance", error=True)
          return

      total = cash_usd + sum((leg["value_usd"] or 0) for leg in legs)
      if total == 0:
          agent.log("No funds to allocate")
          return
      target = total / len(INDEX_TOKENS)
      agent.log(f"Rebalancing ${total:.2f} to ${target:.2f} per token")

      # Trim overweight legs first so the USDC proceeds fund the top-ups below.
      for leg in legs:
          value = leg["value_usd"] or 0
          excess = value - target
          if excess <= REBALANCE_BAND_USD or leg["raw"] == 0:
              continue
          # Sell the fraction of the holding whose value equals the excess.
          sell_raw = leg["raw"] * round(excess * 1e6) // round(value * 1e6)
          execute_swap(agent, leg["token"]["address"], USDC, sell_raw, f"Trim {leg['token']['symbol']}")

      # Top up underweight legs from USDC (~$1, so a dollar gap is that many 6-decimal units).
      for leg in legs:
          shortfall = target - (leg["value_usd"] or 0)
          if shortfall <= REBALANCE_BAND_USD:
              continue
          execute_swap(agent, USDC, leg["token"]["address"], round(shortfall * 1e6), f"Add {leg['token']['symbol']}")

  def unwind(agent: AgentContext, portfolio: SessionPortfolio) -> None:
      # Sell every basket token held back to USDC.
      for token in INDEX_TOKENS:
          held = next(
              (b for b in portfolio.balances if b.token_address.lower() == token["address"].lower()),
              None,
          )
          held_raw = int(held.amount_raw) if held else 0
          if held_raw == 0:
              continue

          execute_swap(agent, token["address"], USDC, held_raw, f"Sell {token['symbol']}")
  ```

  ```typescript TypeScript theme={null}
  import type { AgentContext, SessionPortfolio } from "circuit:sdk";

  const NETWORK = "ethereum:8453"; // Base
  const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // 6 decimals, ~$1

  // The basket this index tracks — equal weight across every entry. Swap these
  // for the tokens you want exposure to (e.g. the top AI tokens); each must be a
  // valid, liquid ERC-20 on NETWORK.
  const INDEX_TOKENS = [
    { symbol: "WETH", address: "0x4200000000000000000000000000000000000006" },
    { symbol: "cbBTC", address: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf" },
  ] as const;

  // Skip drift smaller than this so we don't churn fees on tiny moves.
  const REBALANCE_BAND_USD = 1;

  // One same-chain swap, quoted then executed. Used both ways: USDC -> token to
  // build the basket, token -> USDC to unwind it.
  async function executeSwap(
    agent: AgentContext,
    fromToken: string,
    toToken: string,
    amountRaw: bigint,
    label: string,
  ): Promise<void> {
    const quote = await agent.swap.quote({
      from: { network: NETWORK, address: agent.sessionWalletAddress },
      to: { network: NETWORK, address: agent.sessionWalletAddress },
      amount: amountRaw.toString(),
      fromToken,
      toToken,
    });
    if (!quote.success || !quote.data) {
      await agent.log(`${label} quote failed: ${quote.error}`, { error: true });
      return;
    }

    const result = await agent.swap.execute(quote.data);
    if (result.success) {
      await agent.log(`${label}: ~${quote.data.assetReceive.amountFormatted} expected`);
    } else {
      await agent.log(`${label} failed: ${result.error}`, { error: true });
    }
  }

  export async function run(agent: AgentContext): Promise<void> {
    // Rebalance the basket back to equal weight: mark each leg's value, split the
    // portfolio's total evenly, then trim the overweight legs and top up the
    // underweight ones. The first run (all USDC, no tokens yet) is just the
    // all-underweight case, so this one path both builds and rebalances the basket.
    const balance = (addr: string) =>
      agent.portfolio.balances.find((b) => b.tokenAddress.toLowerCase() === addr.toLowerCase());

    const usdc = balance(USDC);
    const cashUsd = usdc ? Number(usdc.amountRaw) / 1e6 : 0; // USDC is ~$1 — value it from raw, no oracle

    const legs = INDEX_TOKENS.map((token) => {
      const held = balance(token.address);
      return {
        token,
        raw: held ? BigInt(held.amountRaw) : 0n,
        // Marked value from the portfolio. Bail rather than guess if a held leg is
        // unpriced — never rebalance on a fabricated value.
        valueUsd: held && held.valueUsd != null ? Number(held.valueUsd) : null,
      };
    });

    if (legs.some((leg) => leg.raw > 0n && leg.valueUsd === null)) {
      await agent.log("A basket token is unpriced — skipping rebalance", { error: true });
      return;
    }

    const total = cashUsd + legs.reduce((sum, leg) => sum + (leg.valueUsd ?? 0), 0);
    if (total === 0) {
      await agent.log("No funds to allocate");
      return;
    }
    const target = total / INDEX_TOKENS.length;
    await agent.log(`Rebalancing $${total.toFixed(2)} to $${target.toFixed(2)} per token`);

    // Trim overweight legs first so the USDC proceeds fund the top-ups below.
    for (const leg of legs) {
      const value = leg.valueUsd ?? 0;
      const excess = value - target;
      if (excess <= REBALANCE_BAND_USD || leg.raw === 0n) continue;
      // Sell the fraction of the holding whose value equals the excess.
      const sellRaw = (leg.raw * BigInt(Math.round(excess * 1e6))) / BigInt(Math.round(value * 1e6));
      await executeSwap(agent, leg.token.address, USDC, sellRaw, `Trim ${leg.token.symbol}`);
    }

    // Top up underweight legs from USDC (~$1, so a dollar gap is that many 6-decimal units).
    for (const leg of legs) {
      const shortfall = target - (leg.valueUsd ?? 0);
      if (shortfall <= REBALANCE_BAND_USD) continue;
      await executeSwap(agent, USDC, leg.token.address, BigInt(Math.round(shortfall * 1e6)), `Add ${leg.token.symbol}`);
    }
  }

  export async function unwind(agent: AgentContext, portfolio: SessionPortfolio): Promise<void> {
    // Sell every basket token held back to USDC.
    for (const token of INDEX_TOKENS) {
      const held = portfolio.balances.find(
        (b) => b.tokenAddress.toLowerCase() === token.address.toLowerCase()
      );
      const heldRaw = held ? BigInt(held.amountRaw) : 0n;
      if (heldRaw === 0n) continue;

      await executeSwap(agent, token.address, USDC, heldRaw, `Sell ${token.symbol}`);
    }
  }
  ```
</CodeGroup>

### Sample Output

```text theme={null}
Agent run started
Rebalancing $102.40 to $51.20 per token
Trim WETH: ~12.30 expected
Add cbBTC: ~0.00012 expected
Agent run completed
```

```text theme={null}
Agent unwind started
Sell WETH: ~48.90 expected
Sell cbBTC: ~51.30 expected
Agent unwind completed
```

### How It Works

1. **Mark the basket**: Reads each token's held amount and current USD value, plus idle USDC (valued from its raw balance, since it's \~\$1).
2. **Set the target**: Splits the portfolio's total value equally across the basket — that's each token's target allocation.
3. **Trim overweight**: For any leg above target by more than the rebalance band, sells just the excess fraction back to USDC.
4. **Top up underweight**: For any leg below target, buys the shortfall with USDC — funded by the trims plus any idle cash.
5. **Weekly cadence**: `runtimeIntervalMinutes` is set to a weekly interval. The first run (all USDC) builds the basket; later runs correct whatever weights have drifted.
6. **Unwind**: Sells every basket token held back to USDC.

### Notes

* **The basket is yours to curate.** `INDEX_TOKENS` seeds with WETH and cbBTC as a runnable example — replace them with the tokens you want exposure to (e.g. the top AI tokens). Each must be a valid, liquid ERC-20 on the configured network.
* All legs trade on a single network (Base) so swaps are same-chain and settle quickly. Point `startingAsset` and `NETWORK` at another chain if your basket lives elsewhere.
* Weights are computed from each balance's marked `valueUsd`. If a held token is unpriced the agent skips the run rather than rebalance on a fabricated value — it never coerces a missing price to zero.
* `REBALANCE_BAND_USD` is a deadband: drift smaller than it is left alone so the agent doesn't churn swap fees on noise.
* `agent.swap.quote(...)` returns a quote you pass straight to `agent.swap.execute(...)` — always check `result.success` before assuming the trade landed.
