Compound V3 (Comet): Read Borrow Balance and Liquidation Risk via API

Compound V3 (Comet) skips the health-factor field. Read borrow balance, collateral, and liquidation risk with isBorrowCollateralized and isLiquidatable via API.

evmquery team··7 min read
Share
Compound V3 (Comet) — read borrow balance, collateral, and liquidation risk via API

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 os
import requests
COMET_USDC = "0xc3d688B66703497DAA19211EEdff47f25384cdc3" # Compound V3 USDC market, Ethereum
WETH = "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 further
comet.isLiquidatable(wallet) // true if a liquidator can absorb this account now

Poll 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 value
Threshold: liquidateCollateralFactor 0.88 → $4,400 liquidation threshold
Debt: 3,500 USDC borrowed → $3,500 debt value
liquidationBuffer = liquidationThresholdValue / debtValue
= 4,400 / 3,500
= 1.257

A 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 WATCHLIST

Validated 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

Share

Check liquidation risk without an ABI

Free tier, no credit card. Run the query below against any Compound V3 market.