Aave V3 gives you one number: healthFactor. Below 1.0, a position is liquidatable; above it, you’re safe by however much margin. Compound V3 (Comet) has no equivalent field. There is no getUserAccountData, no single uint256 you can compare against a threshold. Instead Comet exposes two booleans, isBorrowCollateralized and isLiquidatable, and expects you to compute the actual margin yourself from collateral factors and price feeds if you want more than a yes/no answer.
TL;DR
Compound V3 has no healthFactor. Read borrowBalanceOf and userCollateral for the raw position, isBorrowCollateralized/isLiquidatable for the boolean risk check, and combine getAssetInfoByAddress’s collateral factors with getPrice if you need an Aave-style ratio instead of a yes/no.
Why Comet doesn’t have a health factor
Compound V3’s core design change from V2 is that each deployed market, called a Comet instance, borrows exactly one base asset. The Ethereum mainnet deployment alone runs six separate Comet contracts today — USDC, USDS, USDT, WBTC, WETH, and wstETH markets — each with its own address, its own collateral list, and its own risk parameters. Base runs five (USDC, USDbC, USDS, WETH, AERO), Polygon runs two (USDC, USDT).
Because a wallet’s position lives entirely inside one Comet contract (unlike Aave’s single pool aggregating every asset), Compound’s Solidity doesn’t need to compute a portfolio-wide ratio to answer “can this account be liquidated.” It just checks collateral value against the borrowed base asset directly, and exposes that check as a boolean rather than a ratio. That’s a reasonable design choice for the protocol’s own liquidation bot; it’s mildly annoying if you’re building a risk dashboard and want a number, not a boolean.
Reading the raw position: balance and collateral
Every Comet market exposes borrowBalanceOf(account) for the base-asset debt and userCollateral(account, asset) for how much of a given collateral asset the account has deposited. Both need decimals() to format correctly — the base asset’s decimals for the debt (6 for USDC, 18 for WETH), always 18 for userCollateral since Comet stores raw collateral balances in wei regardless of the token.
import osimport requests
COMET_USDC = "0xc3d688B66703497DAA19211EEdff47f25384cdc3" # Compound V3 USDC market, EthereumWETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"comet": {"address": COMET_USDC}}, "context": {"wallet": "sol_address"}, }, "context": {"wallet": "0xYourWalletAddress"}, "expression": ( f'cel.bind(weth, solAddress("{WETH}"), {{' ' "borrowBalanceUSDC": dyn(formatUnits(comet.borrowBalanceOf(wallet), comet.decimals())),' ' "wethCollateral": dyn(formatUnits(comet.userCollateral(wallet, weth).balance, 18)),' ' "isCollateralized": dyn(comet.isBorrowCollateralized(wallet)),' ' "isLiquidatable": dyn(comet.isLiquidatable(wallet))' " })" ), }, timeout=10,)resp.raise_for_status()print(resp.json()["result"]["value"])Run live against a wallet holding no Compound position, this returns:
{ "borrowBalanceUSDC": 0, "wethCollateral": 0, "isCollateralized": true, "isLiquidatable": false}That’s the real response, four eth_calls batched into one round trip, validated against the live USDC Comet market at block 25923009. A wallet with zero debt is trivially collateralized and never liquidatable — the same “no debt, not an error” case the Aave health factor guide documents, just returned as true/false here instead of a 59-digit number.
The two risk checks: isBorrowCollateralized and isLiquidatable
These aren’t the same check. isBorrowCollateralized looks at the account’s current borrow against its collateral using the borrow collateral factor — the conservative threshold Comet enforces when you try to open or increase a borrow. isLiquidatable uses the separate, looser liquidate collateral factor — the threshold at which the position actually becomes eligible for liquidation. A position can fail the first check (you couldn’t borrow more right now) while still passing the second (you’re not liquidatable yet). That gap is Comet’s version of Aave’s LTV-vs-liquidation-threshold spread.
comet.isBorrowCollateralized(wallet) // true if safe to increase borrow furthercomet.isLiquidatable(wallet) // true if a liquidator can absorb this account nowPoll isLiquidatable for an alerting system; check isBorrowCollateralized before letting a UI offer another borrow. Using either one for both jobs produces false alarms or missed ones.
Computing an actual risk ratio
If a boolean isn’t enough — you want to show “this position can absorb a 12% price drop” instead of just “safe” — combine three reads: the collateral factor from getAssetInfoByAddress, the collateral’s USD price from getPrice, and the account’s debt.
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": {"contracts": {"comet": {"address": COMET_USDC}}}, "expression": ( f'cel.bind(info, comet.getAssetInfoByAddress(solAddress("{WETH}")), {{' ' "borrowCollateralFactor": dyn(formatUnits(info.borrowCollateralFactor, 18)),' ' "liquidateCollateralFactor": dyn(formatUnits(info.liquidateCollateralFactor, 18)),' ' "wethPriceUsd": dyn(formatUnits(comet.getPrice(info.priceFeed), 8))' " })" ), }, timeout=10,)Live against the mainnet USDC market, WETH currently carries an 82.5% borrow collateral factor, an 88% liquidate collateral factor, and a Chainlink-fed price around $2,497.54 (block 25923004 — check it live, ETH moves). With round numbers, the math behind Comet’s isLiquidatable check looks like this:
Collateral: 2 WETH deposited, price $2,500 → $5,000 collateral valueThreshold: liquidateCollateralFactor 0.88 → $4,400 liquidation thresholdDebt: 3,500 USDC borrowed → $3,500 debt value
liquidationBuffer = liquidationThresholdValue / debtValue = 4,400 / 3,500 = 1.257A liquidationBuffer above 1.0 means isLiquidatable reads false; at or below 1.0, it flips to true. That’s the same shape as Aave’s healthFactor < 1.0, computed by hand instead of read from a struct field — and it’s per-collateral-asset, so a wallet with several collateral types needs one getAssetInfoByAddress call per asset, summed before dividing by debt.
Base-asset price counts too
The formula above assumes the base asset (USDC) holds at $1.00. For USDC that’s a safe simplification; for markets like the WETH-base Comet, divide by getPrice(comet.baseTokenPriceFeed()) in the debt-value line too, not just 1. basePrice on the USDC market reads 0.99986181 live, close enough to ignore — don’t assume that holds for every base asset.
Markets differ by chain — and Comet skips BNB Chain entirely
Comet is deployed on Ethereum, Base, Arbitrum, Optimism, Polygon, Scroll, Mantle, Linea, Ronin, and Unichain, confirmed against the deployments/ folder in Compound’s own contracts repo. Of evmquery’s four supported chains, that’s Ethereum, Base, and Polygon — there is no Compound V3 deployment on BNB Chain. Query one of the addresses below against evm_bnb_mainnet and you’ll get a contract-resolution failure, not a market with zero activity.
| Chain | Market | Comet address |
|---|---|---|
| Ethereum | USDC | 0xc3d688B66703497DAA19211EEdff47f25384cdc3 |
| Base | USDC | 0xb125E6687d4313864e53df431d5425969c15Eb2F |
| Polygon | USDC | 0xF25212E676D1F7F89Cd72fFEe66158f541246445 |
All three resolved live with an identical ABI shape during research for this post — swap the chain and COMET_USDC values in the examples above and the same expressions run unchanged. Base’s USDC market is a different contract from its older USDbC (bridged USDC) market; don’t assume one address covers both.
Watching a list of borrowers in one request
Same pattern as any multicall-friendly read: list<sol_address>.map() applies one expression to every wallet in a watchlist and returns the results in order, one HTTP round trip instead of one per wallet.
WATCHLIST = [ "0xWallet1...", "0xWallet2...", "0xWallet3...",]
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_ethereum", "schema": { "contracts": {"comet": {"address": COMET_USDC}}, "context": {"wallets": "list<sol_address>"}, }, "context": {"wallets": WATCHLIST}, "expression": "wallets.map(w, comet.isLiquidatable(w))", }, timeout=10,)liquidatable_flags = resp.json()["result"]["value"] # list<bool>, same order as WATCHLISTValidated live against two real addresses, this returns [false, false] — a flat list<bool> you can zip back against WATCHLIST to find which accounts flipped. Swap isLiquidatable for borrowBalanceOf in the same .map() call if you want raw balances instead of the boolean, same one-request pattern. See Multicall3 batching if you’re building this outside evmquery and want to understand what the batching is actually doing under the hood.
Compound V3 vs Aave V3, for the same job
If you’re choosing between the two protocols for a monitoring build: Aave gives you a ready-made scalar (healthFactor) across its entire multi-asset pool in one call. Compound gives you sharper booleans per market, but only for whichever single Comet contract you query, and requires the manual ratio math above if you want Aave’s granularity. Neither is “correct” — Aave optimizes for a simple integration, Comet’s isolated markets optimize for containing risk to one base asset at a time. See the Aave V3 health factor guide for the Aave-side equivalent of everything in this post.
Building this into a scheduled job rather than a one-off script? evmquery for developers covers the API surface end to end; blockchain monitoring in Python with evmquery turns any of the queries above into a polling loop that fires alerts instead of printing to a terminal.
Next steps
- Aave V3 health factor explained: the same problem, solved with a single struct field instead of two booleans
- Multicall3: batch EVM contract reads: the batching pattern behind every
.map()example above - Blockchain monitoring in Python with evmquery: turn a liquidation check into a polling loop
- evmquery for developers: the full API surface, MCP server, and REST reference



