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”).
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:- 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, notBTC/USDC- see “Spot orders” under Hyperliquid Integration). If you can’t confirm an address or ticker, don’t invent one. - Never hardcode asset prices - fetch them at runtime. A numeric literal in an order
pricefield is a bug even as a placeholder. See “Realtime price lookups” below for the recipe per venue. - 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 fromagent.allocation; derive a swap rate from the quote, not from a price. - 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.
agent.allocationis the source of truth for holdings assigned to the session at invocation start - never size from a raw whole-wallet or exchange-account read.balancesis fungible inventory (cash, spot, staking, Kraken’s synthetic availablekraken:allocation:USDcash, and Kraken base inventory carryingkrakenMetadata);positionsis 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.- 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. HyperliquidallMids) over a per-asset loop across the whole market.
- Use viem (TypeScript) or web3py (Python) for numeric formatting, calldata encoding, and RPC reads - everything except signing a transaction.
- Keep ABIs in a separate constants file (and most other constants too).
- 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.
- 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. - 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). DESCRIPTION.mdis 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 isusing exactly yourcircuit.tomlcategory’s labels.circuit checkvalidates 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.quoteoutput 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
runimplements the full sequence andunwindwalks 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.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:
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 inrun, closed in unwind; this is the default venue, see “Choosing where to execute”):
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:
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.
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: 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:
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 identifiersessionWalletAddress(TypeScript) /session_wallet_address(Python) (string) - Wallet address for this sessionallocation(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.availableBaseVolumeis the exact sell quantity enforced for the balance’stokenAddress. See “Kraken Integration”.executionMode(TypeScript) /execution_mode(Python) (string) -"auto"or"manual"settings(object) - Resolved setting values (defaults merged with session overrides)
allocation.balances (fungible inventory: cash, spot, staking):
network(string) - e.g.,"ethereum:137","solana"assetKey(string) - Canonical asset keytokenAddress(string) - Token contract addresstokenId(string | null) - For NFTs/ERC1155symbol(string | null) - Token symboldecimals(number) - Token decimalsamountRaw(string) - Quantity held in raw base unitsmarketValueUsd(string | null) - Current market value in USDhyperliquidMetadata({ 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) - usenetworkto distinguish those. Find HL collateral withbalances.find(b => b.hyperliquidMetadata?.collateral).
allocation.positions (open market exposures: Hyperliquid perps and Polymarket outcome tokens):
network(string),assetKey(string),coin(string) - perp coin or Polymarket outcome labelsize(string) - signed for perps (long > 0, short < 0); shares held for PolymarketaverageEntryPrice(string | null),priceUsd(string | null),unrealizedPnlUsd(string | null)hyperliquidMetadata({ leverage, liquidationPrice, marginUsed }| undefined) - present only on Hyperliquid perps.leverageandmarginUsedare strings;liquidationPriceis 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’shyperliquidMetadata({ 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.
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 discovery, orders, and redemptionplatforms.kraken- Kraken market data, account reads, and spot orderssignAndSend()/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 — useswap,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 historyclearSuggestedTransactions()/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
- 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 pairdecimalToBaseUnits(decimal, decimals)/baseUnitsToDecimal(raw, decimals)(decimal_to_base_units/base_units_to_decimalin Python) andbigint/intarithmetic on raw units. Spend-all is the allocation entry’samountRaw, 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 takeshares. - Minimum order size: 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-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
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).
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
Transactional request shapes that exposeexpiresAt (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):
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, 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
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 incircuit.toml:
- 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
marketfield 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(balancescarrieshyperliquidMetadata.collateral;positionscarrieshyperliquidMetadata- 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’shyperliquidMetadata.marginUsed. Never subtractmarginUsedfrom session collateral - that spuriously fires on healthy leveraged positions - Avoid calling
midpointPricein 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. 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.
coinis a literal ticker string you write -"ETH","BTC","xyz:GOLD"- not a value you derive at runtime. Onlypriceis fetched (it is market data); the asset you trade is fixed by the strategy, so hardcode it. Do NOT readcoinfrom an API response, amidslookup, an allocation position entry, or any variable - that’s how it ends upundefinedand the order is rejected withcoin: Invalid input: expected string, received undefined. The field iscoin, neversymbol.
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).
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 ordermidpointPrice(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 withWALLET_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 nameeventMarkets(slug)/event_markets(slug)- resolution step 2: one event’s COMPLETE market catalogmarketOrder({ tokenId, side, spendUsd | shares })/market_order({...})- amounts are unit-explicit: BUY takesspendUsd(USD to spend, e.g.20= $20 of shares); SELL takesshares(shares to sell). The mismatched field fails validation.redeemPositions({ tokenIds })/redeem_positions({...})- redeem settled positions (only ones your allocation marksisRedeemable)
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 CLOBtokenId. 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:
- 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:eventscarries the top matches with the markets search exposes;moreEventsis lower-ranked{ slug, title, startDate }refs - check it when the wanted event isn’t inevents(a match is often outranked by unrelated popular markets). - 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).eventMarketsreturns 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 carriesquestion,sportsMarketType,groupItemTitle,line,gameStartTime, liquidity/volume, and each outcome’sname,priceUsd, and thetokenIdorders take.
- Search never returns empty - nonsense queries fuzzy-match unrelated events. Verify a returned
title/questionactually names what the user asked for; if nothing does, report “no matching market” instead of trading the closest miss. - 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.
- 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 -groupItemTitlenames the side, so “bet on Egypt” = the market withgroupItemTitle: "Egypt", outcome"Yes". Totals are one market per line with Over/Under outcomes - “over 0.5 goals” = thesportsMarketType: "totals",line: 0.5market, outcome"Over". Match markets carrygameStartTimelike"2026-07-03 18:00:00+00"(space-separated, not ISOT) - use it for “today”.
Worked example: “bet $20 on Egypt in the World Cup today”
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 (orcircuit kraken connect). Declare the requirement in circuit.toml - this is what makes the credential picker and allocation slider appear in the start flow:
- 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 ascredentialRefon 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 priceagainst 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.availableBaseVolumethe gate computed at run start. Match the order’s base asset totokenAddress, 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:USDis 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:USDvirtual 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 isXBT, 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 theKRAKEN: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.
tickerneeds no credential;balances,placeOrder,orderStatus,openOrders,cancelOrderneedcredentialRef.
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:** valuelines, 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 (writeGold, notGold (XAUT)orGold (0x…)). Use exactly the labels for yourcircuit.tomlcategory-circuit checkvalidates 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>.
- spread -
## 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.
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:
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 UIwalletType- 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 runwhen--modeis omitted.[[triggers]]- The dispatch configuration: exactly onescheduletrigger (everyminutes,alignrolling/fixed) plus optional reactive triggers (for example price moves)filesToExclude- Exclude files from the upload bundlestartingAsset- The asset a user must have to start a session;minimumAmountin raw units (wei/lamports)[exchangeCredentials.kraken]- Declares that the agent trades the user’s Kraken account (see “Kraken Integration”);trade = trueplusminimumAllocationUsddrive 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:
Example:
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 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.
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.
Common DeFi protocols (Base):
- Aave V3 Pool:
0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 - Uniswap V3 Router:
0x2626664c2603336E57B271c5C0b26F421741e481
- Aave V3 Pool:
0x794a61358D6845594F94dc1DB02A252b5b4814aD - Uniswap V3 Router:
0xE592427A0AEce92De3Edee1F18E0157C05861564
minimumAmount or constructing transaction amounts.
CLI Commands Reference
Global flags (available on all commands):
--json- Output machine-readable JSON: a single document for one-shot commands; an NDJSON event stream forrun/unwind--env <name>- Target deployment:production(default),staging, orlocal. Takes precedence overCIRCUIT_ENV.--help- Show help--version- Show CLI version
-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.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 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. Usefetch() for HTTP APIs and viem/web3py for onchain reads.
DefiLlama (protocol TVL, yield data):
- Let a failed external read throw - the run fails loud and the next interval re-runs. Do NOT
try/catchit 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
Checkresult.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: