| name | aave-agentic-wallet |
| description | Use when interacting with Aave V3 protocol (supply, borrow, repay, withdraw, check positions, fetch investment opportunities) via the agentic-wallet MCP. Covers reserve APY fetching, calldata encoding, permission setup, approve+action batching, and health factor monitoring. |
Aave V3 via Agentic Wallet
Overview
Aave V3 interactions require ABI-encoded calldata passed to the agentic-wallet MCP tools. The wallet handles signing and submission — you encode the call and specify the Pool address as to.
Transaction tools:
execute_tx / execute_batch_tx — Autonomous execution using session permissions (requires grant_permissions first)
send_tx — One-time user-approved transaction (user signs via URL, no session needed)
Key constraint: ERC-20 operations (supply, repay) require a prior approve to the Pool. Always batch these atomically with execute_batch_tx.
CLI Scripts
All scripts run via:
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js <subcommand> [options]
Standalone usage (not as a plugin): run from the repository root with node dist/skills/aave-agentic-wallet/aave.js <subcommand> [options]
opportunities
Fetch and rank supply/borrow APYs across one or all Aave V3 networks.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js opportunities --network all
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js opportunities --network arbitrum
position
Show health factor, collateral, and debt for a wallet on a given network.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js position \
--wallet 0xYourWalletAddress \
--network arbitrum
grant-permissions
Print the parameters to pass to the grant_permissions MCP tool. Covers approve, supply, borrow, repay, and withdraw for a given token.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js grant-permissions \
--wallet 0xYourWalletAddress \
--network arbitrum \
--token 0xaf88d065e77c8cc2239327c5edb3a432268e5831 \
--limit 1000000000 \
--expiry-hours 24
supply
Print the parameters to pass to execute_batch_tx (approve + supply).
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js supply \
--wallet 0xYourWalletAddress \
--network arbitrum \
--asset 0xaf88d065e77c8cc2239327c5edb3a432268e5831 \
--amount 100
borrow
Print the parameters to pass to execute_tx (borrow).
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js borrow \
--wallet 0xYourWalletAddress \
--network arbitrum \
--asset 0xaf88d065e77c8cc2239327c5edb3a432268e5831 \
--amount 50
repay
Print the parameters to pass to execute_batch_tx (approve + repay). Use --amount max to repay full debt.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js repay \
--wallet 0xYourWalletAddress \
--network arbitrum \
--asset 0xaf88d065e77c8cc2239327c5edb3a432268e5831 \
--amount max
withdraw
Print the parameters to pass to execute_tx (withdraw). Use --amount max to withdraw full balance.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/aave-agentic-wallet/aave.js withdraw \
--wallet 0xYourWalletAddress \
--network arbitrum \
--asset 0xaf88d065e77c8cc2239327c5edb3a432268e5831 \
--amount max
Pool Contract Addresses (V3)
| Network | Pool Address |
|---|
| Ethereum | 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 |
| Arbitrum | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Optimism | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Polygon | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
| Base | 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5 |
| Avalanche | 0x794a61358D6845594F94dc1DB02A252b5b4814aD |
Canonical source: https://aave.com/docs/resources/addresses and the Aave Address Book
Interest rate mode: Always 2 (variable). Stable rate (1) is deprecated.
ABI Encoding (viem)
All calldata uses encodeFunctionData from viem:
import { encodeFunctionData, parseUnits, maxUint256 } from "viem";
const POOL = "0x794a61358D6845594F94dc1DB02A252b5b4814aD";
const USDC = "0xaf88d065e77c8cc2239327c5edb3a432268e5831";
const userAddress = "0xYourWalletAddress";
const amount = parseUnits("100", 6);
Core Operations
Supply (deposit collateral)
Requires: approve(Pool, amount) + supply(asset, amount, onBehalfOf, 0)
const approveTx = {
to: USDC,
data: encodeFunctionData({
abi: [{ name: "approve", type: "function", inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" }
], outputs: [{ type: "bool" }] }],
functionName: "approve",
args: [POOL, amount],
}),
value: "0x0",
};
const supplyTx = {
to: POOL,
data: encodeFunctionData({
abi: [{ name: "supply", type: "function", inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "onBehalfOf", type: "address" },
{ name: "referralCode", type: "uint16" }
], outputs: [] }],
functionName: "supply",
args: [USDC, amount, userAddress, 0],
}),
value: "0x0",
};
await execute_batch_tx({ sessionId: "<from list_wallets>", calls: [approveTx, supplyTx], feeToken: USDC });
Borrow
No prior approval needed. Pool pulls nothing — it sends tokens to you.
const borrowTx = {
to: POOL,
data: encodeFunctionData({
abi: [{ name: "borrow", type: "function", inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "interestRateMode", type: "uint256" },
{ name: "referralCode", type: "uint16" },
{ name: "onBehalfOf", type: "address" }
], outputs: [] }],
functionName: "borrow",
args: [USDC, amount, 2n, 0, userAddress],
}),
value: "0x0",
};
await execute_tx({ sessionId: "<from list_wallets>", to: POOL, data: borrowTx.data, value: "0x0", feeToken: USDC });
Repay
Requires: approve(Pool, amount) + repay(asset, amount, 2, onBehalfOf)
Use maxUint256 as amount to repay the full debt:
const repayAmount = maxUint256;
const approveTx = {
to: USDC,
data: encodeFunctionData({
abi: [{ name: "approve", type: "function", inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" }
], outputs: [{ type: "bool" }] }],
functionName: "approve",
args: [POOL, repayAmount],
}),
value: "0x0",
};
const repayTx = {
to: POOL,
data: encodeFunctionData({
abi: [{ name: "repay", type: "function", inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "interestRateMode", type: "uint256" },
{ name: "onBehalfOf", type: "address" }
], outputs: [{ name: "", type: "uint256" }] }],
functionName: "repay",
args: [USDC, repayAmount, 2n, userAddress],
}),
value: "0x0",
};
await execute_batch_tx({ sessionId: "<from list_wallets>", calls: [approveTx, repayTx], feeToken: USDC });
Withdraw
No approval needed. Use maxUint256 to withdraw entire balance.
const withdrawTx = {
to: POOL,
data: encodeFunctionData({
abi: [{ name: "withdraw", type: "function", inputs: [
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "to", type: "address" }
], outputs: [{ name: "", type: "uint256" }] }],
functionName: "withdraw",
args: [USDC, maxUint256, userAddress],
}),
value: "0x0",
};
await execute_tx({ sessionId: "<from list_wallets>", to: POOL, data: withdrawTx.data, value: "0x0", feeToken: USDC });
Fetching Investment Opportunities
Use UiPoolDataProvider.getReservesData() — a single read call returning all reserves with live APYs, liquidity, and collateral parameters. No wallet or transaction needed.
Key Contracts
| Network | UiPoolDataProvider | PoolAddressesProvider |
|---|
| Ethereum | 0x56b7A1012765C285afAC8b8F25C69Bf10ccfE978 | 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e |
| Arbitrum | 0x145dE30c929a065582da84Cf96F88460dB9C4b9C | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
| Optimism | 0xbd83DdBE37fc91923d59C8c1E0bDe0CccCa332d5 | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
| Base | 0x5d4D4007A4c6336550DdAa2a7c0d5e7972eebd16 | 0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64B |
| Polygon | 0xC69728f11E9E6127733751c8410432913123acf1 | 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb |
Canonical source: Aave Address Book
Example: Fetch & Rank Opportunities
import { createPublicClient, http, formatUnits } from "viem";
import { arbitrum } from "viem/chains";
const UI_POOL_DATA_PROVIDER = "0x145dE30c929a065582da84Cf96F88460dB9C4b9C";
const POOL_ADDRESSES_PROVIDER = "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb";
const RAY = 1e27;
const SECONDS_PER_YEAR = 31536000;
const client = createPublicClient({ chain: arbitrum, transport: http() });
const [reserves] = await client.readContract({
address: UI_POOL_DATA_PROVIDER,
abi: [{
name: "getReservesData",
type: "function",
stateMutability: "view",
inputs: [{ name: "provider", type: "address" }],
outputs: [
{
name: "",
type: "tuple[]",
components: [
{ name: "underlyingAsset", type: "address" },
{ name: "name", type: "string" },
{ name: "symbol", type: "string" },
{ name: "decimals", type: "uint256" },
{ name: "baseLTVasCollateral", type: "uint256" },
{ name: "reserveLiquidationThreshold", type: "uint256" },
{ name: "reserveLiquidationBonus", type: "uint256" },
{ name: "reserveFactor", type: "uint256" },
{ name: "usageAsCollateralEnabled", type: "bool" },
{ name: "borrowingEnabled", type: "bool" },
{ name: "isActive", type: "bool" },
{ name: "isFrozen", type: "bool" },
{ name: "liquidityIndex", type: "uint128" },
{ name: "variableBorrowIndex", type: "uint128" },
{ name: "liquidityRate", type: "uint128" },
{ name: "variableBorrowRate", type: "uint128" },
{ name: "availableLiquidity", type: "uint256" },
{ name: "totalScaledVariableDebt", type: "uint256" },
{ name: "priceInMarketReferenceCurrency", type: "uint256" },
{ name: "priceOracle", type: "address" },
{ name: "variableRateSlope1", type: "uint256" },
{ name: "variableRateSlope2", type: "uint256" },
{ name: "baseVariableBorrowRate", type: "uint256" },
{ name: "optimalUsageRatio", type: "uint256" },
{ name: "isPaused", type: "bool" },
{ name: "isSiloedBorrowing", type: "bool" },
{ name: "accruedToTreasury", type: "uint256" },
{ name: "unbacked", type: "uint256" },
{ name: "isolationModeTotalDebt", type: "uint256" },
{ name: "debtCeilingDecimals", type: "uint256" },
{ name: "debtCeiling", type: "uint256" },
{ name: "borrowCap", type: "uint256" },
{ name: "supplyCap", type: "uint256" },
{ name: "eModeCategoryId", type: "uint8" },
{ name: "borrowableInIsolation", type: "bool" },
{ name: "flashLoanEnabled", type: "bool" },
],
},
{ name: "", type: "tuple", components: [] },
],
}],
functionName: "getReservesData",
args: [POOL_ADDRESSES_PROVIDER],
});
const rayToApy = (rate: bigint) => (Number(rate) / RAY) * 100;
const opportunities = reserves
.filter(r => r.isActive && !r.isFrozen && !r.isPaused)
.map(r => ({
symbol: r.symbol,
asset: r.underlyingAsset,
supplyApy: rayToApy(r.liquidityRate).toFixed(2) + "%",
borrowApy: rayToApy(r.variableBorrowRate).toFixed(2) + "%",
availableLiquidity: formatUnits(r.availableLiquidity, Number(r.decimals)),
canBeCollateral: r.usageAsCollateralEnabled,
ltv: (Number(r.baseLTVasCollateral) / 100).toFixed(0) + "%",
liquidationThreshold: (Number(r.reserveLiquidationThreshold) / 100).toFixed(0) + "%",
}))
.sort((a, b) => parseFloat(b.supplyApy) - parseFloat(a.supplyApy));
console.table(opportunities);
How to Present to the User
After fetching, summarize as a ranked list:
Top Supply Opportunities on Arbitrum:
1. USDC — Supply APY: 8.2% | Borrow APY: 10.1% | Collateral: Yes (LTV 77%)
2. USDT — Supply APY: 7.9% | Borrow APY: 9.8% | Collateral: No
3. WETH — Supply APY: 2.1% | Borrow APY: 2.9% | Collateral: Yes (LTV 80%)
...
Then ask the user which asset they want to supply and how much.
APY Note
liquidityRate is in ray units (1e27). A simple linear approximation (rate / 1e27 * 100) is accurate enough for display. For compound APY: (1 + rate/RAY/SECONDS_PER_YEAR)^SECONDS_PER_YEAR - 1.
Checking Position Health
Use getUserAccountData to read position before/after operations:
import { createPublicClient, http } from "viem";
import { arbitrum } from "viem/chains";
const client = createPublicClient({ chain: arbitrum, transport: http() });
const [
totalCollateralBase,
totalDebtBase,
availableBorrowsBase,
currentLiquidationThreshold,
ltv,
healthFactor,
] = await client.readContract({
address: POOL,
abi: [{ name: "getUserAccountData", type: "function", stateMutability: "view",
inputs: [{ name: "user", type: "address" }],
outputs: [
{ name: "totalCollateralBase", type: "uint256" },
{ name: "totalDebtBase", type: "uint256" },
{ name: "availableBorrowsBase", type: "uint256" },
{ name: "currentLiquidationThreshold", type: "uint256" },
{ name: "ltv", type: "uint256" },
{ name: "healthFactor", type: "uint256" },
]
}],
functionName: "getUserAccountData",
args: [userAddress],
});
const hfFormatted = Number(healthFactor) / 1e18;
Permissions Setup
Request permissions that cover the operations you need. Always include the fee token in spend permissions.
await grant_permissions({
chainId: 42161,
description: "Aave V3 operations on Arbitrum",
calls: [
{ to: USDC, signature: "approve(address,uint256)", label: "Approve USDC" },
{ to: POOL, signature: "supply(address,uint256,address,uint16)", label: "Supply to Aave" },
{ to: POOL, signature: "borrow(address,uint256,uint256,uint16,address)", label: "Borrow from Aave" },
{ to: POOL, signature: "repay(address,uint256,uint256,address)", label: "Repay Aave debt" },
{ to: POOL, signature: "withdraw(address,uint256,address)", label: "Withdraw from Aave" },
],
spend: [
{ token: USDC, limit: "1000000000", period: "day", label: "USDC spend" },
],
expiry: 86400,
});
Workflow
- Check wallet →
list_wallets (no params) — returns wallet address and all sessions with sessionId, description, chainId, permissions
- Create wallet →
create_wallet() — chainId and description are required when granting permissions at creation
- Discover fee tokens →
supported_gas_tokens({ chainId }) — chainId is required (get it from supported_chains)
- Grant permissions →
grant_permissions({ chainId, description, calls, spend, expiry }) — creates a new session. Include Pool calls + token spend limits (include fee token in spend). Suggest a description and confirm with the user before calling.
- Select session → use
list_wallets to find the right sessionId based on description/permissions for the operation
- Check permissions →
check_permissions({ sessionId, to, feeToken }) before sending — data and value are optional
- Encode calldata → use viem
encodeFunctionData
- Send →
execute_batch_tx({ sessionId, calls, feeToken }) for approve+action, execute_tx({ sessionId, to, feeToken }) for borrow/withdraw
- Verify position → call
getUserAccountData on-chain
- Revoke session (optional) →
revoke_session({ sessionId }) — revokes a specific session, others remain active
Common Mistakes
| Mistake | Fix |
|---|
Forgetting approve before supply/repay | Always batch approve+action with execute_batch_tx |
Using interest rate mode 1 (stable) | Always use 2 (variable); stable is deprecated |
healthFactor < 1e18 after borrow | Check availableBorrowsBase before borrowing |
| Wrong token decimals | USDC/USDT = 6, WETH/WBTC = 18/8 — use parseUnits |
| Insufficient spend permission | Include both the asset AND the fee token in spend permissions |
Not scoping Pool address in calls | Specify to: POOL to keep permissions minimal |