DeFi API integration is the backbone of every modern decentralized finance application. Whether you're building a portfolio tracker, a trading bot, or a yield optimizer, you need reliable access to on-chain data across multiple protocols and chains. This tutorial walks you through the complete integration process—from provider selection to production-ready code—with a special focus on the Vietnamese DeFi landscape.
Why DeFi API Integration Matters
The Fragmented Data Problem
DeFi doesn't live on one chain, one protocol, or one standard. A single user's portfolio might span Ethereum mainnet, Arbitrum, Optimism, BSC, Polygon, and Solana—with positions in Aave, Uniswap, Curve, GMX, and dozens of smaller protocols. Querying this data directly via RPC nodes means:
- Running your own archive nodes (expensive, complex)
- Handling different RPC methods per chain
- Parsing raw logs and events for every protocol
- Managing rate limits across dozens of endpoints
DeFi APIs solve this by providing a unified abstraction layer. They index on-chain data, normalize it across chains and protocols, and serve it via REST, GraphQL, or WebSocket interfaces.
Vietnamese Context: A Unique Market
Vietnam ranks #3 globally in crypto adoption (Chainalysis 2024), with DeFi participation driven by:
- High yield farming and AMM usage
- VNDC (VND-pegged stablecoin) deployed across multiple chains
- Local protocols: Kyber Network (VN-founded), Coin98 ecosystem
- Mobile-first users integrating with Zalo/Telegram for notifications
Any DeFi API integration targeting Vietnam must account for VNDC tracking, local protocol coverage, and mobile-optimized data delivery.
Understanding DeFi Data Architecture
Before choosing a provider, understand the data layers:
Layer 1: Raw On-Chain Data (RPC Nodes)
- Source: Ethereum, BSC, Polygon, Solana RPC endpoints
- Access: JSON-RPC (
eth_call,eth_getLogs,eth_getTransactionReceipt) - Best for: Real-time transaction submission, low-level contract interaction
- Limitation: No indexing, no historical aggregation, rate-limited
Layer 2: Indexing Layer (The Graph, Custom Indexers)
- Source: Subgraphs mapping contract events to GraphQL schemas
- Access: GraphQL queries over indexed data
- Best for: Complex protocol-specific queries (e.g., "all Uniswap V3 positions for address X")
- Limitation: Deployment complexity, sync latency, query costs
Layer 3: Aggregation APIs (Covalent, Moralis, Alchemy Enhanced, Zapper)
- Source: Pre-computed, normalized data across 100+ chains
- Access: REST endpoints (
/balances,/transactions,/nfts,/defi/positions) - Best for: Multi-chain portfolio views, historical analytics, rapid prototyping
- Limitation: Less granular than raw RPC, potential staleness (seconds to minutes)
Layer 4: Analytics APIs (DeFi Llama, DefiPulse, Token Terminal)
- Source: Protocol-level TVL, revenue, fee data
- Access: REST for market-wide metrics
- Best for: Dashboard analytics, protocol comparison, research
- Limitation: No user-level data
Major DeFi API Providers Comparison
| Provider | Type | Chains | Key Endpoints | Free Tier | Best For |
|---|---|---|---|---|---|
| Alchemy | RPC + Enhanced | 7+ EVM | getTokenBalances, getAssetTransfers, getNfts | 300M CU/mo | Ethereum-focused apps, real-time webhooks |
| QuickNode | RPC + Add-ons | 25+ | Token API, NFT API, Streams | 10M req/mo | Multi-chain RPC, custom add-ons |
| Infura | RPC + Core | 10+ | Basic RPC, Eth API | 100k req/day | Baseline RPC reliability |
| Covalent | Aggregation | 100+ | Class A/B/C: balances, txs, log events | 1M req/mo (Class A) | Unified multi-chain, historical depth |
| Moralis | Aggregation | EVM + Solana | Web3 API, Streams API, Token API | 40k req/day | Cross-chain, real-time streams |
| The Graph | Indexing | 40+ | Subgraph GraphQL | 100K credits/day | Protocol-specific complex queries |
| Zapper | Aggregation | 10+ | /positions, /balances, /apps | Custom | DeFi position aggregation |
| DeFi Llama | Analytics | 150+ | /tvl, /yields, /protocols | Free | Market analytics, TVL tracking |
Deep Dive: Provider Categories
RPC + Enhanced (Alchemy, QuickNode, Infura)
These start as raw RPC providers but add Enhanced APIs—pre-indexed endpoints for common patterns:
alchemy_getTokenBalances— all ERC-20 balances in one callalchemy_getAssetTransfers— normalized transfer history (ERC-20, ERC-721, native)quicknode_tokenAPI— token metadata, holders, transfers
Use when: You need real-time guarantees, webhook notifications, or Ethereum-mainnet depth.
Aggregation APIs (Covalent, Moralis, Zapper)
Unified REST across chains with normalized schemas: ` GET /v1/{chain}/address/{address}/balances_v2/ GET /v1/{chain}/address/{address}/transactions_v2/ GET /v1/{chain}/address/{address}/nfts/ ` Covalent's "Class A" endpoints (balances, transactions) are free up to 1M req/mo. Class B (log events, DEX trades) costs more.
Use when: Building multi-chain portfolio trackers, need historical data fast, want single API for 50+ chains.
The Graph (Subgraphs)
Deploy or query protocol-specific subgraphs: `graphql query { positions(where: { user: "0x...", protocol: "aave-v3" }) { token { symbol } suppliedAmount borrowAmount apy } } ` Best for deep protocol queries. Hosted Service is sunsetting—migrate to Decentralized Network (pay-per-query).
Use when: You need protocol-internal state (collateral ratios, health factors, position snapshots).
Choosing the Right Provider for Your Use Case
Decision Matrix
| Your Priority | Recommended Primary | Fallback |
|---|---|---|
| Real-time tx notifications | Alchemy (webhooks) | QuickNode Streams |
| Multi-chain breadth (50+ chains) | Covalent | Moralis |
| Protocol-specific depth (Aave, Uniswap V3) | The Graph (subgraphs) | Covalent Class B |
| Solana + EVM unified | Moralis | QuickNode (Solana RPC) |
| Cost-sensitive prototype | Covalent Free / Alchemy Free | QuickNode Free |
| Production scale (1B+ req/mo) | Custom enterprise | Multi-provider abstraction |
Cost Reality Check
Provider pricing is non-linear. A portfolio tracker polling 500 users every 30 seconds:
- 500 users × 2,880 polls/day = 1.44M requests/day = ~43M requests/month
- Free tiers (100M–300M CU) cover prototyping only
- Growth tiers: $49–299/mo for 1–10B CU
- Key insight: Most tutorials ignore this. Plan for Growth tier from Day 1 if you have >100 active users.
Rate Limits & Scaling Strategy
| Provider | Free Limit | Burst | Scaling Approach |
|---|---|---|---|
| Alchemy | 300M CU/mo, 100 req/s | 100 req/s | Exponential backoff + fallback |
| QuickNode | 10M req/mo, 25 req/s | 25 req/s | Request queuing |
| Covalent | 1M Class A/mo | 5 req/s | Batch requests, cache aggressively |
| The Graph | 100K credits/day | N/A | Query optimization, pagination |
Production pattern: Implement a provider abstraction layer with:
- Priority-ordered fallback chain (Primary → Secondary → Tertiary)
- Request routing by data type (balances → Covalent, txs → Alchemy, analytics → DeFi Llama)
- Unified TypeScript interfaces across providers
Hands-On: Building a DeFi Portfolio Tracker
Let's build a production-ready TypeScript portfolio tracker that fetches token balances, protocol positions, and calculates USD value across multiple chains.
Project Setup
`bash mkdir defi-portfolio-tracker && cd defi-portfolio-tracker npm init -y npm install axios zod dotenv npm install -D typescript ts-node @types/node npx tsc --init --target ES2020 --module commonjs --strict --esModuleInterop --outDir dist `
Type-Safe Provider Interfaces
`typescript // types/providers.ts import { z } from 'zod';
// Unified balance response across providers export const BalanceResponseSchema = z.object({ address: z.string(), chainId: z.number(), tokens: z.array(z.object({ contractAddress: z.string(), symbol: z.string(), name: z.string(), decimals: z.number(), balance: z.string(), // raw (wei) balanceFormatted: z.string(), // human-readable usdValue: z.number().optional(), logoUrl: z.string().optional(), })), fetchedAt: z.number(), // unix timestamp });
export type BalanceResponse = z.infer
// Provider-specific configs export interface ProviderConfig { name: string; apiKey: string; baseUrl: string; rateLimit: { requests: number; windowMs: number }; }
export const PROVIDER_CONFIGS: Recordhttps://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}, rateLimit: { requests: 100, windowMs: 1000 }, }, moralis: { name: 'Moralis', apiKey: process.env.MORALIS_API_KEY!, baseUrl: 'https://deep-index.moralis.io/api/v2.2', rateLimit: { requests: 25, windowMs: 1000 }, }, }; `
Covalent Balance Fetcher (Multi-Chain)
`typescript // providers/covalent.ts import axios from 'axios'; import { BalanceResponse, BalanceResponseSchema, PROVIDER_CONFIGS } from '../types/providers';
export class CovalentProvider { private config = PROVIDER_CONFIGS.covalent; private client = axios.create({ baseURL: this.config.baseUrl, timeout: 10000, });
async getBalances(chainId: number, address: string): Promise/${chainId}/address/${address}/balances_v2/; const params = { key: this.config.apiKey, nft: 'false', 'no-nft-fetch': 'true' };
const response = await this.client.get(url, { params }); const data = response.data.data;
// Transform to unified schema const tokens = data.items .filter((item: any) => item.type === 'cryptocurrency' && parseFloat(item.balance) > 0) .map((item: any) => ({ contractAddress: item.contract_address, symbol: item.contract_ticker_symbol || 'UNKNOWN', name: item.contract_name || 'Unknown Token', decimals: item.contract_decimals, balance: item.balance, balanceFormatted: (parseFloat(item.balance) / Math.pow(10, item.contract_decimals)).toFixed(4), usdValue: item.quote || 0, logoUrl: item.logo_url, }));
const result = { address: data.address, chainId, tokens, fetchedAt: Date.now(), };
return BalanceResponseSchema.parse(result); }
async getBalancesMultiChain(address: string, chainIds: number[]): PromiseCovalent error on chain ${chainId}:, e); } } return results; } } `
Alchemy Enhanced API (Ethereum Focus)
`typescript // providers/alchemy.ts import axios from 'axios'; import { BalanceResponse, BalanceResponseSchema, PROVIDER_CONFIGS } from '../types/providers';
export class AlchemyProvider { private config = PROVIDER_CONFIGS.alchemy; private client = axios.create({ baseURL: this.config.baseUrl, timeout: 10000, });
async getTokenBalances(address: string): Promise
const response = await this.client.post('', payload); const tokens = response.data.result.tokenBalances .filter((t: any) => t.tokenBalance !== '0x0') .map((t: any) => ({ contractAddress: t.contractAddress, symbol: '', // Need separate metadata call name: '', decimals: 18, balance: t.tokenBalance, balanceFormatted: '0', usdValue: 0, }));
// Batch fetch metadata (omitted for brevity - use alchemy_getTokenMetadata)
const result = { address, chainId: 1, // Ethereum mainnet tokens, fetchedAt: Date.now(), };
return BalanceResponseSchema.parse(result); } } `
Provider Abstraction Layer with Fallback
`typescript // core/provider-registry.ts import { BalanceResponse, ProviderConfig } from '../types/providers'; import { CovalentProvider } from '../providers/covalent'; import { AlchemyProvider } from '../providers/alchemy';
export type ProviderName = 'covalent' | 'alchemy' | 'moralis';
export interface ProviderAdapter { name: ProviderName; getBalances(chainId: number, address: string): Promise
export class ProviderRegistry { private providers: Map
register(name: ProviderName, adapter: ProviderAdapter) { this.providers.set(name, adapter); }
// Route request to best provider for the data type async getBalancesWithFallback(chainId: number, address: string): Promise
for (const name of this.priorityOrder) { const provider = this.providers.get(name); if (!provider) continue;
try { const healthy = await provider.healthCheck(); if (!healthy) throw new Error(${name} health check failed);
return await provider.getBalances(chainId, address); } catch (e) { errors.push(e as Error); console.warn(${name} failed, trying next:, (e as Error).message); } }
throw new Error(All providers failed: ${errors.map(e => e.message).join('; ')}); }
// Multi-chain: route to aggregation provider (Covalent) async getMultiChainBalances(address: string, chainIds: number[]): Promise`
Protocol Position Fetcher (Aave, Uniswap, etc.)
`typescript // providers/protocol-positions.ts import axios from 'axios'; import { z } from 'zod';
export const PositionSchema = z.object({ protocol: z.string(), chainId: z.number(), positionType: z.enum(['lending', 'borrowing', 'lp', 'staking', 'farming']), tokens: z.array(z.object({ symbol: z.string(), address: z.string(), amount: z.string(), usdValue: z.number(), })), apy: z.number().optional(), healthFactor: z.number().optional(), // for lending fetchedAt: z.number(), });
export type ProtocolPosition = z.infer
export class ProtocolPositionFetcher { private covalentBase = 'https://api.covalenthq.com/v1'; private apiKey: string;
constructor(apiKey: string) { this.apiKey = apiKey; }
// Aave V3 positions via Covalent's DEX/DeFi endpoints async getAavePositions(chainId: number, address: string): Promise${this.covalentBase}/${chainId}/address/${address}/transactions_v2/; const params = { key: this.apiKey, 'protocol': 'aave' };
// Parse Aave supply/borrow events from transactions // (Simplified - production uses dedicated Aave subgraph) return []; }
// Uniswap V3 LP positions async getUniswapV3Positions(chainId: number, address: string): Promise
// Generic DeFi positions via Zapper or Covalent async getAllPositions(address: string, chainIds: number[]): Promise`
Main Application: Portfolio Aggregator
`typescript // index.ts import { ProviderRegistry } from './core/provider-registry'; import { CovalentProvider } from './providers/covalent'; import { AlchemyProvider } from './providers/alchemy'; import { ProtocolPositionFetcher } from './providers/protocol-positions'; import { BalanceResponse } from './types/providers';
const SUPPORTED_CHAINS = [ { id: 1, name: 'Ethereum' }, { id: 137, name: 'Polygon' }, { id: 42161, name: 'Arbitrum' }, { id: 10, name: 'Optimism' }, { id: 56, name: 'BSC' }, { id: 43114, name: 'Avalanche' }, ];
async function main() { const address = process.argv[2] || '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb3'; // Example
// Initialize provider registry with fallback chain const registry = new ProviderRegistry(); registry.register('covalent', new CovalentProvider()); registry.register('alchemy', new AlchemyProvider());
console.log(\n🔍 Fetching portfolio for ${address}...\n);
// Multi-chain balances via Covalent (primary for breadth) const chainIds = SUPPORTED_CHAINS.map(c => c.id); const allBalances = await registry.getMultiChainBalances(address, chainIds);
// Aggregate results let totalUsd = 0; const chainBreakdown: Record
for (const balance of allBalances) { const chain = SUPPORTED_CHAINS.find(c => c.id === balance.chainId); const chainName = chain?.name || Chain ${balance.chainId}; let chainUsd = 0;
console.log(\n📊 ${chainName} (${balance.tokens.length} tokens):); for (const token of balance.tokens) { const usd = token.usdValue || 0; chainUsd += usd; totalUsd += usd; if (usd > 0.01) { // Filter dust console.log( ${token.symbol}: ${token.balanceFormatted} ($${usd.toFixed(2)})); } } chainBreakdown[chainName] = { usd: chainUsd, tokens: balance.tokens.length }; }
console.log(\n💰 Total Portfolio Value: $${totalUsd.toFixed(2)}); console.log('\n📈 Chain Breakdown:'); for (const [chain, data] of Object.entries(chainBreakdown)) { console.log( ${chain}: $${data.usd.toFixed(2)} (${data.tokens} tokens)); }
// Protocol positions (optional) const positionFetcher = new ProtocolPositionFetcher(process.env.COVALENT_API_KEY!); const positions = await positionFetcher.getAllPositions(address, chainIds);
if (positions.length > 0) { console.log('\n🏦 Protocol Positions:'); for (const pos of positions) { console.log( ${pos.protocol} (${pos.positionType}): $${pos.tokens.reduce((s, t) => s + t.usdValue, 0).toFixed(2)}); } } }
main().catch(console.error); `
Production Best Practices
Error Handling & Retries
`typescript // utils/retry.ts export async function withRetry
for (let attempt = 0; attempt <= options.maxRetries; attempt++) { try { return await fn(); } catch (e) { lastError = e as Error;
// Don't retry on 4xx errors (client errors) if (axios.isAxiosError(e) && e.response?.status && e.response.status >= 400 && e.response.status < 500) { throw e; }
if (attempt < options.maxRetries) { const delay = Math.min( options.baseDelayMs Math.pow(2, attempt) + Math.random() 1000, options.maxDelayMs ); console.warn(Attempt ${attempt + 1} failed: ${e.message}. Retrying in ${delay}ms...); await new Promise(r => setTimeout(r, delay)); } } }
throw lastError!; } `
Caching Strategy
`typescript // utils/cache.ts interface CacheEntry
export class TTLCache
constructor(defaultTtlMs = 60000) { // 1 minute default this.defaultTtlMs = defaultTtlMs; }
get(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry.data; }
set(key: string, data: T, ttlMs?: number): void { this.cache.set(key, { data, expiresAt: Date.now() + (ttlMs || this.defaultTtlMs), }); }
// Stale-while-revalidate: return stale data while fetching fresh async getOrFetch(key: string, fetcher: () => Promise
// Fetch fresh const fresh = await fetcher(); this.set(key, fresh, ttlMs); return fresh; } }
// Usage const balanceCache = new TTLCachebalances:${chainId}:${address}, () => provider.getBalances(chainId, address), 30000 ); `
Rate Limit Management
`typescript // utils/rate-limiter.ts export class TokenBucketRateLimiter { private tokens: number; private lastRefill: number;
constructor( private capacity: number, private refillRate: number // tokens per second ) { this.tokens = capacity; this.lastRefill = Date.now(); }
async take(): Promise
if (this.tokens >= 1) { this.tokens -= 1; return; }
// Wait for next token const waitMs = (1 - this.tokens) / this.refillRate * 1000; await new Promise(r => setTimeout(r, waitMs)); return this.take(); }
private refill() { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate); this.lastRefill = now; } }
// Per-provider limiters const limiters = { covalent: new TokenBucketRateLimiter(5, 5), // 5 req/s alchemy: new TokenBucketRateLimiter(100, 100), moralis: new TokenBucketRateLimiter(25, 25), };
// Wrap provider calls async function rateLimitedCall(provider: string, fn: () => Promise`
Monitoring & Alerting
`typescript // monitoring/metrics.ts interface ApiMetrics { provider: string; endpoint: string; latencyMs: number; status: 'success' | 'error'; timestamp: number; errorCode?: string; }
const metrics: ApiMetrics[] = [];
export function recordMetric(metric: ApiMetrics) { metrics.push(metric); // Keep last 10000 if (metrics.length > 10000) metrics.shift();
// Alert on high error rate const recent = metrics.filter(m => m.timestamp > Date.now() - 60000); const errors = recent.filter(m => m.status === 'error').length; if (errors > 10) { console.error(⚠️ ALERT: ${errors} errors in last minute for ${metric.provider}); // Send to alerting system (PagerDuty, Slack, etc.) } }
// Wrapper async function monitoredCall(provider: string, endpoint: string, fn: () => Promise`
Cost Optimization
- Batch requests — Covalent supports batch endpoints; Alchemy supports
eth_callbatching - Cache aggressively — Balance data rarely changes per block; 30-60s TTL is safe
- Use WebSockets for real-time — Alchemy/QuickNode websockets push updates vs polling
- Request only needed fields — GraphQL (The Graph) lets you select exact fields
- Monitor CU usage — Alchemy dashboard shows compute units per endpoint
Vietnamese Market Considerations
Regulatory Landscape (2024)
Vietnam currently treats cryptocurrency as "not legal tender" but not explicitly banned. Key points:
- No licensing framework for DeFi protocols yet
- Banks prohibited from crypto services (SBV Directive 2018)
- Individuals can hold/trade; tax guidance unclear
- Implication: APIs don't need KYC for read-only data, but fiat on-ramp integrations (Momo, ZaloPay) require licensed partners
VND Stablecoin Data
VNDC (VND-pegged) exists on:
- Ethereum (0x...), BSC, Polygon, Tron, Solana, HECO
- Track via standard ERC-20 balance endpoints
- Critical: Verify contract addresses per chain—avoid impersonator tokens
Vietnam-Specific Protocols
| Protocol | Type | Chain Coverage | API Notes |
|---|---|---|---|
| Kyber Network | DEX Aggregator | Ethereum, BSC, Polygon, Avalanche | KyberSwap API for quotes/swaps |
| Coin98 Wallet | Multi-chain Wallet | 40+ chains | Coin98 Extension API for portfolio |
| KardiaChain | L1 (VN-focused) | Native + EVM | KAI token, KardiaChain RPC |
| VNDC | Stablecoin | 6+ chains | Standard ERC-20, track via Covalent/Alchemy |
Mobile-First Data Delivery
Vietnamese users are predominantly mobile:
- Use Server-Sent Events (SSE) or WebSockets for push updates (lower battery than polling)
- Compress payloads (gzip, protocol buffers)
- Implement offline-first caching (IndexedDB/AsyncStorage)
- Zalo/Telegram bot integration for price alerts
FAQ
1. What's the difference between RPC nodes and DeFi APIs?
RPC nodes give you raw blockchain access (eth_call, eth_getLogs). You parse everything yourself. DeFi APIs (Covalent, Alchemy Enhanced, Moralis) give you pre-indexed, normalized data—balances, transactions, NFTs, protocol positions—via simple REST/GraphQL. Use RPC for transaction submission; use DeFi APIs for reading data.
2. How do I handle rate limits when building a DeFi dashboard?
Implement three layers: (1) Client-side cache (React Query/SWR with 30-60s staleTime), (2) Server-side cache (Redis with TTL), (3) Request queue with token bucket per provider. Use webhooks (Alchemy) or WebSockets for real-time updates instead of polling.
3. Which DeFi API is best for historical data?
Covalent excels at historical: Class A endpoints (balances, transactions) go back to genesis on 100+ chains. The Graph subgraphs provide protocol-specific history (e.g., every Aave borrow event). Alchemy getAssetTransfers covers Ethereum mainnet history well.
4. Can I use free tiers for production DeFi apps?
No. Free tiers (100M–300M CU/month) cover prototyping only. A modest portfolio tracker with 500 users polling every 30s = ~1.4B requests/month = Growth tier minimum ($49-299/mo). Plan for paid tiers from Day 1.
5. How do I get liquidity pool positions via API?
Uniswap V3: Query The Graph subgraph (positions entity). Curve/Convex: Use Covalent Class B log_events with pool contract addresses. Generic: Zapper /positions endpoint or DeFi Llama /pools. For custom protocols, deploy a subgraph.
6. What's the best way to aggregate data from multiple chains?
Use an aggregation API (Covalent/Moralis) as primary for breadth, with provider abstraction layer routing specific queries to specialists (Alchemy for Ethereum real-time, The Graph for protocol depth). Never hardcode one provider—build fallback chains.
Summary
DeFi API integration isn't a single API call—it's an architecture decision. The production pattern:
- Abstraction layer — Unified interfaces, provider-agnostic
- Fallback chain — Primary → Secondary → Tertiary per data type
- Type-safe contracts — Zod schemas validate every response
- Caching + rate limiting — Respect limits, serve stale-while-revalidate
- Monitoring — Track latency, errors, costs per provider
- Vietnam context — VNDC tracking, local protocols, mobile optimization
Start with Covalent for multi-chain breadth, add Alchemy for Ethereum real-time, use The Graph for protocol depth. Build the abstraction layer first—it pays off when (not if) you need to switch providers.
Next steps: Explore the provider abstraction pattern for a complete reference implementation with tests, Docker config, and CI/CD pipeline.
