AG2 (formerly AutoGen) is built around a two-agent model: one agent reasons and proposes actions, another executes them. The pattern scales naturally to tool-heavy workflows — but it ships with no blockchain tooling. If you want an AG2 agent to answer “what’s my current USDC balance?” or “is my Aave position healthy?”, you’re reaching for web3.py, RPC endpoints, ABI JSON files, and decimal scaling code before you’ve written a line of agent logic.
The shorter path: register one evmquery function with AG2. evmquery resolves ABIs automatically, evaluates a typed expression against the live chain, and returns a decoded human-readable value. Your agent gains access to every contract view function on Ethereum, Base, or BNB Chain — without any of the web3 infrastructure overhead.
TL;DR
pip install ag2 requests, define a Python function, and call autogen.register_function() to wire it into your AG2 agents. The function POSTs to https://api.evmquery.com/api/v1/query and returns live onchain data — USDC balances, ETH prices, Aave positions — from any prompt. Free tier: 2,000 credits/month.
The AG2 two-agent pattern
AG2’s core model separates reasoning from execution. An AssistantAgent (or ConversableAgent with an LLM config) receives the user message, decides which tools to call, and produces structured arguments. A UserProxyAgent intercepts those tool calls, executes the registered Python functions, and feeds the results back. The conversation loops until the assistant produces a termination signal.
This separation matters for reliability: the executor agent validates and runs tool calls in a sandboxed turn, so the reasoning agent never executes arbitrary code directly. For blockchain tooling, that means your evmquery function only runs when the LLM produces a well-formed call — not on hallucinated arguments.
What you’ll build
A single evmquery_read tool registered with both an AssistantAgent and a UserProxyAgent. Any prompt that involves live contract state — balances, prices, health factors, allowances — triggers the tool automatically. The LLM composes the right chain, contract, and CEL expression from the user’s plain-English question; evmquery executes it against the live chain.
If you’d rather query the chain interactively from Claude Desktop or Cursor without writing any code, the evmquery MCP server guide covers that. This post is for Python agents with custom business logic — scheduled monitors, portfolio dashboards, multi-step DeFi automation.
Setup
pip install ag2 requests
export OPENAI_API_KEY=sk-... # or whichever provider your AG2 config uses
export EVMQUERY_API_KEY=your_key
Get a free evmquery key at https://app.evmquery.com/onboarding?plan=free. The free tier covers 2,000 credits per month — roughly 1,000 typical contract reads.
Defining the evmquery tool
import os
from typing import Any
import requests
EVMQUERY_API = "https://api.evmquery.com/api/v1/query"
def evmquery_read(
chain: str,
contracts: dict[str, str],
expression: str,
context: dict[str, Any] | None = None,
) -> str:
"""Read live data from an EVM smart contract.
Use for current token balances, DeFi positions, oracle prices, or any
contract view function on Ethereum, Base, or BNB Smart Chain.
Supported chains: evm_ethereum, evm_base, evm_bnb_mainnet.
Do NOT use for historical data, event logs, or off-chain prices.
Args:
chain: Chain identifier — evm_ethereum, evm_base, or evm_bnb_mainnet.
contracts: Short name to 0x address mapping.
Example: {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
expression: CEL expression to evaluate. Contract names become callable variables.
Example: "formatUnits(usdc.balanceOf(wallet), usdc.decimals())"
context: Optional runtime values, e.g. wallet address or list of addresses.
"""
def _type(v: Any) -> str:
return "list<sol_address>" if isinstance(v, list) else "sol_address"
body: dict[str, Any] = {
"chain": chain,
"schema": {
"contracts": {k: {"address": v} for k, v in contracts.items()},
},
"expression": expression,
}
if context:
body["schema"]["context"] = {k: _type(v) for k, v in context.items()}
body["context"] = context
resp = requests.post(
EVMQUERY_API,
json=body,
headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
return f"{data['result']['value']} (block {data['meta']['blockNumber']})"
Three things worth unpacking:
- Contract address wrapping. The evmquery REST API expects each contract entry as
{"address": "0x..."}, not a bare string. The dict comprehension handles that conversion transparently so callers pass the simpler{"usdc": "0xA0b..."}form. - Context type inference. evmquery needs to know whether a runtime value is a single address (
sol_address) or a list (list<sol_address>). The_typehelper reads the Python value and sets the schema type automatically. - Docstring drives the LLM schema. In AG2, the function’s docstring and type annotations are what the model sees when deciding whether and how to call the tool. Keep them precise — a vague description produces vague calls.
Registering the tool with AG2 agents
import autogen
from autogen import AssistantAgent, UserProxyAgent
config_list = [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}]
assistant = AssistantAgent(
name="blockchain_assistant",
system_message=(
"You are a blockchain data assistant. When users ask about token balances, "
"DeFi positions, oracle prices, or any live contract state, call evmquery_read "
"to fetch the data. Always report the block number so the user knows the result "
"is current. Reply TERMINATE when you have fully answered the question."
),
llm_config={"config_list": config_list},
)
user_proxy = UserProxyAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=10,
is_termination_msg=lambda x: "TERMINATE" in x.get("content", ""),
code_execution_config=False,
)
autogen.register_function(
evmquery_read,
caller=assistant,
executor=user_proxy,
name="evmquery_read",
description=(
"Read live onchain data from EVM smart contracts. Use for token balances, "
"DeFi protocol positions, oracle prices, or any view function result on "
"Ethereum, Base, or BNB Smart Chain."
),
)
register_function wires the tool in both directions: the caller (assistant) learns when to propose it, and the executor (user_proxy) learns how to run it. The description here and the function’s own docstring complement each other — the description is what appears in the tool schema exposed to the model; the docstring is what the model reads in the function call context.
Which LLM config?
AG2’s config_list accepts any OpenAI-compatible endpoint. Swap gpt-4o for claude-sonnet-4-6 with an api_type: "anthropic" entry, or point it at a local Ollama instance. The evmquery tool works identically regardless of the underlying model.
Three live queries
All expressions below were validated against the live chain before publication.
ERC-20 balance check
user_proxy.initiate_chat(
assistant,
message=(
"What is the current USDC balance of "
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum?"
),
)
The assistant calls evmquery_read with:
{
"chain": "evm_ethereum",
"contracts": {"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
"expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())",
"context": {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
}
# tool returns → "120133.627066 (block 25026309)"
formatUnits reads decimals() directly from the contract, so the scaling is always correct regardless of whether the token uses 6, 8, or 18 decimal places. No hardcoding.
Live ETH/USD price from Chainlink
user_proxy.initiate_chat(
assistant,
message="What is the current ETH price in USD?",
)
The assistant calls:
{
"chain": "evm_ethereum",
"contracts": {"eth_usd": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"},
"expression": "formatUnits(eth_usd.latestAnswer(), eth_usd.decimals())",
}
# tool returns → "2377.74761831 (block 25026309)"
No context variables needed — this is a pure contract read. The address is the canonical Chainlink ETH/USD aggregator on Ethereum mainnet. The feed updates every block; there is no caching layer between the agent and the live price.
Aave v3 health factor on Base
user_proxy.initiate_chat(
assistant,
message=(
"Is my Aave v3 position on Base healthy? "
"Wallet: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
),
)
The assistant calls:
{
"chain": "evm_base",
"contracts": {"aave": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5"},
"expression": "formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)",
"context": {"wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"},
}
# tool returns → "1.157920892373162e+59 (block 45582362)"
Infinite health factor
For wallets with no active borrow, Aave returns the maximum uint256 divided by 1e18 — approximately 1.16e59. This means infinite health (no debt outstanding), not an error. Include a note about this in your system prompt so the assistant describes it correctly rather than reporting a confusing number. A wallet actively borrowing will return a value like 1.87, where liquidation risk begins below 1.0.
Multi-turn conversation
AG2’s back-and-forth conversation loop means the assistant can chain multiple tool calls in a single session without any extra orchestration code.
user_proxy.initiate_chat(
assistant,
message=(
"Give me a quick DeFi snapshot for "
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045: "
"USDC balance on Ethereum, ETH price, and Aave health on Base."
),
)
The assistant makes three sequential evmquery_read calls — one per data point — assembles the results, and replies with a consolidated summary. No manual orchestration. No loop to write. The AG2 conversation protocol handles the tool call / result / next-turn cycle automatically.
Developers building DeFi monitoring pipelines and multi-protocol dashboards can find additional expression patterns — reserve data reads, allowance checks, multi-wallet maps — in the developer resources.
AG2 tool vs MCP: picking the right surface
| AG2 tool (this post) | MCP server | |
|---|---|---|
| Language | Python | Any MCP client |
| Setup | ~50 lines including agent config | Paste one JSON config block |
| Multi-turn chaining | Native — AG2 loops until TERMINATE | Request / response only |
| Programmatic control | Full — custom retry, logging, routing | Limited to client implementation |
| Best for | Automated pipelines, scheduled jobs | Claude Desktop, Cursor, VS Code |
The AG2 approach gives you full programmatic control: custom error handling, structured logging, conditional routing between agents, and easy integration with databases or notification systems. The evmquery MCP server is the right choice when you want conversational chain queries from your AI IDE without writing any Python.
Next steps
- Set up the evmquery MCP server in Claude Desktop — no code required, same chain coverage
- Add a live EVM tool to LangChain — same REST integration, different framework
- Add a live EVM tool to CrewAI — role-based multi-agent approach
- Browse the evmquery REST API docs for the full CEL expression reference and multi-wallet batch macros