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
- ABIs MUST be in a separate constants file, most constants SHOULD be in a separate constants file.
- 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.
- 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.
- 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.
- Focus on clear simple understandable auditable financial logic - they must be trustworthy and reliable since people will run money through them.
- 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.
- Use agent.log() instead of print statements.
- Every agent MUST have a working
unwind()that actually closes/exits positions — not just a log message. See “Unwind Patterns” below. - NEVER hardcode asset prices that can change — fetch at runtime. See “Realtime price lookups” below for the recipe per venue. Numeric literals in
price/triggerPricefields are a bug, even as placeholders. - Verify API paths and asset tickers and token addresses, never guess.
- 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
EveryplaceOrder/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.
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: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.
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):
constants.py):
import { USDC, erc20Abi } from "./constants" (TS) or from constants import USDC, ERC20_ABI (Python).
SDK Reference
Execution Model
- Circuit sends a
runorunwindcommand to your agent - SDK creates an
AgentContextobject - SDK calls your
runfunction with the context - Your code uses SDK methods to analyze positions, execute trades, etc.
- SDK returns results to Circuit
- In manual mode, transactions are submitted for user approval in the Circuit UI
auto— Transactions execute automaticallymanual— Transactions become suggestions for user approval
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:
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 identifiersessionWalletAddress(string) — Wallet address for this sessioncurrentPositions(array) — Assets allocated at execution startexecutionMode(string) —"auto"or"manual"settings(object) — Resolved setting values (defaults merged with session overrides)
currentPositions:
network(string) — e.g.,"ethereum:137","solana"assetAddress(string) — Token contract addresstokenId(string | null) — For NFTs/ERC1155avgUnitCost(string) — Average unit cost in USD (currently hardcoded to 0)currentQty(string) — Current quantity in raw units
log()— Send messages to users and log locallymemory— Session-scoped key-value storage (.set(),.get(),.delete())swap— Cross-chain swap operations (.quote(),.execute())platforms.hyperliquid— Hyperliquid DEX integrationplatforms.polymarket— Polymarket prediction market integrationsignAndSend()/sign_and_send()— Sign and broadcast custom transactionssignMessage()/sign_message()— Sign messages (EVM only)transactions()— Get transaction historygetCurrentPositions()/get_current_positions()— Get live positionsclearSuggestedTransactions()/clear_suggested_transactions()— Clear pending manual mode suggestions
SDK Response Pattern
Every SDK method returns a response object with consistent shape:success before using data:
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: 10 cannot be closed with a reduce-only order, so unwind logic must detect and skip it). 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
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.
Swap
Two-step workflow: quote then execute.- Omit
fromToken/toTokenfor 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 — useisSuggestedTransaction()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):
EMPTY_DATA constant ("0x") for simple ETH transfers with no contract interaction:
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
network, transactionHash, from/to, amount, token, tokenType, tokenUsdPrice, timestamp. Note: indexing has a delay per chain.
Hyperliquid Integration
Configuration incircuit.toml:
- Only one Hyperliquid agent per wallet (to safely manage margin)
- Your entire wallet balance is available to the agent
- Use
agent.platforms.hyperliquid.balances()andagent.platforms.hyperliquid.positions()instead ofcurrentPositions - 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
- **10. On unwind, a losing position can decay below the minimum — since
reduceOnly: trueprevents rounding the size up, you must detect the case and skip rather than submit a guaranteed-failing order.
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).
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 includessymbol,side,size,entryPrice,markPrice,liquidationPrice,unrealizedPnl,leverage,marginUsedorder(orderId)/order(order_id)— Get order infodeleteOrder(orderId, symbol)/delete_order(order_id, symbol)— Cancel orderopenOrders()/open_orders()— All open ordersorders()— Historical orders (includestimestamp,statusTimestamp,orderType)orderFills()/order_fills()— Fill history (includesfee,isMaker,timestamp)transfer({ amount, toPerp, expiresAt? })/transfer({ "amount": ..., "toPerp": ... })— Transfer between spot and perpliquidations(startTime?)/liquidations(start_time=None)— Liquidation events (includesliquidatedPositions,totalNotional,leverageType)
Polymarket Integration
Market order:redeemPositions() after market expiry. Position data includes enriched metadata via getCurrentPositions():
question— Market question textoutcome— Outcome name (e.g., “Yes”, “No”)priceUsd/averagePriceUsd— Current and average purchase pricevalueUsd/formattedShares— Current value and share countpnlUsd/pnlPercent— Unrealized P&LpnlRealizedUsd/pnlRealizedPercent— Realized P&LisRedeemable— 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:
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 forcircuit run.runtimeIntervalMinutes— How oftenrun()is calledfilesToInclude/filesToExclude— For monorepo setupsimageUrl— URL for the agent icon displayed in the Circuit UIversion— Semver version (major.minor.patch), must be incremented before each publishdeploymentRegionOverride— Optional:"us-east-1"(default) or"eu-central-1"startingAsset— The asset a user must have to start a session;minimumAmountin 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 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 |
required = true) have no default — users must provide a value before the agent runs. Access at runtime via agent.settings:
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"
- 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 |
- Aave V3 Pool:
0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 - Uniswap V3 Router:
0x2626664c2603336E57B271c5C0b26F421741e481
- Aave V3 Pool:
0x794a61358D6845594F94dc1DB02A252b5b4814aD - Uniswap V3 Router:
0xE592427A0AEce92De3Edee1F18E0157C05861564
minimumAmount or constructing transaction amounts.
CLI Commands Reference
| Command | Description |
|---|---|
circuit signin | Authenticate (opens browser) |
circuit signout | Sign out |
circuit whoami | Show current user |
circuit new | Create new agent project |
circuit run | Test agent locally |
circuit unwind | Unwind positions and end session |
circuit check | Validate agent project offline (no auth required) |
circuit publish | Publish to Circuit infrastructure |
circuit wallets | List wallets and balances |
--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.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)
- Do NOT store balances or position quantities — use
currentPositionsorgetCurrentPositions()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. Usefetch() for HTTP APIs and viem/web3py for on-chain reads.
DefiLlama (protocol TVL, yield data):
- 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
- Always check
successbefore usingdata - Log errors with
{ error: true }for UI visibility - Return early on errors
- Don’t wrap SDK calls in try/catch — use
.successchecks - Uncaught exceptions are auto-caught by SDK
- Use try/catch for your own logic, external APIs, parsing
| Error | Cause | Solution |
|---|---|---|
| ”Key not found” | Memory key doesn’t exist | Handle missing keys gracefully |
| ”Quote failed” | Amount too small / no liquidity | Increase amount, adjust slippage |
| ”Transaction failed” | Insufficient balance/gas | Check getCurrentPositions() 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: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.