Skip to main content
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"
walletType = "ethereum"
allowedExecutionModes = ["auto", "manual"]
imageUrl = "https://cdn.circuit.org/agents/default"
runtimeIntervalMinutes = 60
version = "0.0.1"

[startingAsset]
network = "ethereum:8453" # Base
address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC
minimumAmount = "1000000" # 1 USDC (6 decimals)

Example

from agent_sdk import Agent, AgentContext
from agent_sdk.agent_context import CurrentPosition
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
    usdc = next(
        (p for p in agent.currentPositions if p.assetAddress.lower() == USDC.lower()),
        None,
    )
    if not usdc or int(usdc.currentQty) == 0:
        agent.log("No USDC to deposit")
        return

    amount = int(usdc.currentQty)
    agent.log(f"Depositing {usdc.currentQty} 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.sessionWalletAddress),
            "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, positions: list[CurrentPosition]) -> 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.sessionWalletAddress),
            "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)

agent = Agent(run_function=run, unwind_function=unwind)

handler = agent.get_handler()

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

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

  1. Check positions: Finds USDC in agent.currentPositions
  2. 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.
  3. Supply: Deposits USDC into Aave V3 using the Pool’s supply function
  4. Unwind: Withdraws all USDC from Aave V3 using withdraw with max uint256

Notes

  • This agent uses signAndSend to 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 pip install eth-abi.
  • Aave V3 Pool address and USDC address shown are for Base. Adjust for other networks.
  • The approve + supply pattern 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 subsequent supply call.