Skip to main content

Build a Price Ticker

Create a real-time cryptocurrency price ticker that shows live prices, 24h changes, and auto-refreshes.

What you'll build:

  • Multi-token price display
  • 24h change indicators
  • Sparkline mini-charts
  • Auto-refresh every 10 seconds
  • Responsive design

Time: ~30 minutes
Difficulty: Beginner


Demoโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ๐Ÿ’ฐ Crypto Prices Last update: 12s โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ BTC $97,542.18 โ–ฒ +2.45% โ–ˆโ–ˆโ–ˆโ–ˆโ–โ–‚โ–ƒโ–…โ–ˆโ–ˆโ–ˆ โ”‚
โ”‚ ETH $2,847.32 โ–ฒ +1.23% โ–‚โ–ƒโ–…โ–ˆโ–ˆโ–…โ–ƒโ–‚โ–โ–‚โ–ƒ โ”‚
โ”‚ SOL $142.87 โ–ผ -0.89% โ–ˆโ–ˆโ–ˆโ–…โ–ƒโ–‚โ–โ–‚โ–ƒโ–…โ–ˆ โ”‚
โ”‚ USDC $1.00 โ— +0.01% โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ– โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Step 1: HTML Structureโ€‹

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crypto Price Ticker</title>
<style>
:root {
--bg: #0a0a0a;
--card: #141414;
--border: #262626;
--text: #fafafa;
--muted: #888;
--green: #22c55e;
--red: #ef4444;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}

.ticker {
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
width: 100%;
max-width: 500px;
overflow: hidden;
}

.ticker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}

.ticker-title {
font-size: 1.25rem;
font-weight: 600;
}

.last-update {
color: var(--muted);
font-size: 0.85rem;
}

.price-row {
display: grid;
grid-template-columns: 80px 1fr 100px;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
transition: background 0.2s;
}

.price-row:last-child {
border-bottom: none;
}

.price-row:hover {
background: rgba(255, 255, 255, 0.03);
}

.symbol {
font-weight: 600;
font-size: 1.1rem;
}

.price {
font-size: 1.25rem;
font-weight: 500;
font-family: 'SF Mono', 'Monaco', monospace;
}

.change {
text-align: right;
font-weight: 500;
}

.change.positive { color: var(--green); }
.change.negative { color: var(--red); }
.change.neutral { color: var(--muted); }

.sparkline {
display: flex;
align-items: flex-end;
gap: 2px;
height: 20px;
margin-top: 4px;
}

.sparkline-bar {
width: 4px;
background: var(--muted);
border-radius: 1px;
transition: height 0.3s;
}

.loading {
padding: 3rem;
text-align: center;
color: var(--muted);
}

.error {
padding: 1rem 1.5rem;
color: var(--red);
text-align: center;
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

.updating .price {
animation: pulse 1s infinite;
}
</style>
</head>
<body>
<div class="ticker" id="ticker">
<div class="ticker-header">
<span class="ticker-title">๐Ÿ’ฐ Crypto Prices</span>
<span class="last-update" id="last-update">Loading...</span>
</div>
<div id="prices">
<div class="loading">Fetching prices...</div>
</div>
</div>

<script type="module" src="ticker.js"></script>
</body>
</html>

Step 2: JavaScript Logicโ€‹

Create ticker.js:

const API_BASE = 'https://api.web3identity.com';

// Tokens to track
const TOKENS = ['BTC', 'ETH', 'SOL', 'USDC', 'ARB', 'OP'];

// Price history for sparklines (last 10 data points)
const priceHistory = {};
TOKENS.forEach(t => priceHistory[t] = []);

// DOM elements
const pricesEl = document.getElementById('prices');
const lastUpdateEl = document.getElementById('last-update');

// Fetch prices
async function fetchPrices() {
try {
const symbols = TOKENS.join(',');
const res = await fetch(`${API_BASE}/api/price/batch?symbols=${symbols}`);

if (!res.ok) {
throw new Error(`API error: ${res.status}`);
}

const data = await res.json();
return data.prices;
} catch (error) {
console.error('Failed to fetch prices:', error);
throw error;
}
}

// Update price history for sparklines
function updateHistory(prices) {
for (const [symbol, data] of Object.entries(prices)) {
if (!priceHistory[symbol]) priceHistory[symbol] = [];

priceHistory[symbol].push(data.price);

// Keep only last 10 data points
if (priceHistory[symbol].length > 10) {
priceHistory[symbol].shift();
}
}
}

// Generate sparkline bars
function generateSparkline(symbol) {
const history = priceHistory[symbol] || [];
if (history.length < 2) {
return '<div class="sparkline"></div>';
}

const min = Math.min(...history);
const max = Math.max(...history);
const range = max - min || 1;

const bars = history.map(price => {
const height = Math.max(4, ((price - min) / range) * 20);
return `<div class="sparkline-bar" style="height: ${height}px;"></div>`;
}).join('');

return `<div class="sparkline">${bars}</div>`;
}

// Format price
function formatPrice(price) {
if (price >= 1000) {
return price.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
return price.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: price < 1 ? 4 : 2,
});
}

// Format change percentage
function formatChange(change) {
const sign = change >= 0 ? '+' : '';
return `${sign}${change.toFixed(2)}%`;
}

// Get change class
function getChangeClass(change) {
if (change > 0.1) return 'positive';
if (change < -0.1) return 'negative';
return 'neutral';
}

// Get change arrow
function getChangeArrow(change) {
if (change > 0.1) return 'โ–ฒ';
if (change < -0.1) return 'โ–ผ';
return 'โ—';
}

// Render prices
function render(prices) {
const html = TOKENS.map(symbol => {
const data = prices[symbol];
if (!data) {
return `
<div class="price-row">
<span class="symbol">${symbol}</span>
<span class="price">--</span>
<span class="change neutral">N/A</span>
</div>
`;
}

const changeClass = getChangeClass(data.change24h);
const arrow = getChangeArrow(data.change24h);

return `
<div class="price-row">
<span class="symbol">${symbol}</span>
<div>
<div class="price">${formatPrice(data.price)}</div>
${generateSparkline(symbol)}
</div>
<span class="change ${changeClass}">
${arrow} ${formatChange(data.change24h)}
</span>
</div>
`;
}).join('');

pricesEl.innerHTML = html;
}

// Update last update time
let lastUpdateTime = null;

function updateTimestamp() {
if (!lastUpdateTime) {
lastUpdateEl.textContent = 'Loading...';
return;
}

const seconds = Math.floor((Date.now() - lastUpdateTime) / 1000);
lastUpdateEl.textContent = `${seconds}s ago`;
}

// Main update function
async function update() {
try {
// Add updating class for visual feedback
pricesEl.classList.add('updating');

const prices = await fetchPrices();
updateHistory(prices);
render(prices);

lastUpdateTime = Date.now();
updateTimestamp();

} catch (error) {
pricesEl.innerHTML = `
<div class="error">
Failed to load prices. Retrying...
</div>
`;
} finally {
pricesEl.classList.remove('updating');
}
}

// Initialize
async function init() {
// Initial fetch
await update();

// Refresh prices every 10 seconds
setInterval(update, 10000);

// Update timestamp every second
setInterval(updateTimestamp, 1000);
}

init();

Step 3: Run Itโ€‹

npx serve .

Open http://localhost:3000 and watch the prices update!


Step 4: Enhancementsโ€‹

Add More Tokensโ€‹

// Expand the token list
const TOKENS = [
'BTC', 'ETH', 'SOL', 'USDC', 'ARB', 'OP',
'MATIC', 'AVAX', 'LINK', 'UNI', 'AAVE', 'SNX'
];

Add Search/Filterโ€‹

// Add to HTML
<input type="text" id="search" placeholder="Search tokens...">

// Add filtering logic
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toUpperCase();
const rows = document.querySelectorAll('.price-row');

rows.forEach(row => {
const symbol = row.querySelector('.symbol').textContent;
row.style.display = symbol.includes(query) ? '' : 'none';
});
});

Add Price Alertsโ€‹

// Simple price alert system
const alerts = {
BTC: { above: 100000, below: 90000 },
ETH: { above: 3000, below: 2500 },
};

function checkAlerts(prices) {
for (const [symbol, thresholds] of Object.entries(alerts)) {
const price = prices[symbol]?.price;
if (!price) continue;

if (thresholds.above && price > thresholds.above) {
notify(`${symbol} is above $${thresholds.above}!`);
}
if (thresholds.below && price < thresholds.below) {
notify(`${symbol} is below $${thresholds.below}!`);
}
}
}

function notify(message) {
if (Notification.permission === 'granted') {
new Notification('Price Alert', { body: message });
}
}

Embed as Widgetโ€‹

<!-- Embed on any page -->
<iframe
src="https://your-domain.com/ticker.html"
width="500"
height="400"
frameborder="0"
style="border-radius: 16px;"
></iframe>

API Endpoints Usedโ€‹

EndpointPurposeRate
/api/price/batchMultiple token prices1 call per update

API Usage: ~6 calls/minute = ~8,640 calls/day
Free tier: 100 calls/day (use caching in production!)


Production Tipsโ€‹

Add Cachingโ€‹

// Server-side caching to reduce API calls
const cache = {
prices: null,
timestamp: 0,
TTL: 10000, // 10 seconds
};

async function getCachedPrices() {
if (cache.prices && Date.now() - cache.timestamp < cache.TTL) {
return cache.prices;
}

cache.prices = await fetchPrices();
cache.timestamp = Date.now();
return cache.prices;
}

Use WebSocket (If Available)โ€‹

For production apps, consider WebSocket connections for real-time updates rather than polling.


Full Source Codeโ€‹

See the complete example on GitHub.