Skip to main content
Scaffold this example locally and test it with the CLI:
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

circuit.toml

circuit.toml
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

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']}")

Sample Output

Agent run started
Rebalancing $102.40 to $51.20 per token
Trim WETH: ~12.30 expected
Add cbBTC: ~0.00012 expected
Agent run completed
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.