Google ADK agents reason fluently over DeFi protocols, but without a live data feed they cannot tell you your current USDC balance, whether your Aave position is healthy, or how much of a token is in circulation right now. That information did not exist when the model was trained. Wiring evmquery in as a FunctionTool closes this gap in one Python function.
TL;DR
Google ADK (Google’s open-source agent framework) turns any Python function into a tool the agent can call. Wrapping evmquery’s REST API in one function gives your agent live read access to any ERC-20, Aave position, or onchain metric on Ethereum, Base, and BNB Smart Chain — no RPC node, no ABI management, no per-contract boilerplate. Grab a free key and the agent is answering live DeFi questions in under 20 lines.
What is Google ADK?
Google’s Agent Development Kit (ADK) is an open-source Python framework released in April 2025 for building production-grade AI agents. Where older orchestration libraries lean heavily on chaining abstractions, ADK centers the design around Agent objects that hold a model, a system instruction, and a list of tools. The framework handles tool schema generation, multi-turn memory, and multi-agent orchestration out of the box.
The practical win for developers is the near-zero boilerplate for tools. Pass a plain Python function in tools=[] and ADK inspects the function’s signature, type hints, and docstring to generate the JSON schema the LLM uses to decide when and how to call it. No decorator, no manual schema, no wrapper class — if the function is well-typed and the docstring is clear, the agent handles the rest.
Why an ADK agent needs a live chain feed
A Gemini-powered ADK agent already knows how Aave v3 works, what ERC-20 tokens are, and what a health factor represents. What it cannot know is the current state of the chain — your balance, your borrow position, the total supply of a token minted an hour ago.
The standard fix is a tool call. Every time the agent decides it needs current data, it calls the tool, gets a number back, and folds that into its response. The problem with rolling this yourself is the plumbing: an RPC connection, the contract ABI, proxy resolution for EIP-1967 proxies, typed return value decoding, and error handling for each contract variant.
evmquery handles all of that. Its REST API accepts a chain identifier, a map of named contracts, and a CEL expression. It resolves ABIs automatically, unwraps proxies, and returns a typed result. The only thing your ADK tool needs to do is send the HTTP request and return the string.
The evmquery FunctionTool
Create tools.py:
import json
import os
import requests
def read_evm_contract(
chain: str,
contracts_json: str,
expression: str,
wallet_address: str = "",
) -> str:
"""Execute a CEL expression against named EVM smart contracts.
Args:
chain: Chain to query. One of: evm_ethereum, evm_base, evm_bnb_mainnet.
contracts_json: JSON object mapping alias names to addresses, e.g.
'{"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}'.
expression: CEL expression to evaluate, e.g.
'formatUnits(usdc.balanceOf(wallet), usdc.decimals())'.
wallet_address: Optional wallet address injected as the 'wallet'
variable in the expression. Omit for expressions that do not
reference a specific wallet.
"""
contracts = json.loads(contracts_json)
payload: dict = {
"chain": chain,
"schema": {"contracts": contracts},
"expression": expression,
}
if wallet_address:
payload["schema"]["context"] = {"wallet": "sol_address"}
payload["context"] = {"wallet": wallet_address}
resp = requests.post(
"https://api.evmquery.com/api/v1/query",
headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]},
json=payload,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
return str(data.get("result", data))
A few notes on the design:
contracts_jsonis a string rather thandictbecause ADK serializes all tool inputs to JSON-compatible primitives when the LLM generates the call. A string makes that boundary explicit and avoids schema ambiguity.schema.contextdeclareswalletas typesol_address. This is required so evmquery knows how to type-check the variable in the CEL expression. The runtime value goes in the separate top-levelcontextfield.- The function returns a plain string. ADK inserts it directly into the conversation, so a short scalar beats nested JSON for the model’s reasoning loop.
Building the agent
Create agent.py alongside tools.py:
from google.adk.agents import Agent
from tools import read_evm_contract
root_agent = Agent(
name="defi_assistant",
model="gemini-2.0-flash",
instruction="""You are a DeFi data assistant with live access to EVM blockchains.
Use read_evm_contract to answer questions about token balances, DeFi positions,
and on-chain state. Supported chains: evm_ethereum, evm_base, evm_bnb_mainnet.
Common expression patterns:
ERC-20 balance: formatUnits(token.balanceOf(wallet), token.decimals())
Aave health factor: formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)
Total supply: formatUnits(token.totalSupply(), token.decimals())
Pass contracts_json as a JSON string. For expressions that reference a wallet
variable, supply the address in wallet_address. Leave wallet_address empty for
expressions that do not use it.""",
tools=[read_evm_contract],
)
The system instruction doubles as a CEL cheat sheet. The model does not need to know the expression language in depth; it needs a few canonical patterns to copy and adapt for each question.
Store credentials in a .env file in the same directory:
GOOGLE_API_KEY=your_gemini_api_key
EVMQUERY_API_KEY=your_evmquery_key
Your project layout is now:
defi_assistant/
├── __init__.py
├── agent.py
├── tools.py
└── .env
Launch a browser playground with one command:
pip install google-adk requests python-dotenv
adk web defi_assistant/
Or run in the terminal:
adk run defi_assistant/
Three live queries
Here are three prompts, what the agent issues under the hood, and the live results validated against the chain at writing time.
USDC balance on Ethereum
Prompt: “What is the USDC balance of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 on Ethereum?”
The agent calls read_evm_contract with:
{
"chain": "evm_ethereum",
"contracts_json": "{\"usdc\": \"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\"}",
"expression": "formatUnits(usdc.balanceOf(wallet), usdc.decimals())",
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}
Live result at block 24,963,966: 5,567.40 USDC.
Aave v3 health factor on Base
Prompt: “Is my Aave v3 position on Base healthy? Wallet: 0xd8dA…6045.”
{
"chain": "evm_base",
"contracts_json": "{\"aave\": \"0xA238Dd80C259a72e81d7e4664a9801593F98d1c5\"}",
"expression": "formatUnits(aave.getUserAccountData(wallet).healthFactor, 18)",
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}
Live result: 1.157 x 10^59.
Aave infinite health factor
A health factor of roughly 1.16e59 means the wallet has no active borrow on that Aave pool. Aave returns type(uint256).max as the health factor when debt is zero; dividing by 1e18 produces this large float. Add a note in the system instruction so the agent explains this to non-technical users rather than surfacing the raw number.
USDC total supply on Base
Prompt: “How much USDC is in circulation on Base right now?”
{
"chain": "evm_base",
"contracts_json": "{\"usdc\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\"}",
"expression": "formatUnits(usdc.totalSupply(), usdc.decimals())"
}
Live result: ~4.46 billion USDC on Base at time of writing.
wallet_address is omitted entirely here. The expression has no wallet variable, so the agent leaves that field out. The instruction’s annotation — “Leave wallet_address empty for expressions that do not use it” — is enough for Gemini to handle this correctly without explicit branching logic in the tool.
New to evmquery? The free tier gives you 2,000 credits per month, which covers hundreds of balance reads or dozens of multi-contract queries.
Querying multiple contracts in one call
evmquery evaluates expressions across multiple contracts in a single round trip. To read both WETH and USDC balances together, the agent issues:
{
"chain": "evm_ethereum",
"contracts_json": "{\"weth\": \"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2\", \"usdc\": \"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\"}",
"expression": "[formatUnits(weth.balanceOf(wallet), 18), formatUnits(usdc.balanceOf(wallet), usdc.decimals())]",
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}
This returns a list [WETH, USDC] in a single API call rather than two sequential tool invocations. For agents that need to reason over a full portfolio, batching like this keeps latency low and credit cost down.
To extend to multi-wallet scans, declare the variable as list<sol_address> in schema.context, pass an array in context.wallet, and use the CEL map macro:
wallets.map(w, formatUnits(token.balanceOf(w), token.decimals()))
The [wallet_address parameter in the tool would need to accept a list in that case — see the ERC-20 balance scanner guide for the full multi-wallet pattern.
Credential hygiene
The EVMQUERY_API_KEY should never appear in the agent instruction or in any tool return value.
- Store keys in
.envand load them withpython-dotenvfor local dev. - Do not pass the key to
Agent— the tool reads it from the environment at call time, so the LLM never sees it. - evmquery API keys are read-only. They can query state but cannot sign transactions or mutate the chain. Still, rotate them if they leak.
For production deployments on Google Cloud’s Agent Engine, use Secret Manager references in your Cloud Run environment rather than plaintext .env files.
Next steps
- See the /for/ai-users page for more patterns built around LLM-first stacks, including MCP and multi-agent setups.
- The same
read_evm_contractfunction ports directly to other frameworks: the LangChain integration guide and OpenAI Agents SDK guide show the identical pattern with different runners. - Want scheduled monitoring rather than on-demand reads? The blockchain monitoring with Python guide covers polling loops and alert thresholds.
- Browse the full expression language and supported contract methods in the REST API reference.