Skip to content

DeFi Positions

Resolve open positions across Lido, Aave V3, Compound V3, and Uniswap V3 from a single wallet endpoint — no per-protocol RPC plumbing on your side.

The DeFi position resolver answers "what does this address own inside a protocol?" — staked stETH shares, Aave lending collateral, Compound markets, Uniswap V3 NFT positions. It is intentionally separate from /balances, which only sees ERC-20 token balances and would report stETH as a plain token without surfacing the underlying ETH it represents.

Supported protocols

ProtocolChainsPosition typesSource
LidoEthereumstETH (LIDO_STAKED)stETH balanceOf
Aave V3Ethereum, Arbitrum, Optimism, Polygon, Base, AvalancheLEND + BORROWPool getUserAccountData
Compound V3Ethereum, Arbitrum, Base, PolygonLEND + collateralComet userBasic + userCollateral
Uniswap V3Ethereum, Arbitrum, Optimism, Polygon, BaseLP (per NFT tokenId)NPM enumeration

Protocols outside this list are excluded from the response (no silent fallback to a third-party API). For chains that are not in this matrix, the resolver returns an empty list — see pitfalls below.

Endpoint

GET /v1/wallets/{address}/defi-positions

ParamTypeDefaultNotes
chainstringethereumChain slug; must be in the supported matrix above
protocolsCSV string(all)Restrict to lido,aave_v3,compound_v3,uniswap_v3

POST /v1/defi/refresh — evict the cache for one address and re-resolve. Body: { chainId, address }. Same response shape as the GET. Use this after a known on-chain action (deposit, withdraw, position close) when you want sub-cache-TTL freshness.

Example 1 — all positions for a wallet (Ethereum)

bash
curl 'https://api.cryptachain.com/v1/wallets/0xd8dA.../defi-positions?chain=ethereum' \
  -H "X-API-Key: $CRYPTACHAIN_API_KEY"

Response:

json
{
  "chain_id": 1,
  "address":  "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
  "protocols": ["LIDO", "AAVE_V3", "COMPOUND_V3", "UNISWAP_V3"],
  "positions": [
    {
      "protocol": "LIDO",
      "position_id": "lido:stETH",
      "position_type": "LIDO_STAKED",
      "asset": "stETH",
      "balance_wei": "120000000000000000000",
      "balance_usd": 462030.00
    },
    {
      "protocol": "AAVE_V3",
      "position_id": "aave_v3:0xb53c1a33016b2dc2ff3653530bff1848a515c8c5",
      "position_type": "LEND",
      "asset": "USDC",
      "balance_wei": "250000000000",
      "balance_usd": 250000.00,
      "health_factor": 2.41
    },
    {
      "protocol": "UNISWAP_V3",
      "position_id": "uniswap_v3:nft:8472",
      "position_type": "LP",
      "asset": "USDC/WETH 0.05%",
      "balance_usd": 18247.92
    }
  ]
}

balance_wei is the protocol-native raw amount (NUMERIC(80,0) — wide enough for any uint256). balance_usd is computed using the same VWAP pricing as /v1/prices (see Prices guide) at the time of resolution. health_factor is only present on Aave/Compound borrow positions.

Example 2 — filter to a single protocol

python
import os, requests

r = requests.get(
    "https://api.cryptachain.com/v1/wallets/0xd8dA.../defi-positions",
    params={"chain": "ethereum", "protocols": "uniswap_v3"},
    headers={"X-API-Key": os.environ["CRYPTACHAIN_API_KEY"]},
)
for p in r.json()["positions"]:
    print(f"{p['position_id']}: ${p['balance_usd']:,.2f}")

Protocol names in the CSV are case-insensitive; unknown names are silently skipped (no error). This lets you over-specify (protocols=lido,aave_v3,future_proto) without breaking when a new protocol ships.

Example 3 — refresh after a known on-chain action

Positions are cached for 60 seconds by default. If your app just executed a deposit on behalf of the user and the UI needs to see the new position immediately, force a refresh:

typescript
await fetch('https://api.cryptachain.com/v1/defi/refresh', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.CRYPTACHAIN_API_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ chainId: 1, address: '0xd8dA...' }),
});

The refresh response is the same shape as the GET (the refreshed positions are returned inline), plus "refreshed": true. Calling this from a hot loop is wasteful — the underlying RPC calls do cost compute; the cache exists for a reason.

Common pitfalls

Chains without RPC coverage

If you query chain= a chain not in the supported matrix, the resolver returns:

json
{ "chain_id": 7777777, "address": "0x...", "protocols": [], "positions": [] }

…not an error. This is intentional — the resolver is chain-internalised: it only resolves protocols on chains where CryptaChain runs its own RPC node. There is no fallback to public providers. If a chain you need is missing, file an issue; the matrix grows quarterly.

Cache TTL during volatility

The default 60-second cache works well for balance / valuation queries but can be confusing during fast-moving positions (e.g. an Aave health factor moving close to 1.0 in a volatile market). For real-time monitoring, drive POST /v1/defi/refresh from a websocket-driven price oracle on your side; don't poll the GET endpoint sub-minute.

Wrapped staking exposures

wstETH (Wrapped stETH, a non-rebasing wrapper) shows up in /balances but not in /defi-positions (which currently surfaces native stETH only). If you build a "staked ETH" total, sum native stETH from /defi-positions + wstETH ERC-20 balance from /balances × wstETH's redemption rate. Full wrapper resolution is on the roadmap.

Empty position arrays are valid

A wallet with no DeFi exposure returns positions: []. Do not treat that as an error — it just means the address has no resolvable positions across the supported protocols.

Try it live

The /playground lets you call /defi-positions with your own key. Filter operations by tag Wallets (it's grouped under wallets, not a separate DeFi tag).

Next steps

  • Wallet guide — context on the surrounding wallet endpoints
  • Prices guide — how USD valuations are computed
  • Rate limits — the resolver is more expensive than /balances; budget accordingly

CryptaChain API — built by CryptaCount Luxembourg S.a r.l.