DeFi Portfolio Dashboard Development: Aggregation, P&L, Multi-Chain

We build turnkey DeFi portfolio dashboards. Imagine a user holding USDC in Aave, an ETH-USDC LP in Uniswap V3, wstETH in Lido, and an open perpetual position on GMX. Four different protocols, four different ways to represent positions, four different APIs or subgraphs. The dashboard's job is to aggr

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1310
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1270
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1012
  • image_logo-aider_0.webp
    AIDER company logo development
    955
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1062

We build turnkey DeFi portfolio dashboards. Imagine a user holding USDC in Aave, an ETH-USDC LP in Uniswap V3, wstETH in Lido, and an open perpetual position on GMX. Four different protocols, four different ways to represent positions, four different APIs or subgraphs. The dashboard's job is to aggregate everything into a single screen with real P&L numbers. Our experience shows that 80% of the effort goes into the data layer: normalizing data from different sources and correctly computing stale versus live balances. We guarantee block-level accuracy and minimal latency. Contact us to evaluate your project stack.

How is P&L and impermanent loss calculated?

The hardest part is correctly calculating unrealized P&L for LP positions. For Uniswap V3, a position is an NFT with specific tickLower, tickUpper, and liquidity. Current token0 and token1 amounts depend on the pool's current sqrtPriceX96. The formula is nontrivial:

function getAmountsFromLiquidity( sqrtPriceX96: bigint, sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint ): [bigint, bigint] { if (sqrtPriceX96 <= sqrtRatioAX96) { const amount0 = (liquidity * (sqrtRatioBX96 - sqrtRatioAX96) * Q96) / (sqrtRatioBX96 * sqrtRatioAX96) return [amount0, 0n] } else if (sqrtPriceX96 < sqrtRatioBX96) { const amount0 = (liquidity * (sqrtRatioBX96 - sqrtPriceX96) * Q96) / (sqrtRatioBX96 * sqrtPriceX96) const amount1 = (liquidity * (sqrtPriceX96 - sqrtRatioAX96)) / Q96 return [amount0, amount1] } else { const amount1 = (liquidity * (sqrtRatioBX96 - sqrtRatioAX96)) / Q96 return [0n, amount1] } } 

Impermanent loss is calculated as the difference between the current position value and the value if the same assets were simply held since entry. The dashboard must store entry price and initial amounts at position opening.

What data sources do we use?

We combine on-chain direct calls and indexers. The most accurate way to get a balance is a direct eth_call to the contract. For token balance — balanceOf(). For an Aave position — getUserAccountData(). This is always current but slow: each protocol requires separate calls, and latency grows linearly with dozens of protocols. The solution is Multicall3 (contract 0xC...A11, deployed on all major EVM chains): batching 50+ calls into one transaction. Response time is like one RPC call instead of 50. Comparison: Multicall3 is 20x faster than sequential calls.

import { multicall } from 'viem' const results = await multicall(client, { contracts: [ { address: AAVE_POOL, abi: aavePoolAbi, functionName: 'getUserAccountData', args: [userAddress] }, { address: USDC_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] }, { address: UNISWAP_POSITION_MANAGER, abi: nftAbi, functionName: 'balanceOf', args: [userAddress] }, ] }) 

For historical data (transaction history, PnL over time) direct calls don't work — we need indexers.

We use The Graph for historical data. Uniswap, Aave, Compound, Curve, Balancer all have official subgraphs on The Graph Network. A subgraph provides a GraphQL API to query historical events: deposits, withdrawals, swaps, liquidations.

query UserPositions($user: String!) { aaveV3_deposits(where: { user: $user }, orderBy: timestamp, orderDirection: desc) { amount reserve { symbol, decimals, priceInUSD } timestamp } aaveV3_borrows(where: { user: $user }) { amount reserve { symbol } currentVariableBorrowRate } } 

Problem: different protocol versions have different subgraphs. Aave V2 on Ethereum, Aave V3 on Polygon, Aave V3 on Arbitrum — three different subgraphs with different schemas. Normalization is the main engineering task of the dashboard. We use a unified abstraction layer that maps all schemas into a single data model.

Alchemy and Moralis as API-over-RPC. Alchemy API provides ready-made methods: getTokenBalances() returns all ERC-20 balances of an address without iterating contracts. getAssetTransfers() provides transfer history. This significantly simplifies initial implementation, but costs money at high load. Moralis additionally aggregates NFT positions and DeFi protocol positions via their DeFi API — paid, but saves months of custom data layer development. For an MVP, Alchemy + The Graph for key protocols is justified. For production with tens of thousands of users, we recommend a custom indexer.

Source Freshness Cost Integration Complexity
Multicall3 Live (block-level) Free (RPC gas) Medium
The Graph ~1 minute lag Free (rate-limited) High (different schemas)
Alchemy API Live Pay-per-request Low
Moralis Live Subscription Low

Multi-Chain Aggregation

A typical user is active on Ethereum mainnet, Arbitrum, Polygon, and Base. The dashboard must show the total portfolio across chains. Our approach: parallel requests to each chain's RPC via Promise.all(), normalizing balances into USD with a unified price oracle. We use the Coingecko API or DefiLlama Price API to fetch current prices by token address and chain ID. The cross-chain identity problem: the user address is the same on all EVM chains (ECDSA), but a smart contract wallet (Safe, Argent) may have different addresses on different chains if deploy was not synchronized. We support multi-address mode: the user can add multiple addresses to one profile.

How We Ensure Performance

Backend: Node.js + TypeScript with viem for RPC. Redis for caching balances (TTL 30 seconds for live data, 5 minutes for historical). PostgreSQL for storing historical portfolio snapshots (to build equity curves).

Frontend: React + wagmi v2 for wallet connection, Recharts or TradingView Lightweight Charts for graphs, Tanstack Query for data fetching with automatic refetch every 30 seconds.

WebSocket for real-time updates: subscribing to eth_subscribe("newHeads") triggers balance updates on each new block — providing liveliness without wasteful polling.

Example WebSocket configuration
const transport = http('https://mainnet.infura.io/v3/YOUR_KEY') const client = createPublicClient({ transport }) 

What Is Included

  • Data layer documentation (data schemas, normalization description).
  • Full dashboard code repository access.
  • 2 hours of online team training.
  • 2 weeks of free post-deployment support.
  • Optional SLA for ongoing maintenance.

Our track record: 5 years in blockchain development, 20+ successful DeFi projects. We guarantee the dashboard will run without downtime and with block-level accuracy.

Process

  1. Analysis (1–2 days). List target protocols and chains, prioritize by popular audience use cases.
  2. Data layer (5–7 days). Build Multicall aggregator, integrate The Graph for key protocols, normalize into a unified position schema.
  3. Backend API (3–5 days). Deliver REST/GraphQL API for frontend, caching, portfolio history.
  4. Frontend (5–7 days). Implement wallet connection, total balance view, per-protocol details, charts.

Timeline Estimates

Stage Timeline
MVP (5–7 protocols, 2–3 chains) 2–3 weeks
Full dashboard (history, IL, alerts, mobile) 6–8 weeks

Get a consultation for your project — we will assess complexity and timelines for free.