> ## 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.

# AGENTSmd CLI SDK BUILD humanwrittencontext

# AGENTS.md

## Top-Level Context

Circuit is a consumer interface and general-purpose infrastructure for executing programmatic financial strategies (called "agents") on self-custodial wallets. It lets users run algorithmic agents - ranging from simple yield farming scripts to complex AI-assisted trading strategies - on their own wallets across Ethereum, Solana, Hyperliquid, and other blockchains, without ever giving up custody of their private keys.

## What you can build

An agent is freeform code - `run()` on an interval plus `unwind()` - in TypeScript or Python. Any strategy expressible over the venues and data below is buildable; the catalog categories describe an agent, they don't constrain it. This section is the factual answer sheet for "what can I build here?" - quote from it, don't improvise.

**Venues - what an agent can trade:**

* **Hyperliquid perps & spot** - long or short any listed coin, optional leverage, one unified USDC account; `xyz:`-prefixed builder-DEX markets add RWAs (gold, equities, indices). The default venue for directional exposure.
* **Onchain swaps & transfers** - 23 EVM chains (Ethereum, Base, Arbitrum, …) plus Solana; the complete set is in "Network Identifiers".
* **Lending & yield** - supply real assets to established protocols (Aave, Moonwell, and the like); tokenized RWA routes via vaults and wrappers (see "RWA yield and spot exposure").
* **Kraken spot (exchange credential)** - trade spot on the user's own Kraken account through a connected API credential; the session gets a USD-denominated virtual slice of the account, orders are **limit-only**, and pairs are allowlisted (XBT/ETH/SOL vs USD/USDC). See "Kraken Integration".
* **Custom transactions** - arbitrary contract calls and signing for anything the SDK doesn't wrap (see "Custom Transactions / Signing").

**Data - what an agent can see:** live prices and mids per venue; swap quotes (also the depth probe); the session's wallet-backed and Polymarket holdings plus its available Kraken cash and base-asset inventory via `agent.allocation`; Kraken whole-account balances and orders through credentialed reads (never ownership authority); transaction history; DefiLlama for protocol TVL and yields; any external HTTP API via fetch (see "External Data Integration"); session-scoped memory for state between runs.

**The shape of every agent:** `run()` reads data, decides, and executes; `unwind()` closes everything back to a withdrawable state; `circuit.toml` sets the interval, starting asset, and user-tunable settings. Strategies range from a two-line DCA to a multi-venue basket - if the venues and data above can express it, it's in scope.

## Agent Requirements Checklist

**Money-correctness - get these wrong and funds are lost or the agent silently does nothing:**

1. **Verify every hardcoded token, ticker, address, and API path** from an authoritative source - never guess, recall from memory, or leave a placeholder. Check this file's supported networks/tokens and the SDK's asset constants first; for anything they don't list, confirm the address and ticker against a reputable source (a block explorer - Etherscan/Basescan/Solscan - or official docs) before hardcoding it. A wrong address is a silent, fund-losing bug that still compiles and passes `circuit check` - it sends real money to the wrong contract. The same goes for plausible-but-wrong tickers: Hyperliquid *spot* pairs use the live listing names (`UBTC/USDC`, not `BTC/USDC` - see "Spot orders" under Hyperliquid Integration). If you can't confirm an address or ticker, don't invent one.
2. **Never hardcode asset prices - fetch them at runtime.** A numeric literal in an order `price` field is a bug even as a placeholder. See "Realtime price lookups" below for the recipe per venue.
3. **Size every trade from exact onchain amounts or a live quote** - never a guessed, defaulted, or USD-oracle value. A failed or untrustworthy read must stop the decision, never fall back to a default (`?? 0`, `|| []`): a defaulted amount sizes a wildly wrong order. Read holdings from `agent.allocation`; derive a swap rate from the quote, not from a price.
4. **Every agent MUST have a real `unwind()`** that actually closes/exits all positions and returns funds to a withdrawable state - never just a log message. See "Unwind Patterns" below.

**Correct execution:**

5. **`agent.allocation` is the source of truth** for holdings assigned to the session at invocation start - never size from a raw whole-wallet or exchange-account read. `balances` is fungible inventory (cash, spot, staking, Kraken's synthetic available `kraken:allocation:USD` cash, and Kraken base inventory carrying `krakenMetadata`); `positions` is open market exposure (Hyperliquid perps and Polymarket outcome tokens). Circuit derives Kraken cash and inventory from the session's durable accepted orders plus watcher-ingested fills and exposes the exact buy/sell authority it enforces. The snapshot stays fixed within the invocation; each later trigger receives a new one. Never treat a whole-account Kraken balance or memory as session ownership.
6. **Read only the data the strategy needs.** `run()` executes for real every interval, so an expensive read is slow every time - read only the assets you trade, and prefer one bulk call (e.g. Hyperliquid `allMids`) over a per-asset loop across the whole market.

**Structure & tooling:**

7. **Use viem (TypeScript) or web3py (Python)** for numeric formatting, calldata encoding, and RPC reads - everything except signing a transaction.
8. **Keep ABIs in a separate constants file** (and most other constants too).
9. **Use DefiLlama** as the primary source for protocol/TVL/yield and high-to-medium-timeframe data; use direct onchain reads for time-sensitive, low-latency values.

**Legibility & UX:**

10. **Use `agent.log()` instead of print** - and make logs interesting to a human: include the live stats (price, APY, TVL, entry) and explain *why* the agent did what it did.
11. **Make starting assets easy to get** - native assets (ETH/SOL) or stablecoins (USDC/USDT), never wrapped assets like WETH - and set `startingAsset.minimumAmount` (and any in-code size floors) to the smallest the strategy can truly run with, so a user can test with a few dollars. The only valid floor is a real protocol constraint (e.g. Hyperliquid rejects orders below \~\$10 notional).
12. **`DESCRIPTION.md` is the agent's plan and source of truth** - it MUST contain all five sections (`## Summary`, `## What it is`, `## How it works`, `## Strategy`, `## Risks`), with `## What it is` using exactly your `circuit.toml` `category`'s labels. `circuit check` validates this and names what's missing. See "The plan" under Configuration.

***

## Choosing where to execute

The strategy names *what* to do; you choose *where*. **Default to Hyperliquid perps for directional exposure** - going long or short a coin, an index, a spread, a momentum or mean-reversion view. Hyperliquid is one deep, unified, USDC-settled venue with low fees and tight spreads; onchain DEX liquidity is fickle, fragmented across chains and pools, and expensive (gas + swap fees + slippage). Expressing a view as a perp is almost always cheaper, deeper, and more legible than swapping spot tokens on a DEX.

* **Index and spread strategies are almost always perps.** A spread is long one perp and short another at equal notional; an index is a basket of perp positions. Built from DEX spot swaps instead, these are many fickle, high-fee legs that drift from their target weights - a perp basket is one venue, cheap to rebalance, and simple to unwind.
* **Perps: Hyperliquid native for standard crypto, a builder DEX for RWAs.** Native perps have the deepest liquidity and tightest spreads; use `xyz:`-prefixed builder-DEX symbols only for assets the native venue doesn't list (gold, equities, indices). Detail in "Hyperliquid Integration". Perps give *price* exposure to an RWA; holding or earning yield on the real tokenized asset is a different discipline - see "RWA yield and spot exposure".
* **Size to your collateral (\~1×) unless the strategy explicitly wants leverage.** A perp used for plain exposure should not add liquidation risk the strategy never asked for.
* **Long-horizon accumulation (DCA) targets spot, not perps.** When the edge is holding an asset over weeks or months, buy and hold the spot asset - perps bleed funding over long holds and carry liquidation/rollover risk a buy-and-hold thesis never wanted. Perps are for active directional views; spot is for patient accumulation.
* **Yield is almost always an onchain lending protocol.** Earning yield means holding the real asset and supplying it to an established lending market (Aave, Moonwell, and the like) - this is the main reason to go onchain. Otherwise go onchain only when the strategy must hold a specific asset (staking, or one Hyperliquid doesn't list); a directional bet never qualifies - don't reach for a DEX swap when a perp expresses the same view.
* **When you do go onchain, verify depth for your size.** The `agent.swap.quote` output is your depth probe - if the expected output implies large price impact for your amount, the venue is too shallow: log it and don't route, never widen slippage to force it through. Keep the strategy on one chain, on the cheapest chain with enough depth (L2s cost cents; Ethereum L1 pays off only when the depth or protocol is L1-only), and bridge only when the strategy genuinely spans chains.
* **Vet a protocol before routing funds to it.** Prefer established, audited protocols whose TVL and liquidity dwarf your position - check them on DefiLlama (its chain ids map to Circuit's networks). A tiny or brand-new pool with a headline APY is a safety risk, not an opportunity, and a user can't audit a venue they've never heard of.

***

## RWA yield and spot exposure

Builder-DEX perps (above) are *synthetic* RWA price exposure; holding the real tokenized asset or earning its yield is different. **An RWA opportunity is a route, not a token** - evaluate `(underlying, wrapper, entry path, yield mechanism, exit path, permission gate)`, never just a ticker and APY.

* **Discover on rwa.xyz unfiltered** (every chain and asset class - treasuries, credit, CLOs, reinsurance, loan pools), **then traverse up**: vaults routing into the asset (Morpho/4626 on EVM, Kamino on Solana) and yield derivatives repackaging it (Pendle PT for fixed rates) - the best-executable form is often a wrapper no issuer registry lists.
* **Holding ≠ earning**: the mechanism is NAV-accruing, rebasing, or **stake-required** (base token earns zero until staked) - stake-required means `run` implements the full sequence and `unwind` walks it back, cooldowns included.
* **Permissioning is per-path**: a KYC-gated mint is fine if a permissionless route exists in *and* out (secondary DEX liquidity, permissionless vault) - the agent's wallet cannot KYC.
* **Exit is a requirement**: redemption queues, T+N windows, and KYC redemption break a human-free `unwind` - quote the reverse swap for your full size *before* entering.
* **Live-verify every product** (chain, address, yield mechanism, exit terms) against issuer docs + rwa.xyz + a block explorer - RWA terms churn fast; same rule as tickers.

***

## Realtime price lookups

Every order price and every sizing calculation that divides a dollar amount by a unit price MUST derive from a value fetched at runtime. Do not substitute a "reasonable guess" even as a placeholder - numeric literals in those fields are wrong regardless of how they're multiplied. Pick one of:

**Hyperliquid mid prices (preferred for Hyperliquid perps/spot):** hit the public Hyperliquid info endpoint.

```typescript theme={null}
// TypeScript
async function getHyperliquidMid(symbol: string): Promise<number> {
  const colonIdx = symbol.indexOf(":");
  const body = colonIdx > 0
    ? { type: "allMids", dex: symbol.slice(0, colonIdx) }
    : { type: "allMids" };
  const res = await fetch("https://api.hyperliquid.xyz/info", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const mids = (await res.json()) as Record<string, string>;
  const price = mids[symbol];
  if (!price) throw new Error(`No mid price for ${symbol}`);
  return parseFloat(price);
}
```

```python theme={null}
# Python
import json, urllib.request

def get_hyperliquid_mid(symbol: str) -> float:
    body: dict = {"type": "allMids"}
    if ":" in symbol:
        body["dex"] = symbol.split(":", 1)[0]
    req = urllib.request.Request(
        "https://api.hyperliquid.xyz/info",
        data=json.dumps(body).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req) as resp:
        mids = json.loads(resp.read())
    if symbol not in mids:
        raise RuntimeError(f"No mid price for {symbol}")
    return float(mids[symbol])
```

Symbol format matches `placeOrder` for perps: `"BTC"`, `"ETH"`, `"xyz:GOLD"`, etc. Builder-DEX assets (anything with a `dexName:` prefix) aren't in the default `allMids` response - you must pass `{ dex: "<dexName>" }` in the request body to fetch them, as the snippets above do. The response key for a builder-DEX asset includes the prefix (e.g. `mids["xyz:GOLD"]`).

**Spot pairs need a different lookup** - `allMids` keys spot pairs as `"@<spotIndex>"`, not by pair name (`mids["UBTC/USDC"]` is always missing; only legacy `PURR/USDC` appears by name). Resolve the pair through spot metadata instead:

```typescript theme={null}
// TypeScript - pair is the live listing name, e.g. "UBTC/USDC" (see "Spot orders" under Hyperliquid Integration)
async function getHyperliquidSpotMid(pair: string): Promise<number> {
  const res = await fetch("https://api.hyperliquid.xyz/info", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ type: "spotMetaAndAssetCtxs" }),
  });
  const [meta, ctxs] = (await res.json()) as [
    {
      tokens: { index: number; name: string }[];
      universe: { index: number; tokens: [number, number] }[];
    },
    { coin: string; midPx: string | null }[],
  ];
  const tokenName = new Map(meta.tokens.map((t) => [t.index, t.name]));
  const entry = meta.universe.find(
    (u) => `${tokenName.get(u.tokens[0])}/${tokenName.get(u.tokens[1])}` === pair,
  );
  if (!entry) throw new Error(`Unknown spot pair: ${pair}`);
  const mid = ctxs.find((c) => c.coin === `@${entry.index}` || c.coin === pair)?.midPx;
  if (!mid) throw new Error(`No mid price for ${pair}`);
  return parseFloat(mid);
}
```

```python theme={null}
# Python - pair is the live listing name, e.g. "UBTC/USDC"
def get_hyperliquid_spot_mid(pair: str) -> float:
    req = urllib.request.Request(
        "https://api.hyperliquid.xyz/info",
        data=json.dumps({"type": "spotMetaAndAssetCtxs"}).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req) as resp:
        meta, ctxs = json.loads(resp.read())
    token_name = {t["index"]: t["name"] for t in meta["tokens"]}
    for u in meta["universe"]:
        if f'{token_name[u["tokens"][0]]}/{token_name[u["tokens"][1]]}' == pair:
            key = f'@{u["index"]}'
            for c in ctxs:
                if c["coin"] in (key, pair) and c.get("midPx"):
                    return float(c["midPx"])
    raise RuntimeError(f"No mid price for {pair}")
```

Symbols are **exact-case and prefix-exact**: the live key is `xyz:GOLD` - `xyz:gold`, `GOLD`, or a lookup without `{ dex: "xyz" }` all silently return no price. `xyz:GOLD`, `xyz:SILVER`, `xyz:SP500`, etc. are **real live tickers** on the `xyz` real-world-assets builder DEX, so a `No mid price` here is almost always the lookup format (wrong case / missing prefix / missing `dex`), not a missing asset - fix the symbol string, don't assume the market doesn't exist.

**Existing positions:** your open perps at invocation start are in `agent.allocation.positions` (each carries `size`, `markPriceUsd`, and `hyperliquidMetadata` with leverage / liquidation price / margin used) - the canonical source for reading and unwinding your exposure. Fetch a fresh mid (above) when you need a current price for a close.

**EVM swaps:** `agent.swap.quote(...)` returns a live quote including expected output amount - derive implicit price from `toAmount / fromAmount` if you need it, and pass the quote to `agent.swap.execute(...)` without modification.

**DefiLlama:** use it for high/medium timeframe data (APY, TVL, historical prices) - not for placing orders. Never for slippage limits.

Once you have the mid price, compute the slippage limit as `mid * (1 + slippage)` for buys or `mid * (1 - slippage)` for sells. The multiplier (e.g. `1.01`, `0.99`) is allowed to be a literal; the price itself must not be.

***

## Agent Code Pattern

**TypeScript** (a complete, runnable pattern - a directional Hyperliquid perp opened in `run`, closed in `unwind`; this is the default venue, see "Choosing where to execute"):

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

// The asset is fixed by the strategy - a literal ticker, never derived at runtime.
const COIN = "BTC";

export async function run(agent: AgentContext): Promise<void> {
  const allocation = agent.allocation;
  // run() fires every interval - don't re-open a position we already hold.
  if (allocation.positions.some((p) => p.coin === COIN)) {
    await agent.log(`Already long ${COIN} - holding`);
    return;
  }

  const mid = await getHyperliquidMid(COIN); // live price - never hardcode one (see "Realtime price lookups")

  // Size from THIS session's allocation - the slice assigned to it, at ~1× (no
  // added leverage). Never size from a whole-wallet read: on a shared wallet that
  // would overspend the other sessions sharing it (see `allocation`).
  const cash = allocation.balances.find((b) => b.hyperliquidMetadata?.collateral);
  if (!cash) {
    await agent.log("No cash in this session's allocation to deploy");
    return;
  }
  // Exact raw math for the deploy gate - never float-divide an amountRaw.
  const collateralUsd = baseUnitsToDecimal(cash.amountRaw, cash.decimals);
  if (BigInt(cash.amountRaw) < BigInt(decimalToBaseUnits("10", cash.decimals))) {
    await agent.log(`Allocation $${collateralUsd} below $10 minimum`);
    return;
  }
  const size = Number(collateralUsd) / mid; // venue-human perp size from the live mid

  const order = await agent.platforms.hyperliquid.placeOrder({
    coin: COIN,
    side: "buy",
    size,
    price: mid * 1.01, // 1% slippage limit for a market buy
    market: "perp",
    type: "market",
  });
  await agent.log(
    `Long ${COIN} ${size.toFixed(4)} @ ~$${mid.toFixed(0)}: ${order.success ? order.data?.status : order.error}`,
  );
}

export async function unwind(agent: AgentContext): Promise<void> {
  const allocation = agent.allocation;
  // Close THIS session's perps, sized from its allocation (its scoped share) - never
  // a whole-wallet read, which on a shared wallet would close other sessions' perps.
  for (const pos of allocation.positions) {
    if (pos.network !== "hypercore:perp") continue; // HL perps only
    // pos.size is a signed decimal string: long > 0, short < 0. Keep it exact -
    // strip the sign for the venue size instead of float math.
    const isShort = pos.size.startsWith("-");
    const size = Number(isShort ? pos.size.slice(1) : pos.size); // venue-human number
    if (size === 0) continue;
    const mark = await getHyperliquidMid(pos.coin); // live price for the close bound
    if (size * mark < 10) {
      // A reduce-only close can't round size up, so a sub-$10 position is stuck - skip it.
      await agent.log(`Skipping ${pos.coin}: notional below $10 minimum`);
      continue;
    }
    const closeSide = isShort ? "buy" : "sell"; // long → sell to close; short → buy
    const result = await agent.platforms.hyperliquid.placeOrder({
      coin: pos.coin,
      side: closeSide,
      size,
      price: closeSide === "sell" ? mark * 0.5 : mark * 1.5, // wide limit to guarantee the fill
      market: "perp",
      type: "market",
      reduceOnly: true,
    });
    await agent.log(`Closed ${pos.coin}: ${result.success ? result.data?.status : result.error}`);
  }
}
```

Always check `result.success` before `result.data`, and make `unwind` do real work (not just a log). Hyperliquid sizes and prices are formatted numbers; EVM swap amounts are strings in smallest units. See "Unwind Patterns" below for the protocol/yield and swap-based unwind shapes.

The top-level `run` (required) and `unwind` (optional) function exports ARE the agent's contract - no `new Agent({...})` wrapper, no boilerplate.

The SDK is provided by the environment (the CLI locally, the hosted runtime when deployed) - `circuit:sdk` / `circuit_sdk` is NEVER a dependency in `package.json` or `pyproject.toml`; do not add it. If the imports stop resolving in your editor, run `circuit check` - it repairs the CLI-managed SDK installation.

**Python:**

```python theme={null}
from circuit_sdk import AgentContext

# The asset is fixed by the strategy - a literal ticker, never derived at runtime.
COIN = "BTC"

def run(agent: AgentContext) -> None:
    allocation = agent.allocation
    # run() fires every interval - don't re-open a position we already hold.
    if any(p.coin == COIN for p in allocation.positions):
        agent.log(f"Already long {COIN} - holding")
        return

    mid = get_hyperliquid_mid(COIN)  # live price - never hardcode one (see "Realtime price lookups")

    # Size from THIS session's allocation - the slice assigned to it, at ~1x (no
    # added leverage). Never size from a whole-wallet read: on a shared wallet that
    # would overspend the other sessions sharing it (see allocation).
    cash = next(
        (b for b in allocation.balances if b.hyperliquid_metadata and b.hyperliquid_metadata.collateral),
        None,
    )
    if cash is None:
        agent.log("No cash in this session's allocation to deploy")
        return
    collateral_usd = int(cash.amount_raw) / 10**cash.decimals
    if collateral_usd < 10:
        agent.log(f"Allocation ${collateral_usd:.2f} below $10 minimum")
        return
    size = collateral_usd / mid

    agent.platforms.hyperliquid.place_order({
        "coin": COIN,
        "side": "buy",
        "size": size,
        "price": mid * 1.01,  # 1% slippage limit for a market buy
        "market": "perp",
        "type": "market",
    })
    agent.log(f"Long {COIN} {size:.4f}")

def unwind(agent: AgentContext) -> None:
    allocation = agent.allocation
    # Close THIS session's perps, sized from its allocation (its scoped share) - never
    # a whole-wallet read, which on a shared wallet would close other sessions' perps.
    for pos in allocation.positions:
        if pos.network != "hypercore:perp":  # HL perps only
            continue
        signed = float(pos.size)  # signed: long > 0, short < 0
        size = abs(signed)
        if size == 0:
            continue
        mark = get_hyperliquid_mid(pos.coin)  # live price for the close bound
        if size * mark < 10:  # reduce-only can't round up - a sub-$10 position is stuck; skip
            agent.log(f"Skipping {pos.coin}: notional below $10 minimum")
            continue
        close_side = "buy" if signed < 0 else "sell"  # long → sell to close; short → buy
        agent.platforms.hyperliquid.place_order({
            "coin": pos.coin,
            "side": close_side,
            "size": size,
            "price": mark * 0.5 if close_side == "sell" else mark * 1.5,
            "market": "perp",
            "type": "market",
            "reduceOnly": True,
        })
        agent.log(f"Closed {pos.coin}")
```

### Unwind Patterns

`unwind()` is called when a user stops a session. It must close/exit all positions and return assets to a state the user can withdraw. The default Hyperliquid case is the canonical example above - close each open position with a reduce-only market order, skipping any whose notional has decayed below \$10. The other shapes:

**Swap-based agents** (holding non-starting tokens): Swap everything back to the starting asset.

```typescript theme={null}
async function unwind(agent: AgentContext): Promise<void> {
  const allocation = agent.allocation;
  for (const bal of allocation.balances) {
    if (bal.tokenAddress.toLowerCase() === STARTING_TOKEN.toLowerCase()) continue;
    if (BigInt(bal.amountRaw) === 0n) continue;

    const quote = await agent.swap.quote({
      from: { network: NETWORK, address: agent.sessionWalletAddress },
      to: { network: NETWORK, address: agent.sessionWalletAddress },
      amount: bal.amountRaw,
      fromToken: bal.tokenAddress,
      toToken: STARTING_TOKEN,
      slippage: "3.0",
    });
    if (quote.success && quote.data) {
      const result = await agent.swap.execute(quote.data);
      await agent.log(`Swapped ${bal.tokenAddress} back: ${result.success ? "ok" : result.error}`);
    }
  }
}
```

**Protocol/yield agents** (deposited into a contract): Withdraw from the protocol, then swap back if needed.

```typescript theme={null}
async function unwind(agent: AgentContext): Promise<void> {
  // Withdraw all from Aave
  const withdrawData = encodeFunctionData({
    abi: aavePoolAbi, functionName: "withdraw",
    args: [USDC, 2n ** 256n - 1n, agent.sessionWalletAddress],
  });
  const result = await agent.signAndSend({
    network: "ethereum:8453",
    request: { toAddress: AAVE_V3_POOL, data: withdrawData, value: "0" },
    message: "Withdraw all from Aave V3",
  });
  await agent.log(`Aave withdrawal: ${result.success ? "ok" : result.error}`);
}
```

### Multi-File Agent Pattern

For anything beyond a trivial agent, split code into modules. At minimum, put ABIs and addresses in a constants file:

**TypeScript (`constants.ts`):**

```typescript theme={null}
import { parseAbi } from "viem";

// -- Addresses (Base) --
export const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
export const AAVE_V3_POOL = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5";

// -- ABIs --
export const erc20Abi = parseAbi([
  "function approve(address spender, uint256 amount) returns (bool)",
  "function balanceOf(address account) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
]);

export const aavePoolAbi = parseAbi([
  "function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)",
  "function withdraw(address asset, uint256 amount, address to) returns (uint256)",
]);
```

**Python (`constants.py`):**

```python theme={null}
# -- Addresses (Base) --
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
AAVE_V3_POOL = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5"

# -- ABIs --
ERC20_ABI = [
    {"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"name": "", "type": "bool"}]},
    {"name": "balanceOf", "type": "function", "stateMutability": "view", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
]
```

Then in your entry file: `import { USDC, erc20Abi } from "./constants"` (TS) or `from constants import USDC, ERC20_ABI` (Python).

***

## SDK Reference

### Execution Model

1. Circuit sends a `run` or `unwind` command to your agent
2. SDK creates an `AgentContext` object
3. SDK calls your `run` function with the context
4. Your code uses SDK methods to analyze positions, execute trades, etc.
5. SDK returns results to Circuit
6. In **manual mode**, transactions are submitted for user approval in the Circuit UI

**Two modes:**

* `auto` - Transactions execute automatically
* `manual` - Transactions become suggestions for user approval

From the agent's perspective, the code is identical - Circuit's API handles the routing based on mode. In manual mode, the response `data` will be a `SuggestedTransactionData` object with `suggested: true` and `suggestionId: string` instead of the normal execution result. Actions with no suggestion card (`transfer`, `hyperliquidClose`) cannot be captured for approval — in manual mode they return an error instead of a suggestion.

**Type guards (TypeScript only):** The SDK exports two helpers to distinguish manual-mode suggestions from executed results:

```typescript theme={null}
import { isSuggestedTransaction } from "circuit:sdk";

const result = await agent.signAndSend({...});
if (result.success && result.data) {
  if (isSuggestedTransaction(result)) {
    await agent.log(`Transaction queued for approval (ID: ${result.data.suggestionId})`);
  } else {
    // Auto mode: data is the executed result - read its fields directly
    await agent.log(`Executed: ${result.data.txHash}`);
  }
}
```

Available type guards: `isSuggestedTransaction(response)` and `isSuccessResponse(response)`. In auto mode the response `data` is the executed result itself - read its fields (`txHash`, `orderId`, `status`, …) directly rather than reaching for a per-platform guard.

**Important**: All suggested transactions are automatically soft-deleted at the beginning of each `run` execution. Use `expiresAt` field for shorter expiry. Call `clearSuggestedTransactions()` / `clear_suggested_transactions()` to manually clear pending suggestions mid-run.

**Execution interval**: Set via the `[[triggers]]` schedule entry in `circuit.toml` (`every` minutes; `align = "rolling"` starts the interval after the previous `run` completes).

### Agent Context (`AgentContext`)

The `AgentContext` object is passed to both `run` and `unwind` functions. It contains:

**Session Data:**

* `sessionId` (TypeScript) / `session_id` (Python) (number) - Unique session identifier
* `sessionWalletAddress` (TypeScript) / `session_wallet_address` (Python) (string) - Wallet address for this session
* `allocation` (object) - the session's allocated slice at execution start, **not** the whole wallet/account: `allocation.balances` + `allocation.positions`. Kraken contributes its synthetic USD ceiling and each positive base-asset inventory balance; `krakenMetadata.availableBaseVolume` is the exact sell quantity enforced for the balance's `tokenAddress`. See "Kraken Integration".
* `executionMode` (TypeScript) / `execution_mode` (Python) (string) - `"auto"` or `"manual"`
* `settings` (object) - Resolved setting values (defaults merged with session overrides)

**Each balance in `allocation.balances`** (fungible inventory: cash, spot, staking):

* `network` (string) - e.g., `"ethereum:137"`, `"solana"`
* `assetKey` (string) - Canonical asset key
* `tokenAddress` (string) - Token contract address
* `tokenId` (string | null) - For NFTs/ERC1155
* `symbol` (string | null) - Token symbol
* `decimals` (number) - Token decimals
* `amountRaw` (string) - Quantity held in raw base units
* `marketValueUsd` (string | null) - Current market value in USD
* `hyperliquidMetadata` (`{ collateral: true }` | undefined) - present only on the **Hyperliquid perp-margin USDC balance** (the deployable collateral you size perps from). **Total basis**: includes margin already locked in open positions, excludes unrealized PnL. Equity = collateral + Σ positions' `unrealizedPnlUsd`; free margin ≈ equity − Σ positions' `hyperliquidMetadata.marginUsed`. Absent on everything else (EVM/Solana tokens, HL spot/staking) - use `network` to distinguish those. Find HL collateral with `balances.find(b => b.hyperliquidMetadata?.collateral)`.

**Each position in `allocation.positions`** (open market exposures: Hyperliquid perps and Polymarket outcome tokens):

* `network` (string), `assetKey` (string), `coin` (string) - perp coin or Polymarket outcome label
* `size` (string) - signed for perps (long > 0, short \< 0); shares held for Polymarket
* `averageEntryPrice` (string | null), `priceUsd` (string | null), `unrealizedPnlUsd` (string | null)
* `hyperliquidMetadata` (`{ leverage, liquidationPrice, marginUsed }` | undefined) - present only on Hyperliquid perps. `leverage` and `marginUsed` are strings; `liquidationPrice` is a string or null. These are wallet-level facts (sessions sharing a wallet share the netted perp, so the same leverage / liquidation price). Distinct from the *balance's* `hyperliquidMetadata` (`{ collateral: true }`): the same venue field carries collateral-flagging on a balance and leverage/margin detail on a position.
* `polymarketMetadata` (object | undefined) - present only on Polymarket positions; carries the market/outcome identity, current price, PnL, and redeemability.

**SDK Methods (TypeScript / Python):**

* `log()` - Send messages to users and log locally
* `memory` - Session-scoped key-value storage (`.set()`, `.get()`, `.delete()`)
* `swap` - Cross-chain swap operations (`.quote()`, `.execute()`)
* `platforms.hyperliquid` - Hyperliquid DEX integration
* `platforms.polymarket` - Polymarket discovery, orders, and redemption
* `platforms.kraken` - Kraken market data, account reads, and spot orders
* `signAndSend()` / `sign_and_send()` - Sign and broadcast custom transactions. **Local engine only**: hosted agent sessions refuse raw transactions because their spend cannot be bounded by the session's virtual allocation — use `swap`, `transfer`, or the venue methods instead.
* `signMessage()` / `sign_message()` - Sign messages (EVM only). **Local engine only**: hosted agent sessions refuse arbitrary message signing (an EIP-712 permit is a spend authorization outside the session's virtual allocation).
* `transactions()` - Get transaction history
* `clearSuggestedTransactions()` / `clear_suggested_transactions()` - Clear pending manual mode suggestions

### SDK Response Pattern

**Every** SDK method returns a response object with consistent shape:

```typescript theme={null}
{
  success: boolean;
  data?: any;       // present on success
  error?: string;   // present on failure
}
```

**Always check `success` before using `data`:**

```typescript theme={null}
const result = await agent.swap.quote({...});
if (result.success && result.data) {
  // Safe to use result.data
} else {
  await agent.log(result.error || "Request failed", { error: true });
}
```

**Uncaught exceptions** in `run`/`unwind` are automatically caught by the SDK - the execution is marked failed and the error is logged. You do NOT need try/catch around SDK methods. Use try/catch only if a method is expected to fail.

### Amounts and Units

* EVM and Solana transaction, swap, and allocation amounts are **strings in smallest units** (wei/token base units or lamports). Keep them as strings to preserve precision.
* **Never size an amount through float math** (`parseFloat`, `Math.round(usd * 1e6)`, `Number(amountRaw) / 10 ** decimals`). Use the SDK's exact converter pair `decimalToBaseUnits(decimal, decimals)` / `baseUnitsToDecimal(raw, decimals)` (`decimal_to_base_units` / `base_units_to_decimal` in Python) and `bigint`/`int` arithmetic on raw units. Spend-all is the allocation entry's `amountRaw`, verbatim. The engine rejects an agent swap/transfer whose amount exceeds the session's attributed inventory.
* Hyperliquid order sizes and prices are formatted **numbers**, while allocation fields remain raw string amounts. Its unified USDC collateral uses 8 decimals in Circuit.
* Kraken order volume, price, balances, and fills are formatted **decimal strings** in venue units, never wei/lamports.
* Polymarket order amounts are unit-explicit **numbers**: buys take `spendUsd`; sells take `shares`.
* **Minimum order size**: $10 notional on Hyperliquid (applies to both opens *and* closes - a position whose mark value has decayed below $10 cannot be closed with a reduce-only order, so unwind logic must detect and skip it). Circuit pre-checks this before submit - in dry runs and live alike - so a sub-$10 order fails immediately with a descriptive error instead of a live venue rejection. $50+ on Ethereum L1 and \$10+ on L2s to cover gas and avoid dust issues on swaps/transfers.
* Native gas tokens (ETH/SOL) must already be in the wallet for transactions to succeed

### Logging

```typescript theme={null}
// Standard log (visible to user in Circuit UI)
await agent.log("Processing transaction");

// Error log
await agent.log("Failed to load settings", { error: true });

// Debug log (console only, NOT sent to user)
await agent.log("Internal state", { debug: true });

// Log objects only with debug flag
await agent.log({ key: "value" }, { debug: true });
```

Python equivalent: `agent.log("msg")`, `agent.log("msg", error=True)`, `agent.log("msg", debug=True)`

**Best practice**: User-facing logs should be clean, concise strings. Use `debug=True` for debug/object logging. See checklist rule #10 for what makes a good log.

### Positions

**`agent.allocation`** is a field in both TypeScript and Python (no `await`, no `success`/`data` wrapper), captured at invocation start. It is a **frozen snapshot**: a top-up or transaction does not mutate the active context. The next trigger constructs a fresh context from the committed allocation. Track effects in local variables when later steps in the same invocation depend on earlier actions; never mirror holdings in `agent.memory` across invocations.

* `allocation.balances` - fungible inventory the session holds outright: cash, spot tokens, staking, Kraken synthetic cash and base inventory.
* `allocation.positions` - open market exposures: Hyperliquid perps (signed sizes) and Polymarket outcome tokens (shares).

```typescript theme={null}
const allocation = agent.allocation;
for (const bal of allocation.balances) {
  await agent.log(`${bal.tokenAddress}: ${bal.amountRaw}`);
}
for (const pos of allocation.positions) {
  await agent.log(`Perp ${pos.coin}: size ${pos.size}`);
}
```

### Swap

Two-step workflow: quote then execute.

```typescript theme={null}
// 1. Get quote
const quote = await agent.swap.quote({
  from: { network: "ethereum:137", address: agent.sessionWalletAddress },
  to: { network: "ethereum:137", address: agent.sessionWalletAddress },
  amount: "1000000",  // 1 USDC (6 decimals), in raw units
  fromToken: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",  // USDC.e (bridged USDC on Polygon)
  toToken: "0xd93f7e271cb87c23aaa73edc008a79646d1f9912",    // wSOL on Polygon
  slippage: "10.0"
});

// 2. Execute
if (quote.success && quote.data) {
  const result = await agent.swap.execute(quote.data);
  if (result.success && result.data?.status === "success") {
    await agent.log("Swap completed!");
  }
}
```

**Key notes:**

* Omit `fromToken`/`toToken` for native tokens (ETH, SOL)
* Same network = swap; different networks = bridge
* Default slippage: 0.5%; use 1-2% for volatile/cross-chain
* Minimum \$10-20 recommended to avoid fee issues
* Bulk execution: pass array of quotes to `execute()`
* Execution status is final: `"success"`, `"failure"`, `"refund"`, `"delayed"`, or `"error"`
* In manual mode, `execute()` returns a suggestion instead of executing - use `isSuggestedTransaction()` to detect

### Custom Transactions / Signing

Transactional request shapes that expose `expiresAt` (including `signAndSend`, swap quotes passed to `swap.execute`, and venue order methods) use it only to control suggestion expiry in manual mode.

It is recommended to use viem or web3py to populate calldata and other raw transaction data, rather than doing raw low-level transformations in the script.

**Sign and send (EVM):**

```typescript theme={null}
const response = await agent.signAndSend({
  network: "ethereum:1",
  request: {
    toAddress: "0x...",
    data: "0x...",     // calldata, use "0x" for simple transfers
    value: "100000000000000",  // wei
  },
  message: "Description for UI",
  expiresAt: "2030-01-01T00:00:00Z" // optional, manual mode only
});
```

Use the `EMPTY_DATA` constant (`"0x"`) for simple ETH transfers with no contract interaction:

```typescript theme={null}
import { EMPTY_DATA } from "circuit:sdk";

await agent.signAndSend({
  network: "ethereum:8453",
  request: { toAddress: recipient, data: EMPTY_DATA, value: "1000000000000000000" },
  message: "Send 1 ETH",
});
```

**Sign and send (Solana):**

```typescript theme={null}
const response = await agent.signAndSend({
  network: "solana",
  request: {
    hexTransaction: "0x010001030a0b..."  // Serialized VersionedTransaction as 0x-prefixed hex
  }
});
```

**Sign message (EVM only):** `agent.signMessage({ network, request: { messageType: "eip191" | "eip712", data, chainId } })` - returns `data.formattedSignature`.

**Python equivalents:** `agent.sign_and_send()`, `agent.sign_message()` with snake\_case field names in dicts (`to_address`, `hex_transaction`, `message_type`, `chain_id`, etc.); `sign_message` returns `data.formatted_signature`. The Python SDK also supports optional advanced EVM fields: `gas`, `max_fee_per_gas`, `max_priority_fee_per_gas`, `nonce`, `enforce_transaction_success`.

### Transaction History

```typescript theme={null}
const result = await agent.transactions();
if (result.success && result.data) {
  for (const tx of result.data) {
    await agent.log(`${tx.tokenType} ${tx.amount} on ${tx.network} - ${tx.transactionHash}`);
  }
}
```

Each record includes: `network`, `transactionHash`, `fromAddress`, `toAddress`, `amount`, `tokenAddress`, `tokenId` (nullable), `tokenType`, `tokenUsdPrice`, `timestamp` (TypeScript spellings; Python attributes are snake\_case - `transaction_hash`, `from_address`, `to_address`, `token_address`, `token_id`, `token_type`, `token_usd_price`). Note: indexing has a delay per chain.

### Hyperliquid Integration

**Configuration in `circuit.toml`:**

```toml theme={null}
[startingAsset]
network = "hypercore:perp"
address = "USDC"
minimumAmount = "100000000"  # 1 USDC (8 decimals)
```

**Key differences for Hyperliquid agents:**

* **Unified account - one USDC balance, no spot/perp split.** Every Circuit Hyperliquid account runs in Hyperliquid's unified account mode (Circuit enforces this automatically): a single USDC balance collateralizes spot orders, perp positions, and builder-DEX markets alike. There is no separate "spot balance" or "perp balance" and no transfer between them - never try to "move funds to perp" before trading; just place the order. "Spot" vs "perp" only distinguishes *markets* (the order's `market` field and coin format), not where cash lives
* A session is allocated a slice of the wallet's Hyperliquid USDC collateral (not the whole wallet); multiple Hyperliquid agents can share one wallet, and they share account-level liquidation risk
* Read this session's invocation-start collateral and open perps from `agent.allocation` (`balances` carries `hyperliquidMetadata.collateral`; `positions` carries `hyperliquidMetadata` - leverage / liquidation price / margin used). Sizing from this session allocation - never a whole-wallet read - is what keeps a shared wallet's other sessions safe
* The collateral balance is **total** collateral - it includes margin already locked in open positions and excludes unrealized PnL. For session collateral (kill-switch / drawdown math), ADD each open position's `unrealizedPnlUsd` - a losing book's negative PnL must pull session collateral down, or the rail fires late. For free margin to size new orders, additionally subtract each position's `hyperliquidMetadata.marginUsed`. Never subtract `marginUsed` from session collateral - that spuriously fires on healthy leveraged positions
* **Avoid calling `midpointPrice` in tight loops** - Hyperliquid will rate limit
* All values are in formatted amounts (not raw units)
* Must respect Hyperliquid's tick and lot size rules when placing orders
* \*\*$10 notional minimum** on every order, including closes. Before sizing a trade, compute `size * markPrice` and skip if below $10. On unwind, a losing position can decay below the minimum - skip rather than submit a guaranteed-failing order, and never round the size above the session's attributed position. Circuit enforces this pre-submit (dry run and live), so a sub-\$10 order fails loudly with the arithmetic in the error instead of reaching the venue.

**Place order:** Fetch the mid price first (see "Realtime price lookups"), then derive the market order's slippage limit from it. Circuit supports immediate non-post-only market orders only; limit, post-only, stop, take-profit, and omitted-type GTC orders are rejected on every surface.

> **`coin` is a literal ticker string you write - `"ETH"`, `"BTC"`, `"xyz:GOLD"` - not a value you derive at runtime.** Only `price` is fetched (it is market data); the asset you trade is fixed by the strategy, so hardcode it. Do NOT read `coin` from an API response, a `mids` lookup, an allocation position entry, or any variable - that's how it ends up `undefined` and the order is rejected with `coin: Invalid input: expected string, received undefined`. The field is `coin`, never `symbol`.

```typescript theme={null}
const mid = await getHyperliquidMid("BTC");         // helper from "Realtime price lookups"
const order = await agent.platforms.hyperliquid.placeOrder({
  coin: "BTC",            // LITERAL ticker - write it directly, never derive it. Perps: "BTC". Spot: the live pair name, e.g. "UBTC/USDC" - NOT "BTC/USDC" (see "Spot orders" below).
  side: "buy",
  size: 0.0001,
  price: mid * 1.01,      // Slippage limit for market orders - 1% above mid for a buy
  market: "perp",         // "perp" or "spot"
  type: "market",         // required - resting and trigger orders are not supported for agents
  reduceOnly: true,       // optional: only reduce the session's existing position
  postOnly: false,        // optional: must be false or omitted for agent orders
  message: "z=3.3 24h=+17%", // optional (max 250 chars): Activity-feed caption for this trade; omit to let Circuit synthesize one from recent logs
  expiresAt: "2030-01-01T00:00:00Z" // optional, manual mode only
});
```

**Spot orders (`market: "spot"`): the `coin` is the live spot listing name, which is NOT the perp ticker.** Hyperliquid spot pairs are named after their listed tokens, and the bridged majors are Unit-bridged assets with a `U` prefix: `UBTC/USDC`, `UETH/USDC`, `USOL/USDC`, `UPUMP/USDC`, `UFART/USDC`. Only Hyperliquid-native tokens use their plain ticker (`HYPE/USDC`, `PURR/USDC`). **`BTC/USDC`, `ETH/USDC`, and `SOL/USDC` do not exist** - they look right, compile, and pass `circuit check`, then every order is rejected at run time with `Unknown spot pair`. Before hardcoding any spot pair (Rule 1), confirm the exact name from spot metadata - `POST https://api.hyperliquid.xyz/info` with `{"type":"spotMeta"}`, where each `universe` entry's pair name is `tokens[base].name + "/" + tokens[quote].name` (the error message also lists all valid pairs). Spot mids come from `getHyperliquidSpotMid` in "Realtime price lookups" (`allMids` keys spot pairs as `@<index>`, so the perp helper can't find them). Spot and perp orders draw from the same unified USDC pool - do not transfer between account modes; place the order directly. The \$10 minimum order notional applies to spot orders too.

**Builder DEX perps (subdexes):** Hyperliquid hosts third-party perpetual markets via HIP-3 builder DEXes. Trade them by prefixing the symbol with the builder code. The recommended builder DEX is **Trade\[XYZ]** (`xyz`) - it's the largest by volume, has the deepest liquidity, is USDC-settled, and holds an official S\&P 500 license. Available assets include precious metals (GOLD, SILVER, PLATINUM), equity indices (SP500), stocks (TSLA, AAPL, AMZN), and energy (NATGAS).

```typescript theme={null}
// Trade gold perps on Trade[XYZ]
const goldMid = await getHyperliquidMid("xyz:GOLD"); // same helper, same symbol format
const order = await agent.platforms.hyperliquid.placeOrder({
  coin: "xyz:GOLD",
  side: "buy", size: 1, price: goldMid * 1.01,
  market: "perp", type: "market",
});

// Your open perps (all venues) are in (agent.allocation).positions.
```

For standard crypto assets (BTC, ETH, SOL, etc.), use Hyperliquid's native perps - they have the deepest liquidity and tightest spreads. Use `xyz:` prefixed symbols for real-world assets not listed on the native venue.

**Other methods** (TypeScript / Python):

* `deleteOrder(orderId, coin)` / `delete_order(order_id, coin)` - Cancel order
* `midpointPrice(coin, dex?)` / `midpoint_price(coin, dex=None)` - Read one or many indexed mids; use `"spot"` or a builder-DEX name for non-default markets

### Polymarket Integration

**Grandfathered wallets only.** Polymarket rejects orders from wallets with no prior Polymarket trading history - starting a Polymarket agent on any other wallet is blocked with a clear error, and an order from one fails with `WALLET_NOT_GRANDFATHERED`. There is no way to enroll a new wallet through Circuit.

**Methods** (`agent.platforms.polymarket`):

* `searchEvents(query)` / `search_events(query)` - resolution step 1: discover events by name
* `eventMarkets(slug)` / `event_markets(slug)` - resolution step 2: one event's COMPLETE market catalog
* `marketOrder({ tokenId, side, spendUsd | shares })` / `market_order({...})` - amounts are unit-explicit: **BUY takes `spendUsd`** (USD to spend, e.g. `20` = \$20 of shares); **SELL takes `shares`** (shares to sell). The mismatched field fails validation.
* `redeemPositions({ tokenIds })` / `redeem_positions({...})` - redeem settled positions (only ones your allocation marks `isRedeemable`)

Orders take a `tokenId` - a \~77-digit decimal string identifying ONE outcome of ONE market. You must resolve it from the user's intent; never invent or truncate one.

#### Resolving a market: intent → tokenId

Every trade starts by turning a phrase like "Egypt to win today's World Cup match" into a CLOB `tokenId`. Resolution is **two SDK calls** - the platform owns the venue plumbing (proxying, response decoding, merging Polymarket's split catalogs); your job is interpreting the results:

1. **Discover** with `searchEvents(query)`. Query by the **specific names** involved - teams, people, tickers ("egypt", "australia egypt") - never competition or category words ("world cup" ranks generic futures above today's match). Returns `{ events, moreEvents }` in relevance order: `events` carries the top matches with the markets search exposes; `moreEvents` is lower-ranked `{ slug, title, startDate }` refs - **check it when the wanted event isn't in `events`** (a match is often outranked by unrelated popular markets).
2. **Enumerate** with `eventMarkets(slug)`. Search results NEVER show an event's full catalog - totals/over-under, spreads, and props are missing from search even when they exist (and are often the event's most liquid markets). `eventMarkets` returns the complete liquidity-sorted catalog and is the ONLY authority on which markets exist: never conclude "no such market" from search results alone. Every market row carries `question`, `sportsMarketType`, `groupItemTitle`, `line`, `gameStartTime`, liquidity/volume, and each outcome's `name`, `priceUsd`, and the `tokenId` orders take.

Rules for interpreting the results:

1. **Search never returns empty** - nonsense queries fuzzy-match unrelated events. Verify a returned `title`/`question` actually names what the user asked for; if nothing does, report "no matching market" instead of trading the closest miss.
2. **Trade only the most liquid market that matches the intent.** Markets come back liquidity-sorted; skip thin books rather than filling the user's order badly. For a generic sports bet ("bet on Egypt") that is the match's moneyline.
3. **Sports model**: one match = one event (e.g. slug `fifwc-aus-egy-2026-07-03`, title "Australia vs. Egypt"). A soccer moneyline is **one Yes/No market per side** - `groupItemTitle` names the side, so "bet on Egypt" = the market with `groupItemTitle: "Egypt"`, outcome `"Yes"`. Totals are one market per line with Over/Under outcomes - "over 0.5 goals" = the `sportsMarketType: "totals"`, `line: 0.5` market, outcome `"Over"`. Match markets carry `gameStartTime` like `"2026-07-03 18:00:00+00"` (space-separated, not ISO `T`) - use it for "today".

#### Worked example: "bet \$20 on Egypt in the World Cup today"

```typescript theme={null}
const today = new Date().toISOString().slice(0, 10);

// 1) Discover by participant name
const search = await agent.platforms.polymarket.searchEvents("egypt");
const ref = [...(search.data?.events ?? []), ...(search.data?.moreEvents ?? [])]
  .find((e) => e.title.includes("Egypt"));
if (!ref) throw new Error("No Egypt match today on Polymarket");

// 2) Enumerate the full catalog (the only authority on which markets exist)
const catalog = await agent.platforms.polymarket.eventMarkets(ref.slug);

// 3) Today's Egypt side of the moneyline
const market = catalog.data?.markets.find((m) =>
  m.sportsMarketType === "moneyline" && m.groupItemTitle === "Egypt" &&
  (m.gameStartTime ?? "").startsWith(today));
const yes = market?.outcomes.find((o) => o.name === "Yes");
if (!yes) throw new Error("No Egypt moneyline today");
await agent.log(`${market.question} - Yes @ $${yes.priceUsd} (liquidity $${market.liquidityUsd})`);

// 4) BUY: spendUsd is the USD to spend
const order = await agent.platforms.polymarket.marketOrder({
  tokenId: yes.tokenId, spendUsd: 20, side: "BUY",
});
```

The same chain in Python: `sdk.platforms.polymarket.search_events("egypt")`, `sdk.platforms.polymarket.event_markets(ref["slug"])`, then `sdk.platforms.polymarket.market_order({"tokenId": token_id, "spendUsd": 20, "side": "BUY"})` (the Python reads return the wire envelope as dicts: `result["data"]["events"]`, `event["data"]["markets"]`).

**Ambiguity is a stop, not a guess.** If several events plausibly match (two Egypt matches today, multiple "Fed rate" markets), pick only when one candidate clearly wins on participant + date + liquidity; otherwise log the candidates and skip the trade.

### Kraken Integration

Kraken agents trade spot on the **user's own Kraken account** via an API credential the user connects in Settings (or `circuit kraken connect`). Declare the requirement in `circuit.toml` - this is what makes the credential picker and allocation slider appear in the start flow:

```toml theme={null}
[startingAsset]
network = "ethereum:8453"
address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
minimumAmount = "0"   # exchange-only start: the venue allocation UI renders only when no on-chain minimum is required

[exchangeCredentials.kraken]
read = true
trade = true                 # required for placing orders; read-only agents omit it
minimumAllocationUsd = 40    # smallest USD slice a user may allocate
```

**Key rules for Kraken agents:**

* **The credential is handed to you - never a setting.** At start the user picks a credential and a USD allocation; the runtime exposes the selection as `agent.credentials.kraken` (a ready-made credential ref). Pass it as `credentialRef` on every Kraken call. If it's missing, fail loud - don't fall back.
* **Virtual allocation partitions both cash and inventory.** A new buy reserves `volume × limit price` against the session's USD slice, which is why orders are limit-only. Once terminal, a buy uses its realized cost; filled sells recycle realized proceeds. A session may sell only base inventory acquired by its own filled buys, less filled sells and open sell reservations; that inventory may be sold through either enabled quote pair. Orders from another session never establish ownership, so multiple sessions can safely share one credential.
* **Size sells from base inventory in the allocation.** Each positive Kraken base balance carries the exact `krakenMetadata.availableBaseVolume` the gate computed at run start. Match the order's base asset to `tokenAddress`, use that decimal volume, and return proceeds through an enabled USD or USDC pair; never use the credential-wide balance. No entry means the session has zero sellable inventory for that base.
* **The USD allocation entry is available cash at invocation start.** `kraken:allocation:USD` is the original session slice minus filled net deployment and accepted orders' unfilled limit notional. Circuit recomputes authority before every order, so an earlier action in this invocation can make the gate stricter than the snapshot; track dependent actions locally or wait for the next trigger's fresh context.
* **Free USD-priced account value backs a new allocation.** Circuit values free USD, USDC, USDT, ETH, XBT, SOL, and other priceable holdings into the synthetic `kraken:allocation:USD` virtual allocation. This does not convert assets or guarantee quote liquidity: inspect whole-account balances, choose an enabled quote for each order, and remember the live order gate requires enough free balance in that concrete quote.
* **Pair allowlist and minimums.** Pairs use the namespaced slashed form: `KRAKEN:XBT/USD`, `KRAKEN:XBT/USDC`, `KRAKEN:ETH/USD`, `KRAKEN:ETH/USDC`, `KRAKEN:SOL/USD`, `KRAKEN:SOL/USDC`. Note Bitcoin is `XBT`, not BTC. Sell minimums: XBT 0.0001, ETH 0.01, SOL 0.1. Fee-safe buy minimums: XBT 0.000102, ETH 0.0102, SOL 0.102.
* **Adapt the quote to what the account holds.** Accounts hold USD or USDC (or neither) - read balances first and trade the dollar leg that's actually funded; don't hardcode `/USD`.
* **Balance codes arrive namespaced and legacy-prefixed** (`KRAKEN:XETH`, `KRAKEN:ZUSD`, `KRAKEN:XXBT`): strip the `KRAKEN:` namespace and Kraken's legacy X/Z class prefix before comparing symbols.
* **For immediate fills, use a marketable limit**: price a buy slightly above the ask (sell slightly below the bid). The order fills as a taker but keeps a bounded worst-case execution price; for buys, that price also gives the allocation gate a maximum notional to reserve.
* `ticker` needs no credential; `balances`, `placeOrder`, `orderStatus`, `openOrders`, `cancelOrder` need `credentialRef`.

```typescript theme={null}
const credentialRef = agent.credentials.kraken;
if (!credentialRef) throw new Error("Kraken credential missing from this session");

const ticker = await agent.platforms.kraken.ticker({ pair: "KRAKEN:XBT/USDC" });
if (!ticker.success || !ticker.data) throw new Error(ticker.error ?? "ticker failed");
const ask = Number(ticker.data.ask);

const order = await agent.platforms.kraken.placeOrder({
  credentialRef,
  validate: false,
  order: {
    pair: "KRAKEN:XBT/USDC",   // LITERAL allowlisted pair - XBT, not BTC
    side: "buy",
    type: "limit",             // limit is the ONLY allowed type
    volume: "0.0001",          // string; respect the pair minimum
    price: (ask * 1.002).toFixed(2), // marketable limit: crosses the spread, meterable notional
  },
});
```

**Unwind:** retain the order ids returned by this session's `placeOrder` calls. Use `orderStatus`/`openOrders` to check them, cancel only those ids (never every order on a shared pair), and derive filled volume from their status before submitting marketable-limit sells on the original buy pair. Persisting this session's order ids is safe, but never store balances/quantities or treat memory or whole-account balances as ownership authority. Circuit independently derives and enforces sellable inventory from durable session order history.

## Configuration

### The plan: `DESCRIPTION.md`

`DESCRIPTION.md` (project root) is the agent's plan - the short prose a user reads to decide whether to deploy, rendered as a card on the agent's page in Circuit. It is the source of truth for the strategy; the code implements it. Optimize for fast comprehension (a user grasps the whole agent in \~15s): plain, active, present tense, concrete, real asset/protocol names. No filler, hedging, disclaimers, or marketing.

Write **exactly these five `##` sections**, within these budgets:

* `## Summary` - **1-2 sentences, ≤45 words**: what it does and why it works. No preamble.
* `## What it is` - header chips: `**Label:** value` lines, one per line. Each value an **ultra-short noun phrase** (aim ≤24 chars, Title Case; hard limit 80) - the shortest recognizable form ("Gold", "NVDA", "Aave V3", "8-12% APY"), never a sentence. **Plain names only - no tickers, symbols, contract addresses, or parentheticals** (write `Gold`, not `Gold (XAUT)` or `Gold (0x…)`). Use **exactly** the labels for your `circuit.toml` `category` - `circuit check` validates the rows against the category and reports exactly what's missing or mismatched:
  * **spread** - `**Long:** <asset longed>`, `**Short:** <asset shorted>` - the bare asset name only (`Gold`, `Silver`, `NVDA`, `Oil`).
  * **yield** - `**Source:** <where the yield comes from>`, `**Strategy:** <how it captures it>`, `**Target:** <the return it aims for>`, `**Destination:** <protocol(s), bare names joined " & ">` (name one whenever there is one)
  * **quant** - `**Model:** <the strategy/model>`, `**Trigger:** <the signal that makes it act>`, `**Execution:** <what it does when triggered>`
  * **index** - reuse the quant labels: `**Model:** <the strategy/model>`, `**Trigger:** <the signal that makes it act>`, `**Execution:** <what it does when triggered>`.
  * **experimental** - `**Signal:** <the market read>`, `**Action:** <what it does>`, `**Reason:** <the edge behind it>`.
* `## How it works` - the elevator pitch: a markdown list, **up to 3 lines, one line each (≤14 words)**. Verb-first, subject dropped ("It", "The agent"), plain enough for anyone - the *gist*, not the mechanics (e.g. `- Scans the AI-token universe hourly for momentum.`).
* `## Strategy` - the concrete rails: a markdown list, **3-5 lines, one line each (≤16 words)**. Named assets/markets/protocols, when it enters / rebalances / exits, and sizing (e.g. `- Long GOLD, short BTC at equal notional`; `- Rotate USDC to the top APY across Moonwell & Aave`).
* `## Risks` - a markdown list, **up to 3 lines, one line each**: each a concrete failure mode paired with the rail that bounds it (drawdown kill-switch %, per-trade cap, max drift for spreads, max leverage for perps). No generic disclaimers.

**How it works = what it does in plain words; Strategy = the exact rails (names, triggers, sizing) - never repeat one in the other.** `circuit check` enforces the structure (five sections, category-matched rows); re-read the plan yourself for what it can't check - no run-on or filler lines.

### The `circuit.toml` Configuration File

This is critical - it defines your agent's metadata, asset requirements, and behavior:

```toml theme={null}
name = "My Agent"                        # max 32 characters
tagline = "Short subtitle for cards"     # max 32 characters
category = "experimental"
imageUrl = "https://api.circuit.org/assets/agents/default"
walletType = "ethereum"  # or "solana"
allowedExecutionModes = ["auto", "manual"]
filesToExclude = []

[[triggers]]
type = "schedule"
every = 15
align = "rolling"

[startingAsset]
network = "ethereum:1"
address = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
minimumAmount = "10000000000000000"
```

**Key fields:**

* `name` - Display name shown in the Circuit UI (32 characters or less). **Cannot change after first upload.**
* `tagline` - Brief subtitle shown on agent cards (32 characters or less)
* `category` - Catalog category for the explore page. One of `"spread"`, `"index"`, `"prediction"`, `"quant"`, `"yield"`, `"experimental"`. Defaults to `"experimental"`.
* `imageUrl` - URL for the agent icon displayed in the Circuit UI
* `walletType` - The wallet type for this agent: `"ethereum"` or `"solana"`
* `allowedExecutionModes` - `"auto"` (transactions execute immediately) and/or `"manual"` (user must approve in UI). First entry is used for `circuit run` when `--mode` is omitted.
* `[[triggers]]` - The dispatch configuration: exactly one `schedule` trigger (`every` minutes, `align` rolling/fixed) plus optional reactive triggers (for example price moves)
* `filesToExclude` - Exclude files from the upload bundle
* `startingAsset` - The asset a user must have to start a session; `minimumAmount` in raw units (wei/lamports)
* `[exchangeCredentials.kraken]` - Declares that the agent trades the user's Kraken account (see "Kraken Integration"); `trade = true` plus `minimumAllocationUsd` drive the start flow's credential picker and USD allocation slider

### Settings (User-Configurable Parameters)

Settings let you define parameters users can customize when starting a session. Each setting is a TOML table under `[settings.X]`.

**Types and their runtime values:**

| Setting Type    | Runtime Value | Default Type              | Notes                             |
| --------------- | ------------- | ------------------------- | --------------------------------- |
| `text`          | `string`      | string (max 1000 chars)   | Free-form text                    |
| `boolean`       | `boolean`     | `true` or `false`         | Toggle                            |
| `single_select` | `string`      | string matching an option | Requires `options` array (max 20) |
| `integer`       | `number`      | whole number              | No decimals                       |
| `number`        | `number`      | any number                | Decimals allowed                  |
| `percentage`    | `number`      | number 0-100              | Validated range                   |
| `address`       | `string`      | wallet address            | Validated against `walletType`    |

**Example:**

```toml theme={null}
[settings.risk_level]
description = "How aggressively the agent trades"
type = "single_select"
default = "medium"
required = false
options = ["low", "medium", "high"]

[settings.slippage_tolerance]
description = "Maximum slippage tolerance"
type = "percentage"
default = 0.5
required = false

[settings.buy_amount_usd]
description = "USD amount per buy order"
type = "number"
default = 50.0
required = false

[settings.api_key]
type = "text"
required = true
description = "Your API key for the external service"
```

**Rules:** Max 20 settings per agent. Required settings (`required = true`) have no default - users must provide a value before the agent runs. Access at runtime via `agent.settings`:

```typescript theme={null}
const risk = agent.settings.risk_level;       // "low" | "medium" | "high"
const slippage = agent.settings.slippage_tolerance; // 0.5
const apiKey = agent.settings.api_key;        // always present (required)
```

Test locally with overrides: `circuit run --setting risk_level=high --setting buy_amount_usd=100`

### Network Identifiers

Used throughout the SDK (`agent.swap`, `agent.signAndSend`, allocation balances, …):

* **EVM**: `"ethereum:{chainId}"` - the 23 chains in the table below are the complete supported set. Circuit cannot execute on an EVM chain that is not listed here.
* **Solana**: `"solana"`
* **Hyperliquid**: `"hypercore:perp"` (perps) and `"hypercore:spot"` (spot), including builder-DEX markets - see the Hyperliquid section.

**Supported EVM chains:**

| Chain           | Network ID         |
| --------------- | ------------------ |
| Ethereum        | `ethereum:1`       |
| Arbitrum        | `ethereum:42161`   |
| Polygon         | `ethereum:137`     |
| Optimism        | `ethereum:10`      |
| Base            | `ethereum:8453`    |
| ZKsync Era      | `ethereum:324`     |
| Zora            | `ethereum:7777777` |
| BNB Smart Chain | `ethereum:56`      |
| Avalanche       | `ethereum:43114`   |
| World Chain     | `ethereum:480`     |
| Blast           | `ethereum:81457`   |
| Celo            | `ethereum:42220`   |
| Abstract        | `ethereum:2741`    |
| HyperEVM        | `ethereum:999`     |
| Robinhood Chain | `ethereum:4663`    |

**Native token addresses:**

* EVM chains: `"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"` (EIP-7528)
* Solana: `"11111111111111111111111111111111"` (System Program)

### Key Token Addresses

Commonly used tokens by chain. Use these rather than guessing addresses - incorrect addresses will cause transactions to fail.

| Token | Mainnet (1)                                  | Arbitrum (42161)                             | Base (8453)                                  | Polygon (137)                                | Optimism (10)                                |
| ----- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| USDC  | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` |
| USDT  | `0xdAC17F958D2ee523a2206206994597C13D831ec7` | `0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9` | -                                            | `0xc2132D05D31c914a87C6611C10748AEb04B58e8F` | `0x94b008aA00579c1307B0EF2c499aD98a8ce58e58` |
| WETH  | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1` | `0x4200000000000000000000000000000000000006` | `0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619` | `0x4200000000000000000000000000000000000006` |
| DAI   | `0x6B175474E89094C44Da98b954EedeAC495271d0F` | `0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1` | `0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb` | `0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063` | `0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1` |

**Common DeFi protocols (Base):**

* **Aave V3 Pool**: `0xA238Dd80C259a72e81d7e4664a9801593F98d1c5`
* **Uniswap V3 Router**: `0x2626664c2603336E57B271c5C0b26F421741e481`

**Common DeFi protocols (Arbitrum):**

* **Aave V3 Pool**: `0x794a61358D6845594F94dc1DB02A252b5b4814aD`
* **Uniswap V3 Router**: `0xE592427A0AEce92De3Edee1F18E0157C05861564`

**Token decimals:** USDC/USDT = 6, WETH/DAI = 18, native ETH = 18, SOL = 9, Hyperliquid USDC = 8. Always use correct decimals when computing `minimumAmount` or constructing transaction amounts.

### CLI Commands Reference

| Command                           | Description                                                                  |
| --------------------------------- | ---------------------------------------------------------------------------- |
| `circuit auth login`              | Authenticate (opens browser)                                                 |
| `circuit auth logout`             | Log out                                                                      |
| `circuit auth whoami`             | Show current user                                                            |
| `circuit auth token`              | Print your bearer token for CI capture                                       |
| `circuit kraken connect`          | Connect a Kraken API credential for uploaded agents                          |
| `circuit kraken list`             | List connected Kraken credentials                                            |
| `circuit new`                     | Create new agent project                                                     |
| `circuit run`                     | Test agent locally; `--hosted engine` to run against Circuit's hosted engine |
| `circuit unwind`                  | Unwind agent locally; `--hosted engine` to unwind a hosted session           |
| `circuit check`                   | Validate agent project offline (no auth required)                            |
| `circuit upload`                  | Upload to Circuit infrastructure                                             |
| `circuit wallet list`             | List wallets in the local encrypted vault                                    |
| `circuit wallet add`              | Add a local wallet (imported or generated)                                   |
| `circuit wallet delete <address>` | Remove a wallet from the vault                                               |
| `circuit wallet export <address>` | Decrypt and print a local key (audited)                                      |

**Global flags (available on all commands):**

* `--json` - Output machine-readable JSON: a single document for one-shot commands; an NDJSON event stream for `run`/`unwind`
* `--env <name>` - Target deployment: `production` (default), `staging`, or `local`. Takes precedence over `CIRCUIT_ENV`.
* `--help` - Show help
* `--version` - Show CLI version

The CLI is long-flag only - there are no single-character aliases (not even `-h`/`-v`).

`--path` (agent project directory) is on the project commands - `new`, `check`, `run`, `unwind`, `upload`. `--var KEY=VALUE` (inject an environment variable into the agent, repeatable) is on `upload`, `run`, `unwind`. Note: `--var` injects an agent env var; `--env` selects the deployment - they are different axes.

**`circuit new`:** `--language`, `--name`, `--template` (`basic`, `yield`, `index`, `hyperliquid`), `--path` (output directory).

**`circuit run`:** `--hosted engine` (route through Circuit's hosted engine), `--upload` (upload and run in Circuit's sandbox), `--dry-run` (journal writes instead of executing - no wallet/funding needed; requests never manufacture observed allocation state), `--wallet`, `--keystore`, `--rpc`, `--mode` (`auto`/`manual`), `--amount` (hosted targets only; initial token amount in smallest unit), `--setting` (`KEY=VALUE`, repeatable), `--kraken-credential` (connected credential id), `--kraken-allocation` (session USD slice; use with the credential).

**`circuit unwind`:** `--hosted engine` (route through hosted sessions for unwind), `--upload` (unwind in Circuit's sandbox), `--dry-run` (journal `unwind()`'s writes with an empty allocation; earlier dry-run requests never become observed positions), `--wallet`, `--keystore`, `--rpc`.

**`circuit auth token`:** default prints the bare bearer token (capture into `CIRCUIT_TOKEN`); `--json` wraps it as `{ token }`; `--decode` prints the decoded pre-b64 permit (payload + proof) instead. `circuit auth whoami` (no command-specific flags) shows what the token grants.

**`circuit kraken connect`:** prompts for a credential label, Kraken API key, and Kraken private key, then seals the secret server-side. For headless use, set `KRAKEN_CREDENTIAL_LABEL`, `KRAKEN_API_KEY`, and `KRAKEN_API_SECRET`. Read access requires Kraken's Funds → Query, Orders and trades → Query open orders & trades, and Query closed orders & trades permissions; the watcher reads complete order/fill history so every observed venue event is attributed once. Trading also requires Create & modify orders and Cancel & close orders. Keep Withdraw off. Add `--read-only` to connect without trade permission. `circuit kraken list` prints the ids users select as the credential when starting an agent that declares `[exchangeCredentials.kraken]` (the runtime exposes the selection as `agent.credentials.kraken`). Deleting a credential is web-only (Settings → Kraken); it is blocked while an active session or unreleased allocation still uses it.

**`circuit wallet *`:** `--keystore <path>` (override the keystore file location; default `~/.circuit/local/keystore`).

**`circuit upload`** and **`circuit check`:** No command-specific flags (use global flags only).

***

## Patterns & Troubleshooting

### Memory (Session-Scoped Key-Value Storage)

Keys are auto-namespaced by agent and session. Memory persists across execution cycles within the same session, cleared when the session ends. **Values must be strings** - serialize JSON/numbers before storing.

```typescript theme={null}
// Set
await agent.memory.set("lastPrice", "45000.50");

// Get (includes `updatedAt` unix timestamp of when the value was last written)
const result = await agent.memory.get("lastPrice");
// A miss returns success:true with value:null - check value, not success.
if (result.data?.value != null) {
  const price = parseFloat(result.data.value);
}

// Delete
await agent.memory.delete("tempKey");

// Shared memory (accessible across all sessions of this agent, last-write-wins)
await agent.memory.set("globalConfig", "value", { shared: true });
await agent.memory.get("globalConfig", { shared: true });
```

Python: `agent.memory.set("key", "value")`, `agent.memory.get("key")`, `agent.memory.delete("key")`. Add `shared=True` for shared scope.

**When to use memory:**

* Tracking one-time setup actions (e.g., `"aaveApproved": "true"` to skip redundant approvals)
* Persisting computed values across runs (run count, last execution price, cooldown timestamps)
* Storing small config state (last chosen pool, last rebalance time)

**When NOT to use memory:**

* Do NOT store balances or position quantities across invocations - each new context carries `agent.allocation`; Kraken's server-side session order history remains the ownership authority, and its base-inventory projection exposes that authority to the agent (rule #5)
* Do NOT store transaction hashes for tracking - use `transactions()` instead
* Do NOT use memory as a general database - it's key-value only, strings only, and scoped to a session

### External Data Integration

Agents often need external data for decision-making. Use `fetch()` for HTTP APIs and viem/web3py for onchain reads.

**DefiLlama (protocol TVL, yield data):**

```typescript theme={null}
// Get protocol TVL
const res = await fetch("https://api.llama.fi/protocol/aave-v3");
const data = await res.json();
await agent.log(`Aave V3 TVL: $${(data.tvl[data.tvl.length - 1].totalLiquidityUSD / 1e9).toFixed(2)}B`);

// Get yield pools
const pools = await fetch("https://yields.llama.fi/pools");
const { data: poolData } = await pools.json();
const aavePools = poolData.filter((p: any) => p.project === "aave-v3" && p.chain === "Base");
```

**Onchain reads with viem (TypeScript):**

```typescript theme={null}
import { createPublicClient, http } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

// Read ERC-20 balance
const balance = await client.readContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "balanceOf",
  args: [agent.sessionWalletAddress],
});

// Read any contract
const totalSupply = await client.readContract({
  address: USDC,
  abi: parseAbi(["function totalSupply() view returns (uint256)"]),
  functionName: "totalSupply",
});
```

**Onchain reads with web3py (Python):**

```python theme={null}
from web3 import Web3

w3 = Web3(Web3.HTTPProvider("https://mainnet.base.org"))
contract = w3.eth.contract(address=USDC, abi=ERC20_ABI)
balance = contract.functions.balanceOf(agent.session_wallet_address).call()
```

**Tips:**

* Let a failed external read throw - the run fails loud and the next interval re-runs. Do NOT `try/catch` it into a default (`?? 0`, `|| []`); a defaulted read is a fund-losing bug when it feeds a sizing or trade decision.
* DefiLlama endpoints are unauthenticated and free

### Error Handling

Check `result.success` before `result.data`, log failures with `{ error: true }`, and return early - never coerce a failed read into a default (see "SDK Response Pattern"). Let anything unexpected throw: the SDK marks the run failed, surfaces the real error, and the next interval re-runs.

**Common error patterns:**

| Error                          | Cause                           | Solution                                                            |
| ------------------------------ | ------------------------------- | ------------------------------------------------------------------- |
| "Quote failed"                 | Amount too small / no liquidity | Increase amount, adjust slippage                                    |
| "Transaction failed"           | Insufficient balance/gas        | Check `agent.allocation.balances` first                             |
| "Transaction reverted onchain" | Tx was mined but reverted       | Check contract address, input params, token approvals               |
| "Invalid request"              | Bad parameters                  | Validate inputs before SDK calls                                    |
| "No routes found"              | Swap routing failure            | Check token addresses, try higher slippage, verify liquidity exists |

### Multi-Agent Monorepo Support

You can organize multiple agents in a monorepo with shared utility code:

```text theme={null}
my-circuit-agents/
├── agents/
│   ├── agent-1/
│   │   ├── main.py / index.ts
│   │   ├── DESCRIPTION.md
│   │   ├── circuit.toml
│   │   └── pyproject.toml / package.json
│   ├── agent-2/
│   │   └── ...
└── utils/
    ├── __init__.py / index.ts
    └── shared_module/
```

Use a workspace package (bun/uv workspaces) to share code across agents: shared directories are installed by name, one lockfile owns the dependency graph.
