| name | spark-agentic-wallet |
| description | Use when interacting with SparkLend 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. |
SparkLend via Agentic Wallet
Overview
SparkLend is a permissionless, non-custodial lending protocol (an Aave V3 fork) focused on DAI/USDS and stablecoin lending. It borrows from Sky's stablecoin reserves to deploy capital across DeFi. Deployed on Ethereum and Gnosis.
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/spark-agentic-wallet/spark.js <subcommand> [options]
Standalone usage (not as a plugin): run from the repository root with node dist/skills/spark-agentic-wallet/spark.js <subcommand> [options]
opportunities
Fetch and rank supply/borrow APYs across one or all SparkLend networks.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/spark-agentic-wallet/spark.js opportunities --network all
node ${CLAUDE_PLUGIN_DATA}/dist/skills/spark-agentic-wallet/spark.js opportunities --network ethereum
position
Show health factor, collateral, and debt for a wallet on a given network.
node ${CLAUDE_PLUGIN_DATA}/dist/skills/spark-agentic-wallet/spark.js position \
--wallet 0xYourWalletAddress \
--network ethereum
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/spark-agentic-wallet/spark.js grant-permissions \
--wallet 0xYourWalletAddress \
--network ethereum \
--token 0x6B175474E89094C44Da98b954EedeAC495271d0F \
--limit 1000000000000000000000 \
--expiry-hours 24
supply
Print the parameters to pass to execute_batch_tx (approve + supply).
node ${CLAUDE_PLUGIN_DATA}/dist/skills/spark-agentic-wallet/spark.js supply \
--wallet 0xYourWalletAddress \
--network ethereum \
--asset 0x6B175474E89094C44Da98b954EedeAC495271d0F \
--amount 1000
borrow
Print the parameters to pass to execute_tx (borrow).
node ${CLAUDE_PLUGIN_DATA}/dist/skills/spark-agentic-wallet/spark.js borrow \
--wallet 0xYourWalletAddress \
--network ethereum \
--asset 0x6B175474E89094C44Da98b954EedeAC495271d0F \
--amount 500
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/spark-agentic-wallet/spark.js repay \
--wallet 0xYourWalletAddress \
--network ethereum \
--asset 0x6B175474E89094C44Da98b954EedeAC495271d0F \
--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/spark-agentic-wallet/spark.js withdraw \
--wallet 0xYourWalletAddress \
--network ethereum \
--asset 0x6B175474E89094C44Da98b954EedeAC495271d0F \
--amount max
Pool Contract Addresses
| Network | Pool Address |
|---|
| Ethereum | 0xC13e21B648A5Ee794902342038FF3aDAB66BE987 |
| Gnosis | 0x2Dae5307c5E3FD1CF5A72Cb6F698f915860607e0 |
Canonical source: Spark Address Registry and Spark Docs
Interest rate mode: Always 2 (variable). Stable rate (1) is deprecated.
ABI Encoding (viem)
SparkLend is an Aave V3 fork — all function signatures are identical.
All calldata uses encodeFunctionData from viem:
import { encodeFunctionData, parseUnits, maxUint256 } from "viem";
const POOL = "0xC13e21B648A5Ee794902342038FF3aDAB66BE987";
const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
const userAddress = "0xYourWalletAddress";
const amount = parseUnits("1000", 18);
Core Operations
Supply (deposit collateral)
Requires: approve(Pool, amount) + supply(asset, amount, onBehalfOf, 0)
const approveTx = {
to: DAI,
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: [DAI, amount, userAddress, 0],
}),
value: "0x0",
};
await execute_batch_tx({ sessionId: "<from list_wallets>", calls: [approveTx, supplyTx], feeToken: DAI });
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: [DAI, amount, 2n, 0, userAddress],
}),
value: "0x0",
};
await execute_tx({ sessionId: "<from list_wallets>", to: POOL, data: borrowTx.data, value: "0x0", feeToken: DAI });
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: DAI,
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: [DAI, repayAmount, 2n, userAddress],
}),
value: "0x0",
};
await execute_batch_tx({ sessionId: "<from list_wallets>", calls: [approveTx, repayTx], feeToken: DAI });
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: [DAI, maxUint256, userAddress],
}),
value: "0x0",
};
await execute_tx({ sessionId: "<from list_wallets>", to: POOL, data: withdrawTx.data, value: "0x0", feeToken: DAI });
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 | 0xF028c2F4b19898718fD0F77b9b881CbfdAa5e8Bb | 0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE |
| Gnosis | 0xF028c2F4b19898718fD0F77b9b881CbfdAa5e8Bb | 0xA98DaCB3fC964A6A0d2ce3B77294241585EAbA6d |
Canonical source: Spark Address Registry
APY Note
liquidityRate is in ray units (1e27). A simple linear approximation (rate / 1e27 * 100) is accurate enough for display.
Checking Position Health
Use getUserAccountData to read position before/after operations:
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, 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: 1,
description: "SparkLend operations on Ethereum",
calls: [
{ to: DAI, signature: "approve(address,uint256)", label: "Approve DAI" },
{ to: POOL, signature: "supply(address,uint256,address,uint16)", label: "Supply to SparkLend" },
{ to: POOL, signature: "borrow(address,uint256,uint256,uint16,address)", label: "Borrow from SparkLend" },
{ to: POOL, signature: "repay(address,uint256,uint256,address)", label: "Repay SparkLend debt" },
{ to: POOL, signature: "withdraw(address,uint256,address)", label: "Withdraw from SparkLend" },
],
spend: [
{ token: DAI, limit: "1000000000000000000000", period: "day", label: "DAI 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 | DAI = 18, 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 |
| Using Aave Pool address instead of SparkLend | SparkLend has its own Pool contracts — do not mix with Aave addresses |