Build a Wallet Tracker
Create a wallet portfolio tracker that shows balances, token values, and ENS identity โ all from a single address.
What you'll build:
- Address input with ENS resolution
- Token balance display with USD values
- Portfolio total value
- Recent transactions
- Auto-refresh functionality
Time: ~45 minutes
Difficulty: Beginner to Intermediate
Demoโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ Wallet Tracker โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Address: [vitalik.eth____________] [Track] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ค vitalik.eth โ
โ ๐ 0xd8dA6BF...96045 โ
โ ๐ฐ Total: $1,234,567.89 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Tokens Value โ
โ โโ 125.5 ETH $357,342.16 โ
โ โโ 50,000 USDC $50,000.00 โ
โ โโ 1,000,000 UNI $8,250,000.00 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Recent Transactions โ
โ โโ Received 10 ETH from nick.eth โ
โ โโ Sent 1,000 USDC to dao.eth โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 1: Project Setupโ
mkdir wallet-tracker
cd wallet-tracker
npm init -y
npm install @atv-eth/x402-sdk
Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wallet Tracker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #0a0a0a;
color: #fafafa;
padding: 2rem;
}
.container { max-width: 600px; margin: 0 auto; }
h1 { margin-bottom: 1.5rem; }
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #141414;
color: #fafafa;
font-size: 1rem;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: #3b82f6;
color: white;
font-size: 1rem;
cursor: pointer;
}
button:hover { background: #2563eb; }
button:disabled { background: #666; cursor: not-allowed; }
.card {
background: #141414;
border: 1px solid #262626;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.identity { display: flex; align-items: center; gap: 1rem; }
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: #333;
}
.name { font-size: 1.5rem; font-weight: 600; }
.address { color: #888; font-family: monospace; }
.total { font-size: 2rem; font-weight: 700; color: #22c55e; }
.token-list { margin-top: 1rem; }
.token {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #262626;
}
.token:last-child { border-bottom: none; }
.token-value { color: #22c55e; }
.error { color: #ef4444; padding: 1rem; }
.loading { color: #888; }
</style>
</head>
<body>
<div class="container">
<h1>๐ Wallet Tracker</h1>
<div class="input-group">
<input type="text" id="address-input" placeholder="Enter address or ENS name (e.g., vitalik.eth)">
<button id="track-btn" onclick="trackWallet()">Track</button>
</div>
<div id="results"></div>
</div>
<script type="module" src="app.js"></script>
</body>
</html>
Step 2: Core Tracking Logicโ
Create app.js:
const API_BASE = 'https://api.web3identity.com';
// State
let currentAddress = null;
let refreshInterval = null;
// DOM Elements
const input = document.getElementById('address-input');
const results = document.getElementById('results');
const trackBtn = document.getElementById('track-btn');
// Track wallet on Enter key
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') trackWallet();
});
// Main tracking function
async function trackWallet() {
const addressOrName = input.value.trim();
if (!addressOrName) return;
trackBtn.disabled = true;
results.innerHTML = '<div class="loading">Loading...</div>';
try {
// Step 1: Resolve address (handles both ENS and raw addresses)
const address = await resolveAddress(addressOrName);
currentAddress = address;
// Step 2: Fetch all data in parallel
const [identity, balances, prices] = await Promise.all([
fetchIdentity(address),
fetchBalances(address),
fetchPrices(balances?.tokens?.map(t => t.symbol) || ['ETH']),
]);
// Step 3: Calculate values and render
const portfolio = calculatePortfolio(balances, prices);
renderWallet(identity, portfolio);
// Step 4: Set up auto-refresh
startAutoRefresh();
} catch (error) {
results.innerHTML = `<div class="error">Error: ${error.message}</div>`;
} finally {
trackBtn.disabled = false;
}
}
// Resolve ENS name or validate address
async function resolveAddress(input) {
// If it's an ENS name
if (input.includes('.')) {
const res = await fetch(`${API_BASE}/api/ens/resolve/${input}`);
if (!res.ok) throw new Error(`Could not resolve ${input}`);
const data = await res.json();
return data.address;
}
// If it's an address
if (/^0x[a-fA-F0-9]{40}$/.test(input)) {
return input;
}
throw new Error('Invalid address or ENS name');
}
// Fetch identity (ENS name, avatar)
async function fetchIdentity(address) {
try {
// Try reverse lookup
const res = await fetch(`${API_BASE}/api/ens/reverse/${address}`);
if (res.ok) {
const data = await res.json();
if (data.name) {
// Fetch full profile for avatar
const profileRes = await fetch(`${API_BASE}/api/ens/${data.name}`);
if (profileRes.ok) {
return await profileRes.json();
}
}
}
} catch (e) {
console.warn('Could not fetch identity:', e);
}
// Return basic identity if no ENS
return { address, name: null, avatar: null };
}
// Fetch token balances
async function fetchBalances(address) {
const res = await fetch(`${API_BASE}/api/wallet/${address}/balances`);
if (!res.ok) {
// Return empty balances if endpoint fails
return { tokens: [] };
}
return await res.json();
}
// Fetch prices for tokens
async function fetchPrices(symbols) {
if (!symbols || symbols.length === 0) return {};
const uniqueSymbols = [...new Set(symbols)].slice(0, 20);
const symbolsParam = uniqueSymbols.join(',');
const res = await fetch(`${API_BASE}/api/price/batch?symbols=${symbolsParam}`);
if (!res.ok) return {};
const data = await res.json();
return data.prices || {};
}
// Calculate portfolio values
function calculatePortfolio(balances, prices) {
const tokens = (balances.tokens || []).map(token => {
const price = prices[token.symbol]?.price || 0;
const value = parseFloat(token.balance) * price;
return { ...token, price, value };
});
// Sort by value descending
tokens.sort((a, b) => b.value - a.value);
const totalValue = tokens.reduce((sum, t) => sum + t.value, 0);
return { tokens, totalValue };
}
// Render wallet UI
function renderWallet(identity, portfolio) {
const shortAddress = identity.address
? `${identity.address.slice(0, 6)}...${identity.address.slice(-4)}`
: '';
results.innerHTML = `
<div class="card">
<div class="identity">
${identity.avatar
? `<img class="avatar" src="${identity.avatar}" alt="Avatar">`
: '<div class="avatar"></div>'
}
<div>
<div class="name">${identity.name || 'Unknown'}</div>
<div class="address">${shortAddress}</div>
</div>
</div>
</div>
<div class="card">
<div class="total">$${formatNumber(portfolio.totalValue)}</div>
<div style="color: #888; margin-top: 0.5rem;">Total Portfolio Value</div>
<div class="token-list">
${portfolio.tokens.map(token => `
<div class="token">
<div>
<strong>${formatBalance(token.balance)} ${token.symbol}</strong>
<div style="color: #888; font-size: 0.85rem;">
$${formatNumber(token.price)} per token
</div>
</div>
<div class="token-value">$${formatNumber(token.value)}</div>
</div>
`).join('')}
${portfolio.tokens.length === 0 ? '<div style="color: #888;">No tokens found</div>' : ''}
</div>
</div>
`;
}
// Format number with commas
function formatNumber(num) {
return num.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
// Format token balance
function formatBalance(balance) {
const num = parseFloat(balance);
if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
if (num >= 1000) return (num / 1000).toFixed(2) + 'K';
return num.toFixed(4);
}
// Auto-refresh every 30 seconds
function startAutoRefresh() {
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
if (currentAddress) {
console.log('Auto-refreshing...');
trackWallet();
}
}, 30000);
}
// Make trackWallet available globally
window.trackWallet = trackWallet;
Step 3: Run Itโ
# Serve with any static server
npx serve .
# or
python -m http.server 8000
Open http://localhost:8000 and try tracking vitalik.eth!
Step 4: Enhancementsโ
Add Transaction Historyโ
async function fetchTransactions(address) {
const res = await fetch(`${API_BASE}/api/wallet/${address}/transactions?limit=5`);
if (!res.ok) return [];
const data = await res.json();
return data.transactions || [];
}
// Add to renderWallet():
const txHtml = transactions.map(tx => `
<div class="token">
<div>
${tx.type === 'receive' ? '๐ฅ Received' : '๐ค Sent'}
${tx.value} ${tx.symbol}
</div>
<div style="color: #888">${formatTimeAgo(tx.timestamp)}</div>
</div>
`).join('');
Add Price Change Indicatorsโ
function renderPriceChange(change24h) {
const color = change24h >= 0 ? '#22c55e' : '#ef4444';
const arrow = change24h >= 0 ? 'โ' : 'โ';
return `<span style="color: ${color}">${arrow} ${Math.abs(change24h).toFixed(2)}%</span>`;
}
Add Loading Skeletonโ
function renderSkeleton() {
return `
<div class="card">
<div class="identity">
<div class="avatar" style="animation: pulse 2s infinite;"></div>
<div>
<div style="width: 150px; height: 24px; background: #333; border-radius: 4px;"></div>
<div style="width: 100px; height: 16px; background: #333; border-radius: 4px; margin-top: 8px;"></div>
</div>
</div>
</div>
`;
}
API Endpoints Usedโ
| Endpoint | Purpose |
|---|---|
/api/ens/resolve/{name} | Resolve ENS to address |
/api/ens/reverse/{address} | Get ENS from address |
/api/ens/{name} | Full ENS profile with avatar |
/api/wallet/{address}/balances | Token balances |
/api/price/batch | Multiple token prices |
/api/wallet/{address}/transactions | Transaction history |
Next Stepsโ
- Add wallet connection (wagmi/RainbowKit)
- Support multiple chains (pass
chainparameter) - Add NFT display
- Implement price alerts
- Add portfolio history chart
Full Source Codeโ
See the complete example on GitHub.