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
| Protocol | Chains | Position types | Source |
|---|---|---|---|
| Lido | Ethereum | stETH (LIDO_STAKED) | stETH balanceOf |
| Aave V3 | Ethereum, Arbitrum, Optimism, Polygon, Base, Avalanche | LEND + BORROW | Pool getUserAccountData |
| Compound V3 | Ethereum, Arbitrum, Base, Polygon | LEND + collateral | Comet userBasic + userCollateral |
| Uniswap V3 | Ethereum, Arbitrum, Optimism, Polygon, Base | LP (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
| Param | Type | Default | Notes |
|---|---|---|---|
chain | string | ethereum | Chain slug; must be in the supported matrix above |
protocols | CSV 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)
curl 'https://api.cryptachain.com/v1/wallets/0xd8dA.../defi-positions?chain=ethereum' \
-H "X-API-Key: $CRYPTACHAIN_API_KEY"Response:
{
"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
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:
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:
{ "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