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.


Illustration for “One API, all positions: unified lending balances across chains”

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.

Your lending positions
Total net worth
$388.13
13.12% 24h
Total deposits
$4,043
Total debt
$3,655
Net APR
14.78%
+39.4% rewards
Lender / account
NAV
Deposits
Debt
APR
Health
Positions
Curve LlamaLendLlamaLend crvUSD / svZCHF
$232.23
$2.2K
$2.0K
18.75% +33.3%
1.05
COLLsvZCHF1,731.28  $2.2K
DEBTcrvUSD1,979.85  $2.0K
Avalon FinanceAvalon
$86.36
$284.98
$198.62
−12.00%
1.15
COLLxSolvBTC0.0040  $284.98
DEBTWBTC0.0027  $197.18
Curve LlamaLendLlamaLend crvUSD / SDOLA
$27.58
$719.30
$691.71
76.47% +171.0%
1.00
COLLSDOLA510.06  $719.30
DEBTcrvUSD692.17  $691.71
Lista DAOLista WBETH–WETH 97
$9.60
$173.49
$163.90
32.46% +41.0%
1.02
COLLWBETH0.0689  $173.49
DEBTWETH0.0719  $163.90
Aave V4Aave V4 Main
$1.40
$3.90
$2.50
−5.61%
1.30
COLLWETH0.0017  $3.90
DEBTUSDC2.50  $2.50
Aave V3Aave V3
$1.06
$12.52
$11.47
7.29% +29.2%
1.04
COLLweETH0.0050  $12.52
DEBTWETH0.0050  $11.47
TwyneTwyne wstETH / WETH
$0.16
$2.39
$2.23
6.13% +30.3%
1.05
COLLwstETH0.0008  $2.39
DEBTWETH0.0010  $2.23
Morpho BlueMorpho sUSDe–AUSD
$0.08
$0.10
$0.02
4.61% +5.4%
5.36
COLLsUSDe0.0787  $0.10
DEBTAUSD0.0173  $0.02
VenusVenus
$0.41
$0.41
$0
0.03%
n/a
COLLWBTC0.0000  $0.41
Radiant V2Radiant V2
$0.06
$0.18
$0.12
−18.35%
1.24
COLLUSDC0.1770  $0.18
DEBTUSDT0.1217  $0.12
7 further rows omitted — Sumer, three more Morpho Blue markets, a second Lista pair, a second Venus pool.

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.

Four things "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: the base depositApr and borrowApr, the incentive legs (rewardApr, rewardDepositApr, rewardBorrowApr), and the intrinsicApr legs that carry the yield the collateral earns on its own - a wstETH deposit is paid twice, and the pool only knows about one of them. All nominal APR, in percent, everywhere. Net APR is the sum, and it can be sharply negative: two rows in the figure are, because the debt leg outruns everything else.

One price

Prices come from the integrated protocols' own oracles rather than a market-data vendor, so the dollar figure agrees with the number the liquidation engine will use. Positions carry both: depositsUSD for display and depositsUSDOracle for anything solvency-shaped. 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 factor

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. Our term sheet work covers what else a market has to state before a rate means anything.

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

47chains served by the API
38of them carrying lending markets
2,128lender × market entries indexed
$36.6Bsupplied 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

EndpointDoes
GET /v1/data/lending/user-positionsPositions and portfolio summary, read server-side. chains, account, optional lenders.
GET …/user-positions/rpc-callThe raw calls to run yourself, plus an rpcCallId. Takes blockTag, batchSize, subAccounts.
POST …/user-positions/parseYour RPC results in, the same decoded structure out.
GET /v1/data/lending/lendersEverything 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.

← All posts