AGENTS.md file, or paste it directly into your AI assistant’s context window.
Copy
# 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 Developer — Building Circuit Agents with the SDK + CLI
### End-to-End Workflow Summary
1. **Install** CLI and SDK
2. **Authenticate** with `circuit login`
3. **Initialize** project with `circuit init`
4. **Install dependencies** (`bun install` or `uv sync`)
5. **Write agent logic** in `run()` and `unwind()` functions
6. **Test locally** with `circuit run`
7. **Deploy** with `circuit publish`
### Installation
**Prerequisites:**
- Bun (required for CLI and TypeScript)
- Python 3.12+ (for Python agents)
- A Circuit account
**CLI (always via Bun):**
```bash
bun install -g @circuitorg/agent-cli
circuit --version
```
**Python SDK:**
```bash
uv pip install circuit-agent-sdk
# or: pip install circuit-agent-sdk
```
**TypeScript SDK:**
```bash
bun add @circuitorg/agent-sdk
# or: npm install @circuitorg/agent-sdk
```
**Recommended tooling:**
- Python: uv (package manager), ruff (linter), ty (typechecker)
- TypeScript: Bun (runtime), Biome (linter)
### Project Structure
After `circuit init`, you get:
**TypeScript:**
```
<agent-name>/
├── index.ts # Main agent code
├── package.json # Dependencies
└── .circuit.toml # Agent configuration
```
**Python:**
```
<agent-name>/
├── main.py # Main agent code
├── pyproject.toml # Dependencies (pin versions!)
└── .circuit.toml # Agent configuration
```
### The `.circuit.toml` Configuration File
This is critical — it defines your agent's metadata, asset requirements, and behavior:
```toml
name = "My Agent"
slug = "my-agent"
description = "Agent description"
shortDescription = ""
defaultSleepIntervalMinutes = 15
allowedWalletTypes = ["ethereum"] # or ["solana"]
allowedExecutionModes = ["manual", "auto"]
devRunExecutionMode = "manual"
filesToInclude = []
filesToExclude = []
[benchmarkCurrency]
network = "fiat:usd"
address = ""
[[requiredAssets]]
network = "ethereum:1"
address = "0x0000000000000000000000000000000000000000"
minimumAmount = "10000000000000000"
devRunAmount = "10000000000000000"
```
**Key fields:**
- `defaultSleepIntervalMinutes` — How often `run()` is called
- `allowedExecutionModes` — `"auto"` (transactions execute immediately) and/or `"manual"` (user must approve in UI)
- `allowedWalletTypes` — `"ethereum"` or `"solana"`
- `requiredAssets` — The asset a user must have to start a session; `minimumAmount` and `devRunAmount` in raw units (wei/lamports)
- `filesToInclude` / `filesToExclude` — For monorepo setups
**IMPORTANT TOML ordering**: Top-level properties (`filesToInclude`, `filesToExclude`) MUST be placed before any table headers (`[benchmarkCurrency]`, `[[requiredAssets]]`).
### Agent Code Pattern
**TypeScript:**
```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
}
const agent = new Agent({
runFunction: run,
unwindFunction: unwind
});
export default agent.getExport();
```
**Python:**
```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
agent = Agent(run_function=run, unwind_function=unwind)
handler = agent.get_handler()
if __name__ == "__main__":
agent.run()
```
### Execution Model
1. Circuit infrastructure sends `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. The response will include `suggested: bool` and `suggestionId: int` in manual mode.
**Important**: All suggested transactions are automatically soft-deleted at the beginning of each `run` execution. Use `expiresAt` field for shorter expiry.
**Execution interval**: Set via `defaultSleepIntervalMinutes` 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"`
**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()`, `.list()`)
- `swidge` — Cross-chain swap and bridge 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
### Network Identifiers
Used everywhere in the SDK:
- **EVM**: `"ethereum:{chainId}"` — e.g., `"ethereum:1"` (Mainnet), `"ethereum:137"` (Polygon), `"ethereum:42161"` (Arbitrum), `"ethereum:8453"` (Base)
- **Solana**: `"solana"`
- **Hyperliquid**: `"hypercore:perp"`
**Native token addresses:**
- EVM chains: `"0x0000000000000000000000000000000000000000"`
- Solana: `"11111111111111111111111111111111"`
### SDK Response Pattern
**Every** SDK method returns a response object with consistent shape:
```typescript
{
success: boolean;
data?: any; // present on success
error?: string; // present on failure
errorMessage?: string;
errorDetails?: object;
}
```
**Always check `success` before using `data`:**
```typescript
const result = await agent.swidge.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 for your own parsing, calculations, or external API calls.
### 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)
### Logging
```typescript
// 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.
### Memory (Session-Scoped Key-Value Storage)
Keys are auto-namespaced by agent and session. Memory persists across execution cycles within the same session, cleared when the session ends. **Values must be strings** — serialize JSON/numbers before storing.
```typescript
// Set
await agent.memory.set("lastPrice", "45000.50");
// Get
const result = await agent.memory.get("lastPrice");
if (result.success && result.data) {
const price = parseFloat(result.data.value);
}
// Delete
await agent.memory.delete("tempKey");
// List all keys
const keys = await agent.memory.list();
```
Python: `agent.memory.set("key", "value")`, `agent.memory.get("key")`, `agent.memory.delete("key")`, `agent.memory.list()`
### Suggestions (Manual Mode)
If your agent supports manual mode, transactions become suggestions for user approval. Circuit auto-clears suggestions at the start of each `run`. You can also manually clear:
```typescript
await agent.clearSuggestedTransactions();
```
Python: `agent.clear_suggested_transactions()`
### 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.
```typescript
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 and Bridge
Two-step workflow: quote then execute.
```typescript
// 1. Get quote
const quote = await agent.swidge.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
toToken: "0xd93f7e271cb87c23aaa73edc008a79646d1f9912", // wSOL
slippage: "10.0"
});
// 2. Execute
if (quote.success && quote.data) {
const result = await agent.swidge.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
- Circuit filters >100% price impact automatically — validate against your own thresholds
- Minimum $10-20 recommended to avoid fee issues
- Bulk execution: pass array of quotes to `execute()`
- Execution status is final: `"success"`, `"failure"`, `"refund"`, or `"delayed"`
- `engines` parameter optional: constrain which routing engines to query
### Custom Transactions / Signing
**Sign and send (EVM):**
```typescript
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"
});
```
**Sign and send (Solana):**
```typescript
const response = await agent.signAndSend({
network: "solana",
request: {
hexTransaction: "010001030a0b..." // Serialized VersionedTransaction as hex
}
});
```
**Sign message (EVM only):**
```typescript
const signature = await agent.signMessage({
network: "ethereum:1",
request: {
messageType: "eip191", // or "eip712"
data: { message: "Hello, world!" },
chainId: 1
}
});
// signature.formattedSignature contains the complete hex signature
```
Python equivalents: `agent.sign_and_send()`, `agent.sign_message()` with snake_case field names (`to_address`, `tx_hash`, etc.)
### Transaction History
```typescript
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`:**
```toml
[[requiredAssets]]
network = "hypercore:perp"
address = ""
minimumAmount = "1000000"
devRunAmount = "1000000"
```
**Key differences for Hyperliquid agents:**
- Only one Hyperliquid agent per wallet (to safely manage margin)
- `devRunAmount` is irrelevant — entire balance available
- 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
**Place order:**
```typescript
const order = await agent.platforms.hyperliquid.placeOrder({
symbol: "BTC", // Perps: "BTC". Spot: "BTC/USDC"
side: "buy",
size: 0.0001,
price: 110000, // Slippage limit for market orders
market: "perp", // "perp" or "spot"
type: "market" // "market", "limit", "stop", "take_profit"
});
```
**Other methods:**
- `balances()` — Perp account value + spot balances
- `positions()` — Open perpetual positions
- `order(orderId)` — Get order info
- `deleteOrder(orderId, symbol)` — Cancel order
- `openOrders()` — All open orders
- `orders()` — Historical orders
- `orderFills()` — Fill history
- `transfer({ amount, toPerp })` — Transfer between spot and perp
- `liquidations(startTime?)` — Liquidation events
### Polymarket Integration
**Market order:**
```typescript
// BUY: size = USD amount to spend
const buy = await agent.platforms.polymarket.marketOrder({
tokenId: "123456",
size: 10, // Spend $10
side: "BUY"
});
// SELL: size = number of shares to sell
const sell = await agent.platforms.polymarket.marketOrder({
tokenId: "123456",
size: 5, // Sell 5 shares
side: "SELL"
});
```
**Redeem positions:**
```typescript
// 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 (question, outcome, PNL) via `getCurrentPositions()`.
### 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:**
| 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 |
| "Invalid request" | Bad parameters | Validate inputs before SDK calls |
### CLI Commands Reference
| Command | Description |
|---|---|
| `circuit login` | Authenticate (opens browser) |
| `circuit logout` | Sign out |
| `circuit whoami` | Show current user |
| `circuit init` | Create new agent project |
| `circuit run` | Test agent locally |
| `circuit publish` | Publish to production |
| `circuit info` | Show CLI/agent info |
| `circuit wallet list` | List wallets |
| `circuit wallet import` | Import wallet for testing |
**`circuit run` behavior:** Creates a test session, starts local agent server, executes `run()`, streams logs to terminal. Requires dependencies installed (`bun install` / `uv sync`).
**`circuit publish` behavior:** Validates config, uploads files, publishes to Circuit infrastructure. Requires authentication and valid `.circuit.toml`.
### 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
│ │ ├── pyproject.toml / package.json
│ │ └── .circuit.toml
│ ├── 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.
### Complete Example: Basic Swap Agent
See the Basic Swap Agent example in the docs for a full working agent that:
1. Checks `currentPositions` for USDC balance
2. Gets a swap quote from USDC to wSOL on Polygon
3. Executes the swap
4. Implements an `unwind` function skeleton