If you built a monitor on Aave V3’s getUserAccountData, it will not see a V4 position. V4 went live on Ethereum mainnet on March 30, 2026 with a different structure: liquidity sits in hubs, borrowing happens in spokes, and every spoke reports its own health factor for a wallet. The number still means what it always did, but where you read it and how liquidation treats it have changed.
TL;DR
In Aave V4 the health factor is read per spoke with spoke.getUserAccountData(user), returned in WAD (1e18 is 1.00). Liquidation no longer uses a fixed close factor: it restores the position to a spoke-level target health factor and pays a bonus that grows as the health factor falls.
What changed from V3 to V4
V3 is one pool per chain, and one getUserAccountData call on that pool covers a wallet. V4 splits the system in two:
- Hubs hold the liquidity and do the system-wide accounting. Ethereum launched with three hubs, Core, Prime and Plus, each with its own risk posture and conservative supply and borrow caps (The Block).
- Spokes are the markets users interact with. Each spoke draws credit from a hub and keeps its own reserves, collateral factors and liquidation configuration, as described in the Aave V4 docs.
The practical effect for a reader: a wallet has one position per spoke, not one per chain. The same wallet can be healthy in one spoke and close to liquidation in another, and there is no single chain-wide health factor to read.
| Aave V3 | Aave V4 | |
|---|---|---|
| Where you read it | Pool.getUserAccountData(user) |
Spoke.getUserAccountData(user), once per spoke |
| Health factor scale | 1e18 is 1.00 |
1e18 is 1.00 |
| Collateral and debt | Base currency, 8 decimals | Value units scaled 1e26 per USD, debt also scaled by RAY |
| Close factor | Fixed percentage of the debt | Replaced by a target health factor per spoke |
| Liquidation bonus | Fixed per asset | Scales with the health factor, between a minimum and a maximum |
| Risk premium | None | riskPremium per position, in BPS |
The V4 getUserAccountData struct
The Aave V4 source (ISpoke.sol) defines the return value as a struct with seven fields:
| Field | Meaning | Scale |
|---|---|---|
riskPremium |
Premium of the position, driven by its collateral mix | BPS |
avgCollateralFactor |
Weighted average collateral factor | WAD |
healthFactor |
Eligible collateral value divided by debt value | WAD, 1e18 is 1.00 |
totalCollateralValue |
Total collateral value | Value units |
totalDebtValueRay |
Total debt value | Value units times RAY |
activeCollateralCount |
Reserves counted as collateral | integer |
borrowCount |
Reserves borrowed | integer |
As in V3, a wallet with no debt does not return an error. It returns healthFactor as the maximum uint256, so treat any value above a sane ceiling as “no debt” instead of formatting it as a number.
Liquidation in V4
The V4 liquidation design has three changes that matter when you set alert thresholds:
- Target health factor. A liquidator repays enough debt to bring the position to the spoke’s target health factor, instead of a fixed share of the debt.
- Variable bonus. The bonus scales with the health factor. At or below
healthFactorForMaxBonusthe liquidator receives the maximum bonus. Above it, the bonus falls linearly toward a minimum, which ismaxLiquidationBonusscaled byliquidationBonusFactor. Aave’s docs call this a Dutch auction: the bonus grows as the health factor falls, not as time passes. - Dust rule. A liquidation must clear all the debt, take all the collateral, or leave at least 1,000 USD of both (
DUST_LIQUIDATION_THRESHOLD), so tiny residual positions cannot accumulate.
Liquidation starts when the health factor drops below 1.00 (HEALTH_FACTOR_LIQUIDATION_THRESHOLD is 1e18). The configuration lives on each spoke and is readable.
Query a V4 position with evmquery
The official addresses come from the Aave Address Book (AaveV4Ethereum.sol). On Ethereum the Main spoke is 0x94e7A5dCbE816e498b89aB752661904E2F56c485. This reads a wallet that has an open borrow on it:
curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "spoke": { "address": "0x94e7A5dCbE816e498b89aB752661904E2F56c485" } }, "context": { "user": "sol_address" } }, "context": { "user": "0xc1cdaa40c85d37b354bf8a016c90265241df48b6" }, "expression": "cel.bind(a, spoke.getUserAccountData(user), {\"healthFactor\": formatUnits(a.healthFactor, 18), \"avgCollateralFactor\": formatUnits(a.avgCollateralFactor, 18)})" }' | python3 -m json.tool{ "result": { "value": { "healthFactor": 1.3071957211202516, "avgCollateralFactor": 0.83 }, "type": "map<string, double>" }, "meta": { "blockNumber": "26100192", "totalCalls": 1, "totalRounds": 1 }}cel.bind calls getUserAccountData once and reuses the struct, so this stays a single eth_call. The health factor drifts block to block as interest accrues and prices move, so your own run will differ slightly. Both fields are WAD, so formatUnits(..., 18) is correct for them. Do not format totalCollateralValue or totalDebtValueRay that way, they use the Value and RAY scales from the table above.
A CEL map needs every value to share a type, which is why the query returns the two WAD fields and leaves the integer counts to a separate call. See the Aave V3 version for the same constraint on the V3 struct.
Read the liquidation config
The thresholds that decide how a spoke liquidates are one call away:
curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "spoke": { "address": "0x94e7A5dCbE816e498b89aB752661904E2F56c485" } } }, "expression": "{\"targetHealthFactor\": formatUnits(spoke.getLiquidationConfig().targetHealthFactor, 18), \"healthFactorForMaxBonus\": formatUnits(spoke.getLiquidationConfig().healthFactorForMaxBonus, 18)}" }' | python3 -m json.tool{ "result": { "value": { "targetHealthFactor": 1.24, "healthFactorForMaxBonus": 0.9 }, "type": "map<string, double>" }, "meta": { "blockNumber": "26100192", "totalCalls": 1, "totalRounds": 1 }}On the Main spoke a liquidation restores a position to a health factor of 1.24, and the bonus reaches its maximum at 0.90 or below. Each spoke carries its own config, so read it per spoke instead of assuming these values.
Which chains have V4
At the time of writing, the Aave Address Book lists V4 deployments on Ethereum, Base, Avalanche and Arc. Of the chains evmquery supports, that means Ethereum and Base. The Base deployment is a separate, smaller market, and a spoke there answers the same getUserAccountData call (a read of the Mag7 spoke at 0x17905Db0e4A3514467539956c084180616AE7B8D on evm_base returns a reserve count of 8). Polygon and BNB Chain have no V4 deployment in the address book yet, so V3 remains the market there. Check the address book again before you rely on a chain, since new spokes and chains are added through governance.
Monitor a wallet across spokes
Because the position is per spoke, a complete check reads every spoke where the wallet could hold a position, using the spoke addresses from the address book. They fit in one request as separate contracts in the schema. Here the same wallet is read on the Main spoke and the Bluechip spoke:
curl -s -X POST https://api.evmquery.com/api/v1/query \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "main": { "address": "0x94e7A5dCbE816e498b89aB752661904E2F56c485" }, "bluechip": { "address": "0x973a023A77420ba610f06b3858aD991Df6d85A08" } }, "context": { "user": "sol_address" } }, "context": { "user": "0xc1cdaa40c85d37b354bf8a016c90265241df48b6" }, "expression": "{\"main\": formatUnits(main.getUserAccountData(user).healthFactor, 18), \"bluechip\": formatUnits(bluechip.getUserAccountData(user).healthFactor, 18)}" }' | python3 -m json.tool{ "result": { "value": { "main": 1.307195624890776, "bluechip": 1.157920892373162e+59 }, "type": "map<string, double>" }, "meta": { "blockNumber": "26100201", "totalCalls": 2, "totalRounds": 1 }}Two eth_calls, one execution round. The Bluechip value of about 1.16e59 is the no-debt case: this wallet holds no borrow there, so the health factor is the maximum uint256. Filter that out before you alert. See Multicall3: batch EVM contract reads for how the batching works. For V3 positions, the Aave Health Factor tool reads a wallet without any code.
Next steps
- Aave V3 Health Factor Explained: the six V3 fields and their three decimal scales
- Aave Health Factor tool: check a wallet’s V3 health factor without writing a query
- Multicall3: batch EVM contract reads: read several spokes in one request
- evmquery for developers: what else the API can read besides lending positions



