One API, all positions: unified lending balances across chains
A wallet's lending book is scattered across dozens of protocols on dozens of chains, and every one of them stores it differently. How the 1delta API returns the whole thing - balances, rates, health, prices - in a single request, and what 'unified' has to mean before the number at the top of the page is worth anything.
1delta is a lending aggregator. It reads and normalizes market data and per-user positions from dozens of protocols into one consistent shape, and builds the calldata to act on them. Our piece on lending aggregation makes the case for the market side. This one is about the other half: your side of the book - what a specific wallet actually owns and owes, everywhere at once.
Ask an on-chain lending protocol what a wallet holds and you get an answer in that protocol's own dialect. Aave hands back a bitmask of enabled reserves and two debt tokens per asset. Morpho Blue keys everything by a 32-byte market id and stores shares, not assets. Euler positions hide in 256 sub-accounts derived from the address. Liquity troves are NFTs; Curvance loans are address-keyed; TermMax loans are ERC-721s with a static face value that never accrues; LlamaLend spreads collateral across price bands and refuses to name a liquidation price at all.
None of that is wrong. It is just that a portfolio view needs one shape, and there are roughly two thousand market-shaped things to fold into it.
One wallet, seventeen lender rows, ten protocol families - rendered from a single request. Every number on that screen, including the four totals, comes back in the response; the client does no aggregation.
That screen is the 1delta portal, and you can point it at your own wallet right now - the ids in the path are the selected chains, so /optimize/1,42161,8453 is Ethereum, Arbitrum and Base. It is doing nothing you cannot do yourself: connect, and it makes exactly the request below. The whole frontend is open source at 1delta-DAO/portal, so cloning it is a reasonable way to start.
The request
Chains are a comma-separated list, the account is an address, and that is the whole contract. Leave lenders off and you get everything the API knows how to read on those chains; pass a prefix like MORPHO_BLUE and it matches every per-market sub-key underneath it.
# everything this wallet has, on ten chains
curl "https://portal.1delta.io/v1/data/lending/user-positions\
?chains=1,10,56,137,8453,42161,43114,146,999,130\
&account=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
The response has exactly two top-level members. items is one entry per lender per chain, each carrying its own balances, APRs, leverage and sub-accounts. summary is the portfolio rolled up - the four totals in the figure above, plus a per-chain breakdown.
{
"items": [{
"lender": "AAVE_V2", "chainId": "1",
"balanceData": { "deposits": 123.53, "debt": 0, "collateral": 106.03,
"nav": 123.53, "nav24h": 103.39 },
"aprData": { "apr": 0.1698, "depositApr": 0.1698, "borrowApr": 0,
"rewardApr": 0, "intrinsicApr": 0 },
"leverage": 1,
"data": [{ // one element per sub-account
"accountId": "0", "health": null, "borrowCapacityUSD": 101.36,
"positions": [{
"marketUid": "AAVE_V2:1:0x2260fac5…c599",
"deposits": "0.0000727", "debt": "0",
"depositsUSD": 5.2246, "withdrawable": "0.0000727",
"collateralEnabled": true,
"underlyingInfo": { "asset": { "symbol": "WBTC", "decimals": 8 },
"prices": { "priceUsd": 71865.17,
"priceChange24h": 11.10 } }
}]
}]
}],
"summary": { "balanceData": {…}, "aprData": {…}, "leverage": 1,
"activeLenders": 3, "activeChains": 10,
"chains": [{ "chainId": "1", "totalDepositsUSD": 123.53,
"totalDebtUSD": 0, "netWorth": 123.53,
"lenderCount": 1 }, … ] }
}
That is trimmed from a live response for vitalik.eth - a dust-sized Aave V2 position on Ethereum plus two smaller ones on BNB and Polygon, found without being told where to look.
What "unified" has to mean
Concatenating protocol responses is easy, and it produces a number that is quietly wrong. Four normalizations do the actual work.
One position identity
Every protocol's notion of "a position" collapses into the same slot: data[], a list of sub-accounts, each with its own health factor and borrow capacity. An Euler sub-account index, a Liquity trove NFT, a TermMax GT token, a Teller bidId, a Sky CDP id and an address-keyed Curvance loan all land there - and a plain Aave account lands there too, as accountId: "0". Callers write the risk logic once.
One rate
A pool's posted supply rate is rarely the rate a position earns, so aprData ships decomposed - and a decomposition is only useful if every field states its denominator. Two kinds of number live in that object.
Leg rates are quoted on the balance they apply to. depositApr is annual deposit interest over deposits, borrowApr is annual borrow interest over debt, and the incentive and intrinsic legs follow the same rule: rewardDepositApr and intrinsicDepositApr are per dollar of deposits, rewardBorrowApr and intrinsicBorrowApr per dollar of debt. The intrinsic pair carries the yield the collateral earns on its own - a wstETH deposit is paid twice, and the pool only knows about one of the payments.
Net rates are quoted on NAV, the equity in the position:
apr = (deposit interest - borrow interest) / (deposits - debt)
rewardApr = (reward accruals, both sides) / (deposits - debt)
intrinsicApr = (intrinsic accruals, both sides) / (deposits - debt)
Only those three are additive. apr + rewardApr + intrinsicApr is the position's net APR because all three share the NAV denominator; depositApr - borrowApr is not a net rate at all, because the two are quoted on different bases. Subtracting them is the exact mistake the split exists to prevent.
The NAV denominator is also why the leveraged rows in the figure read the way they do. LlamaLend crvUSD / SDOLA holds $719.30 of collateral against $691.71 of debt - $27.58 of equity, 26x leverage - so a spread of roughly three percent on the collateral becomes 76.47% on the money actually at risk. Same convention as the leverage field itself, which is deposits / nav.
Roll-up follows the denominators. A lender entry aggregates its sub-accounts, and summary aggregates the lender entries, both by weighting each field with the balance it is quoted on: net legs by NAV, deposit-side legs by deposits, borrow-side legs by debt.
apr_total = Σ (nav_i * apr_i) / Σ nav_i
So the 14.78% on the net-worth tile is the NAV-weighted mean of the row APRs - not the average of the column, and not a yield on the $4,043 of deposits. Everything is nominal, in percent, uncompounded, and holds balances, prices, utilization and incentive rates fixed: it is the position's current rate, not a forecast. A row can be sharply negative, and two in the figure are, because the debt leg outruns everything else.
One price, stated twice
Every position carries its dollar value under both price bases, because the two questions want different prices. depositsUSD and debtUSD use market prices, and the portfolio totals, NAV and the APR denominators are built from them. depositsUSDOracle and debtUSDOracle use the price the protocol's own oracle reports, and collateral, health and borrow capacity are computed from those.
Shipping both is a trade-off made explicit rather than a nicety. A protocol oracle is the number the liquidation engine will act on, but it is protocol-specific - capped, delayed, deliberately conservative, sometimes an owner-declared price - so two positions valued by two protocols' oracles are not strictly comparable to each other. Market prices are comparable across venues and chains, which is what a portfolio total needs, and they are not what decides whether you get liquidated. Solvency numbers use the oracle, display numbers use the market price, and the response says which is which instead of picking one and hoping. A 24-hour price stamp travels alongside, which is what makes the "13.12% 24h" on the net-worth tile possible without a second request.
One health field
Health is computed per sub-account with that protocol's own collateral factors, e-mode or band, and it is null - not 1, not infinity - when there is no debt. Where liquidation is not price-driven at all, the field stays honest rather than inventing a threshold: Frankencoin liquidates through a challenge auction, Teller on a missed payment date, and neither one is an HF < 1 story.
What is unified here is the slot, not the comparability. A 1.05 computed under one protocol's collateral factors and liquidation rule is not the same distance from trouble as a 1.05 computed under another's, and nothing in the response pretends otherwise - which is why borrowCapacityUSD sits next to it, and why our term sheet work covers what else a market has to state before its numbers mean anything.
What "unified" does not mean
Three claims the shape deliberately does not make.
Not one instrument. A Liquity trove, a TermMax fixed-term loan and an Aave account come back in the same shape, not as the same thing: one is a CDP facing a redemption queue, one has a maturity and a face value that never accrues, one is a floating-rate pooled position that can be topped up in any block. Normalizing representation is what lets a client render and act on all three; deciding that their rates or health numbers are comparable is the caller's judgment, and the term-sheet fields exist to inform it.
Not one credit line. The chains are read together, not netted. Collateral on Arbitrum does not back debt on Base, no borrowing capacity is created by the aggregation, and health stays per sub-account. What the summary adds up is dollar values, not risk.
Not one instant. Every number for a chain reflects that chain's latest state at the time of the request, so a ten-chain portfolio is ten per-chain snapshots added together rather than one simultaneous global state. Prices move between them, and the totals carry that much timing noise. The self-RPC path below takes an explicit blockTag when a report needs a fixed point instead.
And the accounting the totals imply: nav is deposits - debt at market prices, with accrued interest already inside the balances, which are current rather than principal-at-open. Unclaimed incentives are not in it - they arrive separately as claimableRewards per position and a rewards[] array per sub-account, so nothing is counted twice.
Already have a fast RPC? Use it
A portfolio read is only as quick as the node underneath it, and plenty of integrators already run a premium or co-located one. There is no reason that node has to be ours.
The same decoding runs in two halves for exactly this case. GET /user-positions/rpc-call returns ready-made JSON-RPC objects plus an rpcCallId; you execute them against your own endpoint, at your own latency, with your own blockTag and batchSize; and POST /user-positions/parse hands back the identical decoded structure. The id is single-use and its context expires in five minutes.
You get the call graph and the decoder, and the round trip stays yours - which also means no account address touches our infrastructure on that path. And because the calls are ordinary eth_calls, the same request runs against a local Anvil fork as against mainnet, which is how our own integration tests use it.
What it reaches today
| 44 | chains with lending markets |
| 2,125 | lender × market entries indexed |
| $36.9B | supplied across them |
Measured against the live API on 20 August 2026, via /v1/data/lending/lenders.
Behind those rows sit the protocol families the fetcher speaks natively - the Aave, Compound and Morpho lineages and their forks, Euler, Fluid, Silo, Dolomite, Venus, Gearbox, Spark, Lista and Moolah, the Liquity V2 family and River, the CDP stacks (Sky, Frankencoin, Inverse, Olympus Cooler, Resupply), the fixed-term venues (Term Finance, Morpho Midnight, TermMax, Teller, Exactly), Curve's LlamaLend, Curvance, Twyne and Fraxlend. Adding one is a fetcher and a term sheet - never a new endpoint.
Endpoints
| Endpoint | Does |
|---|---|
GET /v1/data/lending/user-positions | Positions and portfolio summary, read server-side. chains, account, optional lenders. Sets partial with incompleteLenders if a lender could not be included in the totals. |
GET …/user-positions/rpc-call | The raw calls to run yourself, plus an rpcCallId. Takes blockTag, batchSize, subAccounts. |
POST …/user-positions/parse | Your RPC results in, the same decoded structure out. |
GET /v1/data/lending/lenders | Everything readable on a chain set, with TVL - where the coverage numbers above come from. |
Positions are only half of it. The same market keys feed /v1/actions/*, which builds the transaction to deposit, borrow, repay, loop or migrate against any row the portfolio view just returned - see building on the 1delta API for that side. Reading and acting share one address space, which is the reason the reading side was built this carefully.
Full reference at https://portal.1delta.io/v1/docs.