Skip to main content

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, Polymarket, and other blockchains, without ever giving up custody of their private keys.

Agent Requirements Checklist

  1. ABIs MUST be in a separate constants file, most constants SHOULD be in a separate constants file.
  2. Use viem (TypeScript) or web3py (Python) for most onchain tasks such as numerical formatting or calldata encoding or RPC calls - almost everything except signing a transaction.
  3. Use DefiLlama as your primary source of truth for evaluating protocols or high-medium timeframe data. Use direct onchain calls (or helper SDKs such as viem/web3py which do so) for time-sensitive low-latency data.
  4. Logs should be interesting to a human audience, including live stats (APY, TVL, entry price) or new world information, and explain why the agent did what it did.
  5. Focus on clear simple understandable auditable financial logic - they must be trustworthy and reliable since people will run money through them.
  6. positions/currentPositions represents the virtual asset allocation for this agent session — the specific subset of the wallet’s holdings assigned to this session. It is not a raw wallet balance. Use positions as the source of truth for what the agent owns; do not duplicate this state in memory.
  7. Use agent.log() instead of print statements.
  8. Every agent MUST have a working unwind() that actually closes/exits positions — not just a log message. See “Unwind Patterns” below.
  9. NEVER hardcode asset prices that can change — fetch at runtime. See “Realtime price lookups” below for the recipe per venue. Numeric literals in price / triggerPrice fields are a bug, even as placeholders.
  10. Verify API paths and asset tickers and token addresses, never guess.
  11. Make starting assets easy and likely to get - for example, native assets like ETH/SOL or stablecoins like USDC/USDT - never wrapped assets like WETH.

Realtime price lookups

Every placeOrder/triggerPrice value 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
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
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: "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"]). Existing positions: if you already hold the asset, agent.platforms.hyperliquid.positions() returns markPrice for each open position — use it directly, no extra fetch needed. This is the canonical source for unwind. 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:
import { Agent, type AgentContext, type CurrentPosition } from "@circuitorg/agent-sdk";

async function run(agent: AgentContext): Promise<void> {
  await agent.log("Hello from my agent!");
  // Your strategy logic here
}

async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  await agent.log(`Unwind requested for ${positions.length} positions`);
  // Unwind logic for each position — see "Unwind Patterns" below
}

const agent = new Agent({
  runFunction: run,
  unwindFunction: unwind
});

export default agent.getExport();
Python:
from agent_sdk import Agent, AgentContext
from agent_sdk.agent_context import CurrentPosition

def run(agent: AgentContext) -> None:
    agent.log("Hello from my agent!")
    # Your strategy logic here

def unwind(agent: AgentContext, positions: list[CurrentPosition]) -> None:
    agent.log(f"Unwind requested for {len(positions)} positions")
    # Unwind logic for each position — see "Unwind Patterns" below

agent = Agent(run_function=run, unwind_function=unwind)
handler = agent.get_handler()

if __name__ == "__main__":
    agent.run()

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 pattern depends on what the agent does: Swap-based agents (holding non-starting tokens): Swap everything back to the starting asset.
async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  for (const pos of positions) {
    if (pos.assetAddress.toLowerCase() === STARTING_TOKEN.toLowerCase()) continue;
    if (BigInt(pos.currentQty) === 0n) continue;

    const quote = await agent.swap.quote({
      from: { network: NETWORK, address: agent.sessionWalletAddress },
      to: { network: NETWORK, address: agent.sessionWalletAddress },
      amount: pos.currentQty,
      fromToken: pos.assetAddress,
      toToken: STARTING_TOKEN,
      slippage: "3.0",
    });
    if (quote.success && quote.data) {
      const result = await agent.swap.execute(quote.data);
      await agent.log(`Swapped ${pos.assetAddress} back: ${result.success ? "ok" : result.error}`);
    }
  }
}
Protocol/yield agents (deposited into a contract): Withdraw from the protocol, then swap back if needed.
async function unwind(agent: AgentContext, positions: CurrentPosition[]): 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}`);
}
Hyperliquid agents: Close all open positions with market orders. Skip positions whose current notional has decayed below the 10minimumreduceonlyclosescannotroundthesizeup,soasub10 minimum — reduce-only closes cannot round the size up, so a sub-10 position is stuck until its mark recovers or it gets liquidated.
async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  const openPositions = await agent.platforms.hyperliquid.positions();
  if (!openPositions.success || !openPositions.data?.length) {
    await agent.log("No open positions to close");
    return;
  }
  for (const pos of openPositions.data) {
    const size = parseFloat(pos.size);
    const mark = parseFloat(pos.markPrice);
    const notional = size * mark;
    if (notional < 10) {
      await agent.log(`Skipping ${pos.symbol}: notional $${notional.toFixed(2)} below $10 minimum`);
      continue;
    }
    const closeSide = pos.side === "long" ? "sell" : "buy";
    const slippagePrice = closeSide === "sell" ? mark * 0.5 : mark * 1.5;
    const result = await agent.platforms.hyperliquid.placeOrder({
      symbol: pos.symbol, side: closeSide, size,
      price: slippagePrice, market: "perp", type: "market", reduceOnly: true,
    });
    await agent.log(`Closed ${pos.symbol}: ${result.success ? result.data?.status : result.error}`);
  }
}
Polymarket agents: Sell all share positions, then redeem any settled markets.
async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  const livePositions = await agent.getCurrentPositions();
  if (livePositions.success && livePositions.data) {
    for (const pos of livePositions.data.positions) {
      if (!pos.polymarketMetadata || parseFloat(pos.currentQty) === 0) continue;
      if (pos.polymarketMetadata.isRedeemable) {
        await agent.platforms.polymarket.redeemPositions({ tokenIds: [pos.tokenId!] });
        await agent.log(`Redeemed settled market: ${pos.polymarketMetadata.question}`);
      } else {
        await agent.platforms.polymarket.marketOrder({
          tokenId: pos.tokenId!, size: parseFloat(pos.currentQty), side: "SELL",
        });
        await agent.log(`Sold ${pos.currentQty} shares: ${pos.polymarketMetadata.question}`);
      }
    }
  }
  // Final sweep: redeem any remaining settled positions
  await agent.platforms.polymarket.redeemPositions();
}

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):
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):
# -- 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: number instead of the normal execution result. Type guards (TypeScript only): The SDK exports helpers to distinguish manual mode suggestions from executed results:
import { isSuggestedTransaction, isTransactionExecuted } from "@circuitorg/agent-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 if (isTransactionExecuted(result.data)) {
    await agent.log(`Executed: ${result.data.txHash}`);
  }
}
Available type guards: isSuggestedTransaction(response), isSuccessResponse(response), isTransactionExecuted(data), isSwapExecuted(data), isPolymarketExecuted(data), isHyperliquidExecuted(data). 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 runtimeIntervalMinutes in circuit.toml. The interval starts after the previous run completes.

Agent Context (AgentContext)

The AgentContext object is passed to both run and unwind functions. It contains: Session Data:
  • sessionId (number) — Unique session identifier
  • sessionWalletAddress (string) — Wallet address for this session
  • currentPositions (array) — Assets allocated at execution start
  • executionMode (string) — "auto" or "manual"
  • settings (object) — Resolved setting values (defaults merged with session overrides)
Each position in currentPositions:
  • network (string) — e.g., "ethereum:137", "solana"
  • assetAddress (string) — Token contract address
  • tokenId (string | null) — For NFTs/ERC1155
  • avgUnitCost (string) — Average unit cost in USD (currently hardcoded to 0)
  • currentQty (string) — Current quantity in raw units
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 prediction market integration
  • signAndSend() / sign_and_send() — Sign and broadcast custom transactions
  • signMessage() / sign_message() — Sign messages (EVM only)
  • transactions() — Get transaction history
  • getCurrentPositions() / get_current_positions() — Get live positions
  • clearSuggestedTransactions() / clear_suggested_transactions() — Clear pending manual mode suggestions

SDK Response Pattern

Every SDK method returns a response object with consistent shape:
{
  success: boolean;
  data?: any;       // present on success
  error?: string;   // present on failure
}
Always check success before using data:
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

  • All amounts are strings in smallest units (wei for EVM, lamports for Solana)
  • Keep as strings to preserve precision
  • Exception: Hyperliquid uses formatted values (e.g., “1.5” for $1.5 USDC)
  • Minimum order size: 10notionalonHyperliquid(appliestobothopensandclosesapositionwhosemarkvaluehasdecayedbelow10 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). 50+onEthereumL1and50+ 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

// 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 internal/object logging. See checklist rule #4 for what makes a good log.

Positions

agent.currentPositions — Snapshot at execution start. Use for initial decision-making. getCurrentPositions() / get_current_positions() — Fetch live positions. Note: indexing can lag by chain; may not reflect transactions immediately. Handling pending transactions: Check hasPendingTxs flag. If true, balances may not be accurate. For time-sensitive logic, track deltas in memory.
const positions = await agent.getCurrentPositions();
if (positions.success && positions.data) {
  if (positions.data.hasPendingTxs) {
    await agent.log("Pending transactions detected");
  }
  for (const pos of positions.data.positions) {
    await agent.log(`${pos.assetAddress}: ${pos.currentQty}`);
  }
}

Swap

Two-step workflow: quote then execute.
// 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

All transactional methods (signAndSend, swap.execute, placeOrder, transfer, marketOrder) accept an optional expiresAt parameter (ISO 8601 string) 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):
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:
import { EMPTY_DATA } from "@circuitorg/agent-sdk";

await agent.signAndSend({
  network: "ethereum:8453",
  request: { toAddress: recipient, data: EMPTY_DATA, value: "1000000000000000000" },
  message: "Send 1 ETH",
});
Sign and send (Solana):
const response = await agent.signAndSend({
  network: "solana",
  request: {
    hexTransaction: "010001030a0b..."  // Serialized VersionedTransaction as 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, etc.). 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

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, from/to, amount, token, tokenType, tokenUsdPrice, timestamp. Note: indexing has a delay per chain.

Hyperliquid Integration

Configuration in circuit.toml:
[startingAsset]
network = "hypercore:perp"
address = "USDC"
minimumAmount = "100000000"  # 1 USDC (8 decimals)
Key differences for Hyperliquid agents:
  • Only one Hyperliquid agent per wallet (to safely manage margin)
  • Your entire wallet balance is available to the agent
  • Use agent.platforms.hyperliquid.balances() and agent.platforms.hyperliquid.positions() instead of currentPositions
  • Avoid calling balances/positions in 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
  • **10notionalminimumoneveryorder,includingcloses.Beforesizingatrade,computesizemarkPriceandskipifbelow10 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 — since reduceOnly: true prevents rounding the size up, you must detect the case and skip rather than submit a guaranteed-failing order.
Place order: Fetch the mid price first (see “Realtime price lookups”), then derive slippage and trigger values from it.
const mid = await getHyperliquidMid("BTC");         // helper from "Realtime price lookups"
const order = await agent.platforms.hyperliquid.placeOrder({
  symbol: "BTC",          // Perps: "BTC". Spot: "BTC/USDC"
  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",         // "market", "limit", "stop", "take_profit"
  reduceOnly: true,       // optional: only reduce existing position
  postOnly: false,        // optional: limit orders only, reject if would take
  triggerPrice: mid * 0.95, // optional: trigger price for stop/take_profit orders — derive from mid, never hardcode
  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
});
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).
// Trade gold perps on Trade[XYZ]
const goldMid = await getHyperliquidMid("xyz:GOLD"); // same helper, same symbol format
const order = await agent.platforms.hyperliquid.placeOrder({
  symbol: "xyz:GOLD",
  side: "buy", size: 1, price: goldMid * 1.01,
  market: "perp", type: "market",
});

// Get positions for Trade[XYZ] only
const xyzPositions = await agent.platforms.hyperliquid.positions("xyz");

// Get positions across all venues (native + builder DEXes)
const allPositions = await agent.platforms.hyperliquid.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):
  • balances() — Perp account value + spot balances. Returns { perp: { accountValue, totalMarginUsed, withdrawable }, spot: [{ coin, total, hold }] }
  • positions(dex?) / positions(dex=None) — Open perpetual positions; pass "xyz", "cash", or "vntl" to filter by builder DEX. Each position includes symbol, side, size, entryPrice, markPrice, liquidationPrice, unrealizedPnl, leverage, marginUsed
  • order(orderId) / order(order_id) — Get order info
  • deleteOrder(orderId, symbol) / delete_order(order_id, symbol) — Cancel order
  • openOrders() / open_orders() — All open orders
  • orders() — Historical orders (includes timestamp, statusTimestamp, orderType)
  • orderFills() / order_fills() — Fill history (includes fee, isMaker, timestamp)
  • transfer({ amount, toPerp, expiresAt? }) / transfer({ "amount": ..., "toPerp": ... }) — Transfer between spot and perp
  • liquidations(startTime?) / liquidations(start_time=None) — Liquidation events (includes liquidatedPositions, totalNotional, leverageType)

Polymarket Integration

Market order:
// tokenId identifies a specific outcome — get it from Polymarket's CLOB API or from getCurrentPositions()
// BUY: size = USD amount to spend
const buy = await agent.platforms.polymarket.marketOrder({
  tokenId: "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  size: 10,      // Spend $10
  side: "BUY",
  expiresAt: "2030-01-01T00:00:00Z", // optional, manual mode only
});

// SELL: size = number of shares to sell
const sell = await agent.platforms.polymarket.marketOrder({
  tokenId: "71321045679252212594626385532706912750332728571942532289631379312455583992563",
  size: 5,       // Sell 5 shares
  side: "SELL"
});
Redeem positions:
// Redeem all settled positions
const result = await agent.platforms.polymarket.redeemPositions();

// Redeem specific positions
const result = await agent.platforms.polymarket.redeemPositions({
  tokenIds: ["123456", "789012"]
});
Notes: Polymarket has decimal precision differences for buys/sells — may result in dust. Clean up with redeemPositions() after market expiry. Position data includes enriched metadata via getCurrentPositions():
  • question — Market question text
  • outcome — Outcome name (e.g., “Yes”, “No”)
  • priceUsd / averagePriceUsd — Current and average purchase price
  • valueUsd / formattedShares — Current value and share count
  • pnlUsd / pnlPercent — Unrealized P&L
  • pnlRealizedUsd / pnlRealizedPercent — Realized P&L
  • isRedeemable — Whether position can be redeemed (market settled)
  • endDate — Market end date (ISO 8601)

Configuration

The circuit.toml Configuration File

This is critical — it defines your agent’s metadata, asset requirements, and behavior:
name = "My Agent"                        # max 32 characters
tagline = "Short subtitle for cards"     # max 32 characters
walletType = "ethereum"  # or "solana"
allowedExecutionModes = ["auto", "manual"]
runtimeIntervalMinutes = 15
filesToInclude = []
filesToExclude = []
imageUrl = "https://cdn.circuit.org/agents/default"
version = "0.0.1"

[startingAsset]
network = "ethereum:1"
address = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
minimumAmount = "10000000000000000"
Agent descriptions go in DESCRIPTION.md (same directory). Fill in all four sections: Summary, Strategy, Risks, and Changelog. Key fields:
  • name — Display name shown in the Circuit UI (32 characters or less). Cannot change after first publish.
  • tagline — Brief subtitle shown on agent cards (32 characters or less)
  • 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.
  • runtimeIntervalMinutes — How often run() is called
  • filesToInclude / filesToExclude — For monorepo setups
  • imageUrl — URL for the agent icon displayed in the Circuit UI
  • version — Semver version (major.minor.patch), must be incremented before each publish
  • deploymentRegionOverride — Optional: "us-east-1" (default) or "eu-central-1"
  • startingAsset — The asset a user must have to start a session; minimumAmount in raw units (wei/lamports)

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 TypeRuntime ValueDefault TypeNotes
textstringstring (max 1000 chars)Free-form text
booleanbooleantrue or falseToggle
single_selectstringstring matching an optionRequires options array (max 20)
integernumberwhole numberNo decimals
numbernumberany numberDecimals allowed
percentagenumbernumber 0–100Validated range
addressstringwallet addressValidated against walletType
Example:
[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:
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 everywhere in the SDK:
  • EVM: "ethereum:{chainId}" — e.g., "ethereum:1" (Mainnet), "ethereum:137" (Polygon), "ethereum:42161" (Arbitrum), "ethereum:8453" (Base), "ethereum:10" (Optimism), "ethereum:56" (BSC), "ethereum:43114" (Avalanche), "ethereum:100" (Gnosis), "ethereum:146" (Sonic), "ethereum:59144" (Linea)
  • Solana: "solana"
  • Hyperliquid Perps: "hypercore:perp"
  • Hyperliquid Spot: "hypercore:spot"
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.
TokenMainnet (1)Arbitrum (42161)Base (8453)Polygon (137)Optimism (10)
USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xaf88d065e77c8cC2239327C5EDb3A432268e58310x833589fCD6eDb6E08f4c7C32D4f71b54bdA029130x3c499c542cEF5E3811e1192ce70d8cC03d5c33590x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
USDT0xdAC17F958D2ee523a2206206994597C13D831ec70xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb90xc2132D05D31c914a87C6611C10748AEb04B58e8F0x94b008aA00579c1307B0EF2c499aD98a8ce58e58
WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20x82aF49447D8a07e3bd95BD0d56f35241523fBab10x42000000000000000000000000000000000000060x7ceB23fD6bC0adD59E62ac25578270cFf1b9f6190x4200000000000000000000000000000000000006
DAI0x6B175474E89094C44Da98b954EedeAC495271d0F0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da10x50c5725949A6F0c72E6C4a641F24049A917DB0Cb0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A0630xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1
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

CommandDescription
circuit signinAuthenticate (opens browser)
circuit signoutSign out
circuit whoamiShow current user
circuit newCreate new agent project
circuit runTest agent locally
circuit unwindUnwind positions and end session
circuit checkValidate agent project offline (no auth required)
circuit publishPublish to Circuit infrastructure
circuit walletsList wallets and balances
Global flags (available on all commands):
  • --path / -p — Agent project directory (defaults to current directory)
  • --format / -f — Output format: text (default), json, stream-json
  • --env / -e — Environment variable override (KEY=VALUE, repeatable)
  • --help / -h — Show help
  • --version / -v — Show CLI version
circuit new: --language / -l, --name / -n, --template / -t (basic, yield, polymarket, hyperliquid), --path / -p (output directory). circuit run: --wallet / -w, --mode / -m (auto/manual), --amount / -a (token amount in smallest unit), --port (fixed port), --setting / -s (KEY=VALUE, repeatable), --local-sdk-dependencies / -l (skip dependency install). circuit unwind: --wallet / -w, --setting / -s (KEY=VALUE, repeatable), --local-sdk-dependencies / -l, --port. circuit whoami: --show-auth / -a (show token details for CI/CD). circuit publish 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.
// 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");
if (result.success && result.data) {
  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 — use currentPositions or getCurrentPositions() instead (rule #6)
  • 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 on-chain reads. DefiLlama (protocol TVL, yield data):
// 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");
On-chain reads with viem (TypeScript):
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",
});
On-chain reads with web3py (Python):
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:
  • Wrap external fetches in try/catch — network errors should not crash your agent
  • Cache slow data in memory across runs (e.g., APY that updates hourly)
  • DefiLlama endpoints are unauthenticated and free

Error Handling Best Practices

  1. Always check success before using data
  2. Log errors with { error: true } for UI visibility
  3. Return early on errors
  4. Don’t wrap SDK calls in try/catch — use .success checks
  5. Uncaught exceptions are auto-caught by SDK
  6. Use try/catch for your own logic, external APIs, parsing
Common error patterns:
ErrorCauseSolution
”Key not found”Memory key doesn’t existHandle missing keys gracefully
”Quote failed”Amount too small / no liquidityIncrease amount, adjust slippage
”Transaction failed”Insufficient balance/gasCheck getCurrentPositions() first
”Transaction reverted onchain”Tx was mined but revertedCheck contract address, input params, token approvals
”Invalid request”Bad parametersValidate inputs before SDK calls
”No routes found”Swap routing failureCheck token addresses, try higher slippage, verify liquidity exists

Multi-Agent Monorepo Support

You can organize multiple agents in a monorepo with shared utility code:
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 filesToInclude = ["../../utils"] in circuit.toml to include shared directories. For Python, add sys.path.insert(0, ...) at the top of main.py. For TypeScript, configure tsconfig.json path aliases.