Scaffold this example locally and test it with the CLI:
circuit new --name my-yield-agent --language python --template yield
cd my-yield-agent
circuit run # execute a run cycle
circuit unwind # test the unwind logic
circuit.toml
circuit.toml
name = "Example Aave Yield Agent"
tagline = "Deposits USDC into Aave V3"
category = "yield"
imageUrl = "https://api.circuit.org/assets/agents/default"
walletVmFamily = "evm"
allowedExecutionModes = ["auto", "manual"]
[[triggers]]
type = "schedule"
every = 60
align = "rolling"
[startingAsset]
network = "ethereum:8453" # Base
address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC
minimumAmount = "1000000" # 1 USDC (6 decimals)
Example
from circuit_sdk import AgentContext
from eth_abi import encode
# Base addresses
USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
AAVE_V3_POOL = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5"
# Function selectors
APPROVE_SELECTOR = "095ea7b3"
SUPPLY_SELECTOR = "617ba037"
WITHDRAW_SELECTOR = "69328dec"
MAX_UINT256 = 2**256 - 1
def build_approve_data(spender: str, amount: int) -> str:
params = encode(["address", "uint256"], [spender, amount])
return "0x" + APPROVE_SELECTOR + params.hex()
def build_supply_data(asset: str, amount: int, on_behalf_of: str) -> str:
params = encode(
["address", "uint256", "address", "uint16"], [asset, amount, on_behalf_of, 0]
)
return "0x" + SUPPLY_SELECTOR + params.hex()
def build_withdraw_data(asset: str, amount: int, to: str) -> str:
params = encode(["address", "uint256", "address"], [asset, amount, to])
return "0x" + WITHDRAW_SELECTOR + params.hex()
def run(agent: AgentContext) -> None:
# Find USDC balance
allocation = agent.allocation
usdc = next(
(b for b in allocation.balances if b.token_address.lower() == USDC.lower()),
None,
)
if not usdc or int(usdc.amount_raw) == 0:
agent.log("No USDC to deposit")
return
amount = int(usdc.amount_raw)
agent.log(f"Depositing {usdc.amount_raw} USDC (raw) into Aave V3")
# Step 1: Approve Aave Pool to spend USDC (skip if already approved)
approved_check = agent.memory.get("aaveApproved")
already_approved = approved_check.success and approved_check.data and approved_check.data.value == "true"
if not already_approved:
approve_result = agent.sign_and_send({
"network": "ethereum:8453",
"request": {
"to_address": USDC,
"data": build_approve_data(AAVE_V3_POOL, MAX_UINT256),
"value": "0",
},
"message": "Approve USDC for Aave V3",
})
if not approve_result.success:
agent.log(f"Approve failed: {approve_result.error}", error=True)
return
agent.memory.set("aaveApproved", "true")
agent.log("USDC approved for Aave V3")
# Step 2: Supply USDC to Aave V3
supply_result = agent.sign_and_send({
"network": "ethereum:8453",
"request": {
"to_address": AAVE_V3_POOL,
"data": build_supply_data(USDC, amount, agent.session_wallet_address),
"value": "0",
},
"message": "Supply USDC to Aave V3",
})
if supply_result.success:
agent.log("USDC deposited into Aave V3!")
else:
agent.log(f"Supply failed: {supply_result.error}", error=True)
def unwind(agent: AgentContext) -> None:
# Withdraw all USDC from Aave V3
result = agent.sign_and_send({
"network": "ethereum:8453",
"request": {
"to_address": AAVE_V3_POOL,
"data": build_withdraw_data(USDC, MAX_UINT256, agent.session_wallet_address),
"value": "0",
},
"message": "Withdraw all USDC from Aave V3",
})
if result.success:
agent.log("Withdrawn all USDC from Aave V3")
else:
agent.log(f"Withdraw failed: {result.error}", error=True)
import type { AgentContext } from "circuit:sdk";
import { encodeFunctionData, parseAbi } from "viem";
// Base addresses
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const AAVE_V3_POOL = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5";
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount)"]);
const aavePoolAbi = parseAbi([
"function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)",
"function withdraw(address asset, uint256 amount, address to)",
]);
export async function run(agent: AgentContext): Promise<void> {
// Find USDC balance
const allocation = agent.allocation;
const usdc = allocation.balances.find(
(b) => b.tokenAddress.toLowerCase() === USDC.toLowerCase()
);
if (!usdc || BigInt(usdc.amountRaw) === 0n) {
await agent.log("No USDC to deposit");
return;
}
const amount = BigInt(usdc.amountRaw);
await agent.log(`Depositing ${usdc.amountRaw} USDC (raw) into Aave V3`);
// Step 1: Approve Aave Pool to spend USDC (skip if already approved)
const approvedCheck = await agent.memory.get("aaveApproved");
const alreadyApproved = approvedCheck.success && approvedCheck.data?.value === "true";
if (!alreadyApproved) {
const approveData = encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [AAVE_V3_POOL, 2n ** 256n - 1n], // max uint256 = approve once
});
const approveResult = await agent.signAndSend({
network: "ethereum:8453",
request: {
toAddress: USDC,
data: approveData,
value: "0",
},
message: "Approve USDC for Aave V3",
});
if (!approveResult.success) {
await agent.log(`Approve failed: ${approveResult.error}`, { error: true });
return;
}
await agent.memory.set("aaveApproved", "true");
await agent.log("USDC approved for Aave V3");
}
// Step 2: Supply USDC to Aave V3
const supplyData = encodeFunctionData({
abi: aavePoolAbi,
functionName: "supply",
args: [USDC, amount, agent.sessionWalletAddress, 0],
});
const supplyResult = await agent.signAndSend({
network: "ethereum:8453",
request: {
toAddress: AAVE_V3_POOL,
data: supplyData,
value: "0",
},
message: "Supply USDC to Aave V3",
});
if (supplyResult.success) {
await agent.log("USDC deposited into Aave V3!");
} else {
await agent.log(`Supply failed: ${supplyResult.error}`, { error: true });
}
}
export async function unwind(agent: AgentContext): Promise<void> {
// Withdraw all USDC from Aave V3
const withdrawData = encodeFunctionData({
abi: aavePoolAbi,
functionName: "withdraw",
args: [USDC, 2n ** 256n - 1n, agent.sessionWalletAddress], // max uint256 = withdraw all
});
const result = await agent.signAndSend({
network: "ethereum:8453",
request: {
toAddress: AAVE_V3_POOL,
data: withdrawData,
value: "0",
},
message: "Withdraw all USDC from Aave V3",
});
if (result.success) {
await agent.log("Withdrawn all USDC from Aave V3");
} else {
await agent.log(`Withdraw failed: ${result.error}`, { error: true });
}
}
Sample Output
Agent run started
USDC balance: 50.00 (raw: 50000000)
Approving Aave V3 Pool to spend USDC...
Approval confirmed: 0xabc123...
Supplying 50000000 USDC to Aave V3...
Supply confirmed: 0xdef456...
Agent run completed
Agent unwind started
Withdrawing all USDC from Aave V3...
Withdrawal confirmed: 0x789abc...
Agent unwind completed
How It Works
- Check holdings: Reads
agent.allocationand finds USDC inbalances - Approve (once): On first run, approves Aave V3 Pool to spend USDC with max uint256 and stores a flag in memory. The server automatically waits for onchain confirmation before returning. Subsequent runs skip this step.
- Supply: Deposits USDC into Aave V3 using the Pool’s
supplyfunction - Unwind: Withdraws all USDC from Aave V3 using
withdrawwith max uint256
Notes
- This agent uses
signAndSendto build custom transactions with ABI-encoded calldata. See Custom Transactions and Signing for details. - TypeScript uses viem for ABI encoding - add it with
bun add viem. - Python uses eth-abi - add it with
uv add eth-abi. - Aave V3 Pool address and USDC address shown are for Base. Adjust for other networks.
- The
approve+supplypattern is standard for any ERC-20 DeFi deposit. The server automatically confirms each transaction before returning, so the approval is guaranteed to be confirmed before the subsequentsupplycall.