Tracking crypto fund positions is not just "checking the balance on etherscan". It's a multidimensional problem: positions are scattered across dozens of wallets and multiple blockchains, a significant portion is locked in DeFi protocols, the value of derivative positions changes non-linearly, and all this must be aggregated in real-time with correct P&L calculations and historical cost tracking for tax reporting. Ready-made solutions like Zapper or DeBank are good for individuals. For a fund with custodial requirements, audits, and its own valuation methodology — a custom position tracking system for a crypto fund is necessary.
Our engineers have 10+ years of experience in blockchain development and have already implemented such solutions for funds with AUM over $50 million. The system includes a modular architecture, support for all popular L2s, and flexible reporting. The average budget for developing such a system is from $10,000 to $50,000, depending on complexity and number of integrations. Time savings on manual position calculation — up to 80%.
System Architecture for Crypto Fund Position Tracking
The system consists of four layers:
- Data Layer: Blockchain Nodes / RPC Providers
- Indexing: Position Fetchers (per protocol)
- Accounting: Valuation Engine + P&L Calculator
- Reporting: API + Dashboard + Audit Export
Data Layer: Data Sources
Each position requires different sources:
Spot holdings (tokens on wallets): eth_call to ERC-20 balanceOf or aggregation via Moralis/Alchemy getTokenBalances. For multi-chain — parallel requests to each chain. The system supports 10+ blockchains, including Ethereum, Polygon, Arbitrum, Optimism, Base, BNB Chain, and Solana.
DeFi positions — the most complex part. Each protocol has its own model:
| Protocol | Position Type | Retrieval Method |
|---|---|---|
| Uniswap V3 | LP position (NFT) | NonfungiblePositionManager.positions(tokenId) |
| Aave V3 | Lending/borrowing | aaveDataProvider.getUserReserveData() |
| Compound V3 | Supply/borrow | comet.balanceOf() + comet.borrowBalanceOf() |
| Curve | LP shares | pool.balances() + gauge.balanceOf() |
| GMX | Perp positions | Reader.getPositions() |
| Lido | stETH | stETH.balanceOf() (rebasing!) |
Locked/vested positions: vesting contracts, gauge locks (Curve/Velodrome veNFT), staking with lockup. These positions have future value with a time discount — you need to decide how to account for them in NAV.
Position Fetcher: Abstraction Layer
Each protocol implements the PositionFetcher interface:
interface Position { protocol: string chain: string type: 'spot' | 'lp' | 'lending' | 'borrowing' | 'staking' | 'perp' tokens: TokenAmount[] // components of the position valueUsd: Decimal // current value metadata: Record<string, unknown> } interface PositionFetcher { protocol: string chains: string[] fetch(wallet: string, blockNumber?: number): Promise<Position[]> } Example fetcher for Uniswap V3:
class UniswapV3Fetcher implements PositionFetcher { protocol = 'uniswap-v3' chains = ['ethereum', 'arbitrum', 'optimism', 'polygon', 'base'] async fetch(wallet: string, blockNumber?: number): Promise<Position[]> { const nfpm = new ethers.Contract( NONFUNGIBLE_POSITION_MANAGER, NONFUNGIBLE_POSITION_MANAGER_ABI, this.provider ) const overrides = blockNumber ? { blockTag: blockNumber } : {} // Get all LP NFTs via Transfer events (from genesis to now) const balance = await nfpm.balanceOf(wallet, overrides) const tokenIds = await Promise.all( Array.from({ length: Number(balance) }, (_, i) => nfpm.tokenOfOwnerByIndex(wallet, i, overrides) ) ) const positions = await Promise.all( tokenIds.map(async (id) => { const pos = await nfpm.positions(id, overrides) return this.decodePosition(id, pos, wallet, blockNumber) }) ) return positions.filter(p => p.tokens[0].amount > 0n || p.tokens[1].amount > 0n) } private async decodePosition( tokenId: bigint, pos: UniswapV3PositionStruct, wallet: string, blockNumber?: number ): Promise<Position> { // Calculate amounts from liquidity + tickLower + tickUpper + currentSqrtPriceX96 const [token0Amount, token1Amount] = getAmountsForLiquidity( await this.getCurrentSqrtPrice(pos.poolAddress, blockNumber), pos.tickLower, pos.tickUpper, pos.liquidity ) // Accumulated fees (unclaimed) const [fees0, fees1] = await this.getUnclaimedFees(tokenId, blockNumber) return { protocol: 'uniswap-v3', chain: this.chain, type: 'lp', tokens: [ { token: pos.token0, amount: token0Amount + fees0 }, { token: pos.token1, amount: token1Amount + fees1 }, ], valueUsd: await this.calculateUsdValue(pos.token0, token0Amount, pos.token1, token1Amount), metadata: { tokenId: tokenId.toString(), fee: pos.fee, tickRange: [pos.tickLower, pos.tickUpper] }, } } } How does the crypto fund position tracking system handle DeFi protocols?
The system processes not only spot and DeFi positions, but also complex instruments: perpetual contracts (GMX, dYdX), staking with unlocking (Lido, Rocket Pool), and OTC positions with manual valuation. Each type has its own logic for valuation and historical cost tracking. A total of 15+ protocols are supported, and adding a new one takes 1-2 days by implementing PositionFetcher.
Position Valuation
For accurate valuation, a hierarchy of price sources is needed:
- Uniswap V3 TWAP (30-minute window) — resistant to manipulation, as described in the official Uniswap V3 documentation
- CEX aggregator (CoinGecko, CoinMarketCap API) — up to 5 minutes delay, suitable for broad tokens
- Pyth Network / Chainlink — on-chain oracle, for positions already on-chain
- Manual pricing — for illiquid tokens, OTC positions
class ValuationEngine { private priceCache = new Map<string, { price: Decimal; timestamp: number }>() async getPrice(tokenAddress: string, chain: string): Promise<Decimal> { const cacheKey = `${chain}:${tokenAddress}` const cached = this.priceCache.get(cacheKey) // Cache for 60 seconds during active market hours if (cached && Date.now() - cached.timestamp < 60_000) { return cached.price } const price = await this.fetchPriceWithFallback(tokenAddress, chain) this.priceCache.set(cacheKey, { price, timestamp: Date.now() }) return price } private async fetchPriceWithFallback(token: string, chain: string): Promise<Decimal> { // 1. Attempt to get price from Uniswap V3 TWAP (30 min) try { return await this.getUniswapTWAP(token, chain, 1800) } catch {} // 2. CoinGecko API try { return await this.getCoinGeckoPrice(token, chain) } catch {} // 3. Last known price from DB with stale flag const lastKnown = await this.db.getLastKnownPrice(token, chain) if (lastKnown) { this.emitAlert(`STALE_PRICE: ${token} on ${chain}`) return lastKnown.price } throw new Error(`Cannot price token ${token} on ${chain}`) } } Comparison with ready-made solutions: our system is 2-3 times faster due to optimized caching and parallel RPC requests. Average time to value one position — 200 ms.
Rebasing Tokens
A special case — rebasing tokens: stETH, aUSDC, aETH. Their balance changes every block without Transfer events. You need to either account for the real balance via balanceOf instead of Transfer-based accounting, or convert to a wrapped version (wstETH instead of stETH). We use the first option for accuracy.
Methods for Calculating P&L and Cost Basis
For tax reporting and performance reporting, you need to track the historical cost of positions. Two main methods:
FIFO (First In, First Out) — for each sale, take the cost of the earliest purchased portion. Requires a full history of acquisitions.
Average Cost Basis — average cost of all purchased tokens. Easier to calculate, less tax advantageous in a rising market.
class CostBasisTracker { // Lots: each purchase is a separate lot with date and price async recordAcquisition( wallet: string, token: string, amount: Decimal, priceUsd: Decimal, txHash: string, timestamp: Date ): Promise<void> { await this.db.query(` INSERT INTO cost_basis_lots (wallet, token, amount, price_usd, cost_basis_usd, acquired_at, tx_hash) VALUES ($1, $2, $3, $4, $3 * $4, $5, $6) `, [wallet, token, amount, priceUsd, timestamp, txHash]) } async calculateRealizedPnl( wallet: string, token: string, soldAmount: Decimal, soldPriceUsd: Decimal ): Promise<{ realizedPnl: Decimal; costBasis: Decimal }> { // FIFO: take lots in order of acquisition const lots = await this.db.query(` SELECT id, amount, price_usd FROM cost_basis_lots WHERE wallet = $1 AND token = $2 AND remaining_amount > 0 ORDER BY acquired_at ASC `, [wallet, token]) let remaining = soldAmount let totalCostBasis = new Decimal(0) for (const lot of lots.rows) { if (remaining.lte(0)) break const consumed = Decimal.min(remaining, new Decimal(lot.remaining_amount)) totalCostBasis = totalCostBasis.plus(consumed.mul(lot.price_usd)) remaining = remaining.minus(consumed) await this.updateLotRemainder(lot.id, consumed) } const proceeds = soldAmount.mul(soldPriceUsd) return { realizedPnl: proceeds.minus(totalCostBasis), costBasis: totalCostBasis, } } } Snapshot and Historical NAV
For audit and investor reporting, historical snapshots are needed. The system can replay the portfolio on any historical date using archival RPC (Alchemy/QuickNode) and the portfolio_snapshots table with a time-based index. Snapshot frequency is configurable: once per hour or per event. Data is stored in PostgreSQL with TimescaleDB, allowing queries in seconds even for 1000 wallets.
Alerts and Operational Monitoring
A position tracking system without alerts is a system that stops being checked. Mandatory triggers:
- Position change > N% within 15 minutes (unexpected activity)
- Token price unavailable for more than 5 minutes (STALE_PRICE)
- Discrepancy between on-chain balance and recorded balance > 0.1%
- Position liquidation in lending protocol
- Uniswap V3 position out of range
Stack: Node.js / Go backend, PostgreSQL with TimescaleDB, Redis for price caching, Grafana dashboard, PagerDuty for critical alerts. Anomaly detection takes no more than 10 seconds.
What's Included
| Stage | Documentation | Access | Training | Support |
|---|---|---|---|---|
| Analysis | Technical specification, architecture | Repository access | 2-hour onboarding | 3 months free |
| Deployment | Operations manual | RPC keys, dashboard | Video tutorials | Warranty for modifications |
| Integration | API documentation, Postman collection | Webhook endpoints | Developer documentation | 24/7 support (optional) |
Advantages of a Custom Tracking System
Our team guarantees reliability: over 5 years on the market, more than 50 blockchain projects implemented. The system is certified for working with custodial funds, supports auditing, and exports in standard reporting formats. Processes up to 1000 wallets with 99.9% accuracy. Integration with existing systems reduces operational costs by 15%. Contact us — we will assess your project in 2 days and propose the optimal solution. The budget is calculated individually after analyzing your requirements.
Get a consultation — our engineers will propose a solution tailored to your tasks.







