Stream crypto prices with DexPaprika's free SSE streaming tutorial
Tracking 50 tokens with 1-second polling? That's 4.3 million API requests per day - and most responses show no price change. DexPaprika's free streaming API eliminates the waste: one connection, unlimited updates, ~1 second latency, zero authentication. This tutorial shows you how to build it from scratch with robust error handling and automatic reconnection.

Stream crypto prices with DexPaprika's free API: build a real-time dashboard
Stream real-time DEX token prices using DexPaprika's Streaming API (SSE). Build a robust price dashboard tracking ~6 tokens in-browser (and learn scaling patterns for more) across Ethereum, Solana, and Base in 30–45 minutes. No authentication or API keys required.
If you need a real-time crypto price API for dashboards, tickers, and portfolio UIs, streaming beats polling (fewer requests, simpler client logic).
Ready to start? Jump to Part 1 to get streaming working in 5 minutes.
Table of contents
- Why use streaming instead of polling?
- How does DexPaprika streaming work?
- Architecture & planning
- Part 1: Connect to the streaming API (basic quickstart)
- Part 2: Robust connection
- Part 3: Stream multiple tokens
- Part 4: Build the UI
- Part 5: Performance optimization
- Deployment
- Troubleshooting
- FAQ
What you'll build
- Real-time crypto price dashboard for ~6 tokens (browser-friendly)
- Automatic reconnection on network errors
- Live price change animations (green up, red down)
- Robust error handling with exponential backoff
- Responsive UI working on mobile and desktop
Prerequisites
- Node.js 18+ installed
- Basic JavaScript knowledge (variables, functions, async/await)
- Text editor (VS Code, Sublime Text, etc.)
- 30-45 minutes of focused time
Time estimate: 30-45 minutes for complete implementation
If you've used infrastructure providers for streaming, DexPaprika provides free, public SSE-based streaming for DEX prices. See supported networks.
Why use streaming instead of polling for crypto prices?
You want to show live crypto prices in your app. How do you keep them updated?
Approach 1: Polling
The traditional approach is to request prices repeatedly:
// Polling approach - request prices every second
setInterval(async () => {
const response = await fetch('https://api.example.com/price');
const data = await response.json();
updatePrice(data.price);
}, 1000);Let's calculate the cost:
- 50 tokens × 1 request/second = 50 requests/second
- 50 requests/sec × 60 sec × 60 min × 24 hours = 4,320,000 requests/day
Problems with polling:
- Rate limits: Most APIs limit requests (often 100-1000/day for free tiers)
- Latency: 500ms-1.5s delay between request and response
- Wasted bandwidth: Most responses show no price change
- Server load: Millions of unnecessary requests
Approach 2: Streaming
Streaming inverts the model - the server pushes updates when prices change:
// Streaming approach - server pushes updates
const eventSource = new EventSource('https://streaming.example.com');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updatePrice(data.price);
};Important note: Some SSE servers emit named events (they include event: <name> in the stream). In that case, onmessage may not fire and you should use addEventListener('<name>', handler) instead. DexPaprika streaming uses named events like t_p, so in this tutorial we'll use addEventListener('t_p', ...).
Benefits:
- Single persistent connection
- Server sends updates as they occur
- ~1 second latency
- Bandwidth efficient (only sends when prices change)
Why Server-Sent Events (SSE)?
SSE is simpler than WebSocket for our use case:
- Native browser support:
EventSourceAPI built into all modern browsers - Unidirectional: Perfect for price feeds (server -> client only)
- Automatic reconnection: Built into the protocol
- Text-based: Easy to debug in DevTools Network tab
- HTTP-based: Works through firewalls and proxies
When streaming makes sense:
- Real-time dashboards
- Portfolio trackers
- Price tickers
- Live data feeds
When it doesn't:
- Historical data analysis (use REST API)
- Ultra-low latency trading (<100ms required - SSE is ~1s)
- Bidirectional communication (use WebSocket instead)
How does DexPaprika streaming API work?
DexPaprika streaming is a free, public Server-Sent Events (SSE) API that provides real-time token prices from DEX liquidity pools.
Key features
- Free to use: No API keys, no authentication, no signup required
- Fair use policy: Free access subject to reasonable usage (you may see 429 responses during capacity events)
- Multi-chain: Supports Ethereum, Solana, Base, Arbitrum, Polygon, and more
- Real DEX data: Prices from actual liquidity pools, not centralized exchanges
- Scaling patterns: Stream single-token prices via SSE; see docs for server-side batching patterns when you need many tokens
Technical specifications
- Protocol: HTTP/1.1 Server-Sent Events (text/event-stream)
- Base URL:
https://streaming.dexpaprika.com - Latency: ~1 second per update
- Event name: The stream emits named events like
t_p(token price) - Data format (observed): JSON with short keys
a(address),c(chain),p(price),t(timestamp) - Rate limits: None published; fair use expected (429 responses possible during capacity issues)
How SSE works
- Client opens long-lived HTTP connection to server
- Server keeps connection open indefinitely
- Server pushes data as
text/event-streamwhenever prices update - Client receives updates via an event listener (e.g.
addEventListener('t_p', ...)) - Connection automatically reconnects on network errors
API endpoints
GET /stream?method=t_p&chain=ethereum&address=0x...Parameters:
method=t_p- Token price streamingchain- Blockchain network (ethereum, solana, base, etc.)address- Token contract addresslimit- (optional) Auto-close after N events
Response format
The server sends SSE frames that look like this:
event: t_p
data: {"a":"0xa0b8...","c":"ethereum","p":"1.0000...","t":1766523892}When you parse the JSON from event.data, you get:
{"a":"0xa0b8...","c":"ethereum","p":"1.0000...","t":1766523892}Supported blockchains
See the live list of supported networks: https://api.dexpaprika.com/networks
Popular chains include Ethereum, Solana, Base, Monad, Plasma, Arbitrum, Polygon, BSC, Optimism, Avalanche, and more.
In the next section, we'll connect to this API and start streaming prices.
Architecture & planning
Before writing code, let's plan what we're building.
What we're building:
A live dashboard tracking ~6 tokens across 3 chains (Ethereum, Solana, Base) with real-time price updates, automatic reconnection, and a responsive UI.
Application architecture:
┌─────────────────────────────────┐
│ DexPaprika SSE Endpoint │
│ (streaming.dexpaprika.com) │
└────────────┬────────────────────┘
│ SSE Connection
│ Price Updates (~1/sec)
▼
┌─────────────────────────────────┐
│ StreamConnection Class │
│ - Handles SSE connection │
│ - Exponential backoff │
│ - Error handling │
└────────────┬────────────────────┘
│ onPriceUpdate()
▼
┌─────────────────────────────────┐
│ Dashboard Controller │
│ - Manages multiple tokens │
│ - Debounces UI updates │
│ - Formats prices │
└────────────┬────────────────────┘
│ updateDOM()
▼
┌─────────────────────────────────┐
│ HTML UI (index.html) │
│ - Price cards for each token │
│ - Connection status indicator │
│ - Price change animations │
└─────────────────────────────────┘Component breakdown
stream-basic.js- Minimal SSE example (10 lines)stream-robust.js- Robust patterns (100 lines)dashboard.js- Multi-token manager (150 lines)index.html- UI structurestyles.css- Visual stylingindex.js- Browser application
Technology stack
- Vanilla JavaScript (ES6+, no frameworks)
- Native EventSource API (browser SSE support)
- CSS Grid for responsive layout
- No build tools required (runs directly)
Development flow
- Basic connection (10 lines, works immediately)
- Add error handling and reconnection (robust)
- Multi-token tracking (scale to ~6 tokens in-browser)
- Build UI (HTML/CSS integration)
- Optimize performance (throttling, batching)
- Deploy (optional)
Let's start coding.
Part 1: Connect to the streaming API with basic SSE
This is the basic quickstart. Get streaming working in 5 minutes with a minimal browser example - you'll see live price updates immediately.
Step 1: Browser setup (CORS proxy)
Why do browsers need a proxy?
Browsers block cross-origin SSE unless the streaming server sends CORS headers. If you connect directly to https://streaming.dexpaprika.com from http://localhost:8000, you'll see: No 'Access-Control-Allow-Origin' header. This proxy runs locally so the browser connects to /stream on the same origin.
Create server.js (local SSE proxy + static server):
/**
* server.js - Local browser-friendly SSE proxy for DexPaprika streaming
*
* - Serves index.html on /
* - Proxies /stream?... to https://streaming.dexpaprika.com/stream?... (same-origin for the browser)
*/
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const PORT = 8000;
const INDEX_PATH = path.join(__dirname, 'index.html');
const UPSTREAM = 'https://streaming.dexpaprika.com/stream';
http
.createServer((req, res) => {
// Serve the static page
if (req.url === '/' || req.url.startsWith('/index.html')) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(fs.readFileSync(INDEX_PATH, 'utf8'));
return;
}
// Proxy SSE stream (same-origin for the browser)
if (req.url.startsWith('/stream')) {
const url = new URL(req.url, `http://${req.headers.host}`);
const upstreamUrl = new URL(UPSTREAM);
upstreamUrl.search = url.search; // pass query string (method/chain/address)
const upstreamReq = https.request(
upstreamUrl,
{ headers: { Accept: 'text/event-stream' } },
(upstreamRes) => {
res.writeHead(upstreamRes.statusCode || 200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
});
upstreamRes.pipe(res);
}
);
upstreamReq.on('error', (err) => {
res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`Upstream error: ${err.message}`);
});
// If the browser tab closes, stop the upstream request too
req.on('close', () => upstreamReq.destroy());
upstreamReq.end();
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
})
.listen(PORT, () => {
console.log(`Open http://localhost:${PORT}`);
});Step 2: Browser SSE client
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>Crypto Price Stream</title>
</head>
<body>
<h1>USDC Price (Ethereum)</h1>
<p id="price">Connecting...</p>
<script>
const eventSource = new EventSource('/stream?method=t_p&chain=ethereum&address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48');
// DexPaprika emits named SSE events like `t_p`, not the default `message`
eventSource.addEventListener('t_p', (event) => {
const data = JSON.parse(event.data);
const price = Number(data.p);
document.getElementById('price').textContent = Number.isFinite(price)
? `$${price}`
: `Bad data: ${event.data}`;
});
eventSource.onerror = () => {
document.getElementById('price').textContent = 'Connection error - reconnecting...';
};
</script>
</body>
</html>Step 3: Run it
Start the server:
node server.jsOpen http://localhost:8000 and confirm:
- You see live USDC prices updating every ~1 second
- Status changes to Connected
If you see "Connected" but no price: Leave it open for ~10–20 seconds (ticks depend on market activity). Check DevTools -> Network to confirm the SSE request shows streaming data.
Step 4: Node.js version (optional)
For server-side or testing, create a Node.js version:
# Create project directory
mkdir crypto-dashboard
cd crypto-dashboard
# Initialize npm
npm init -y
# Install EventSource for Node.js
npm install eventsource
# Create code file
touch stream-basic.jsCreate stream-basic.js:
/**
* stream-basic.js - Basic DexPaprika streaming connection
*/
const EventSource = require('eventsource');
// Configuration
const CHAIN = 'ethereum';
const TOKEN_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; // USDC
const TOKEN_SYMBOL = 'USDC';
// Build streaming URL
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${CHAIN}&address=${TOKEN_ADDRESS}`;
console.log('Connecting to DexPaprika streaming...');
// Create SSE connection
const eventSource = new EventSource(url);
// Handle incoming price updates (named event: t_p)
eventSource.addEventListener('t_p', (event) => {
const data = JSON.parse(event.data);
// Observed payload keys: a=address, c=chain, p=price, t=timestamp
console.log(`[DATA] ${TOKEN_SYMBOL} (${data.c}): $${data.p}`);
});
// Handle errors
eventSource.onerror = () => {
console.error('Connection error - EventSource will auto-reconnect');
};
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nClosing connection...');
eventSource.close();
process.exit(0);
});Step 5: Understanding the code
What's happening:
EventSource constructor
const eventSource = new EventSource(url);Opens an HTTP connection to the streaming endpoint. The connection stays open indefinitely.
URL parameters
method=t_p- Token price streaming methodchain=ethereum- Which blockchainaddress=0x...- Token contract address (USDC on Ethereum)
Event listener (named SSE event)
eventSource.addEventListener('t_p', (event) => {
const data = JSON.parse(event.data);
console.log(`${TOKEN_SYMBOL}: $${data.p}`);
});Fires every time the server sends a token-price update (~1 per second). We parse the JSON and log the price.
onerror handler
eventSource.onerror = () => {
console.error('Connection error - EventSource will auto-reconnect');
};Handles connection errors. EventSource automatically attempts to reconnect - we just log the status.
Step 6: Run and verify (Node.js)
Run the code:
node stream-basic.jsExpected output:
Connecting to DexPaprika streaming...
USDC (ethereum): $1.0002
USDC (ethereum): $1.0003
USDC (ethereum): $1.0002
USDC (ethereum): $1.0003
^C
Closing connection...Checkpoint: You should see price updates logging every ~1 second. If not:
- Check internet connection
- Verify token address is correct
- Ensure Node.js 18+ is installed:
node --version - Check for error messages in output
What you learned
- How to create an SSE connection with EventSource
- How to handle incoming messages
- How to parse JSON price data
- How to gracefully shut down connections
Browser version works identically:
The same code works in browsers with native EventSource (no npm package needed). Open the HTML file from Step 1 and check DevTools Network tab - you'll see a single SSE connection with data streaming in.
Part 2: Robust connection with error handling
The basic example works, but if you want to have more polished app, you'll need robust error handling and automatic reconnection.
Why error handling matters:
- Networks drop connections (WiFi, mobile data, server restarts)
- Messages can be malformed (rare but possible)
- Servers have maintenance windows
- Real-world apps must recover automatically
Real-world implementation requirements
- Automatic reconnection on disconnect
- Exponential backoff (don't overwhelm server during outages)
- Retry limits (stop after N failed attempts)
- Error logging (for debugging)
- Graceful shutdown (cleanup on exit)
Step 1: Robust streaming class
Create stream-robust.js:
/**
* stream-robust.js - Robust streaming
*/
const EventSource = require('eventsource');
const CONFIG = {
baseUrl: 'https://streaming.dexpaprika.com/stream',
chain: 'ethereum',
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
tokenSymbol: 'USDC',
maxRetries: 10,
baseDelay: 1000, // 1 second
maxDelay: 30000 // 30 seconds max
};
class StreamConnection {
constructor(config) {
this.config = config;
this.eventSource = null;
this.retries = 0;
this.isShuttingDown = false;
}
connect() {
const url = `${this.config.baseUrl}?method=t_p&chain=${this.config.chain}&address=${this.config.tokenAddress}`;
console.log(`[INFO] Connecting to ${this.config.chain} streaming...`);
this.eventSource = new EventSource(url);
this.eventSource.onopen = () => {
console.log(`[INFO] Connected successfully (attempt ${this.retries + 1})`);
this.retries = 0; // Reset on successful connection
};
this.eventSource.addEventListener('t_p', (event) => {
try {
const data = JSON.parse(event.data);
// Validate data - price is a numeric string
const price = Number(data.p);
if (!Number.isFinite(price)) {
throw new Error('Invalid price data received');
}
console.log(`[DATA] ${this.config.tokenSymbol}: $${price.toFixed(6)}`);
} catch (error) {
console.error('[ERROR] Failed to process message:', error.message);
}
});
this.eventSource.onerror = () => {
const state = this.eventSource.readyState;
const stateText = state === 0 ? 'connecting' : state === 1 ? 'open' : state === 2 ? 'closed' : 'unknown';
console.error(`[ERROR] Connection ${stateText} (readyState: ${state})`);
this.handleError();
};
}
handleError() {
this.eventSource?.close();
if (this.isShuttingDown) {
return;
}
if (this.retries >= this.config.maxRetries) {
console.error(`[ERROR] Max retries (${this.config.maxRetries}) reached. Giving up.`);
return;
}
this.retries++;
// Exponential backoff: 1s → 2s → 4s → 8s → ... → max 30s
const delay = Math.min(
this.config.baseDelay * Math.pow(2, this.retries - 1),
this.config.maxDelay
);
console.log(`[INFO] Reconnecting in ${delay}ms (attempt ${this.retries}/${this.config.maxRetries})...`);
setTimeout(() => this.connect(), delay);
}
close() {
this.isShuttingDown = true;
if (this.eventSource) {
this.eventSource.close();
console.log('[INFO] Connection closed');
}
}
}
// Initialize connection
const stream = new StreamConnection(CONFIG);
stream.connect();
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n[INFO] Shutting down gracefully...');
stream.close();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n[INFO] Received SIGTERM, shutting down...');
stream.close();
process.exit(0);
});Step 2: Understanding exponential backoff
The reconnection logic uses exponential backoff to avoid overwhelming the server:
const delay = Math.min(
this.config.baseDelay * Math.pow(2, this.retries - 1),
this.config.maxDelay
);How it works:
- Attempt 1: 1s delay (1000 × 2^0)
- Attempt 2: 2s delay (1000 × 2^1)
- Attempt 3: 4s delay (1000 × 2^2)
- Attempt 4: 8s delay (1000 × 2^3)
- Attempt 5: 16s delay (1000 × 2^4)
- Attempts 6+: 30s delay (capped at maxDelay)
Why exponential backoff?
During a server outage, if 1000 clients all retry every second, that's 1000 requests/second hitting the recovering server. Exponential backoff spreads out the load, giving the server time to stabilize.
Step 3: Connection lifecycle
The class handles all connection states:
onopen - Connection established successfully
- Resets retry counter
- Logs success message
t_p event - Price update received
- Parses JSON
- Validates data structure (accepts string OR number for price)
- Logs price update
- Handles parse errors with try/catch
onerror - Connection error occurred
- Logs readyState for debugging (CONNECTING=0, OPEN=1, CLOSED=2)
- Closes current connection
- Checks if shutting down (don't reconnect)
- Checks retry limit
- Calculates backoff delay
- Schedules reconnection
Step 4: Testing error scenarios
Run the robust example:
node stream-robust.jsTest reconnection:
- Let it connect successfully
- Disconnect your WiFi for 5 seconds
- You should see:
[ERROR] Connection reconnecting (readyState: 0) [INFO] Reconnecting in 1000ms (attempt 1/10)... [INFO] Reconnecting in 2000ms (attempt 2/10)... - Reconnect WiFi
- Should resume:
[INFO] Connected successfully
Test graceful shutdown:
Press Ctrl+C:
^C
[INFO] Shutting down gracefully...
[INFO] Connection closedNo error messages, clean exit.
Checkpoint: Your robust streaming should:
- Connect and show price updates
- Automatically reconnect on network errors
- Show increasing delays (1s, 2s, 4s, 8s...)
- Shut down cleanly with Ctrl+C
What you learned
- Production error handling patterns
- Exponential backoff algorithm
- Retry limits to prevent infinite loops
- Process signal handling (SIGINT, SIGTERM)
- Data validation before processing (accept string OR number)
- Proper error logging with readyState
Common mistakes to avoid
❌ Immediate retry without backoff:
// BAD - hammers server during outages
eventSource.onerror = () => {
connect(); // Retries instantly
};✅ Exponential backoff:
// GOOD - respects server during recovery
eventSource.onerror = () => {
const delay = calculateBackoff(retries);
setTimeout(() => connect(), delay);
};❌ No retry limit:
// BAD - retries forever, wastes resources
while (true) {
try { connect(); } catch { retry(); }
}✅ Max retries:
// GOOD - gives up after reasonable attempts
if (retries >= maxRetries) {
console.error('Max retries reached');
return;
}Part 3: Stream multiple tokens with dashboard logic
Now let's scale from 1 token to multiple tokens in the browser.
The challenge:
How do you track multiple tokens? Create separate connections?
Browser connection limits:
Browsers limit concurrent HTTP/1.1 connections per origin (commonly ~6). Since each token uses one SSE connection, you'll typically be able to track ~6 tokens per origin in a single page.
Solutions:
- Multiple GET connections (simple, max ~6 tokens per domain)
- Server-side batching (for larger watchlists, see DexPaprika docs)
For this tutorial, we'll use multiple GET connections (browser-compatible, no server needed).
Step 1: Define token list
Create dashboard.js:
/**
* dashboard.js - Multi-token dashboard manager
*/
// Token configuration
const TOKENS = [
{ symbol: 'WETH', chain: 'ethereum', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
{ symbol: 'USDC', chain: 'ethereum', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
{ symbol: 'USDT', chain: 'ethereum', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' },
{ symbol: 'WETH', chain: 'base', address: '0x4200000000000000000000000000000000000006' },
{ symbol: 'USDC', chain: 'base', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
{ symbol: 'SOL', chain: 'solana', address: 'So11111111111111111111111111111111111111112' }
];Step 2: Dashboard manager class
class DashboardManager {
constructor(tokens, updateInterval = 200) {
this.tokens = tokens;
this.connections = new Map();
this.priceData = new Map();
this.updateInterval = updateInterval;
this.updateTimer = null;
}
initialize() {
console.log(`[INFO] Dashboard initializing with ${this.tokens.length} tokens`);
// Subscribe to each token
this.tokens.forEach(token => {
this.subscribe(token);
});
// Start UI update batching
this.startUIUpdates();
}
subscribe(token) {
// Keep IDs stable for UI lookups: <chain>-<symbol>
const tokenId = `${token.chain}-${token.symbol}`;
console.log(`[INFO] Subscribing to ${token.symbol} (${token.chain})`);
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${token.chain}&address=${token.address}`;
const eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log(`[INFO] Connected: ${token.symbol} (${token.chain})`);
};
eventSource.addEventListener('t_p', (event) => {
try {
const data = JSON.parse(event.data);
this.handlePriceUpdate(tokenId, token, data);
} catch (error) {
console.error(`[ERROR] Failed to parse data for ${tokenId}:`, error.message);
}
});
eventSource.onerror = () => {
console.error(`[ERROR] Connection error for ${token.symbol} - auto-reconnecting`);
};
this.connections.set(tokenId, { eventSource, token });
}
handlePriceUpdate(tokenId, token, data) {
const currentData = this.priceData.get(tokenId);
const price = Number(data.p);
// Validate price
if (!Number.isFinite(price)) {
console.error(`[ERROR] Invalid price for ${tokenId}: ${data.p}`);
return;
}
// Calculate price change
let changePercent = 0;
if (currentData && currentData.price) {
changePercent = ((price - currentData.price) / currentData.price) * 100;
}
this.priceData.set(tokenId, {
token,
price,
prevPrice: currentData?.price || price,
changePercent,
timestamp: data.t,
lastUpdate: Date.now()
});
}
startUIUpdates() {
this.updateTimer = setInterval(() => {
if (this.priceData.size > 0) {
this.flushToUI();
}
}, this.updateInterval);
}
flushToUI() {
console.log(`\n[UPDATE] ${new Date().toLocaleTimeString()}`);
this.priceData.forEach((data, tokenId) => {
const changeIcon = data.changePercent > 0 ? '▲' : data.changePercent < 0 ? '▼' : '●';
const changeColor = data.changePercent > 0 ? '+' : '';
console.log(
` ${data.token.symbol} (${data.token.chain}): $${data.price.toFixed(6)} ` +
`${changeIcon} ${changeColor}${data.changePercent.toFixed(2)}%`
);
});
}
closeAll() {
if (this.updateTimer) {
clearInterval(this.updateTimer);
}
this.connections.forEach(({ eventSource }) => {
eventSource.close();
});
this.connections.clear();
this.priceData.clear();
console.log('[INFO] All connections closed');
}
}
// Initialize dashboard
const dashboard = new DashboardManager(TOKENS);
dashboard.initialize();
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n[INFO] Shutting down dashboard...');
dashboard.closeAll();
process.exit(0);
});Step 3: Understanding the architecture
Connection pooling
- Each token gets its own EventSource connection
- Connections stored in Map (tokenId -> connection)
- Independent error handling per token
State management
- Price data stored in Map (tokenId -> priceData)
- Tracks current price, previous price, change percent
- Timestamps for last update
Batched updates (throttling)
- Updates arrive every ~1 second per token
- Instead of updating UI 10 times/second, batch into 1 update every 200ms
- Reduces DOM operations, improves performance
Step 4: Run the dashboard
node dashboard.jsExpected output:
[INFO] Dashboard initializing with 6 tokens
[INFO] Subscribing to WETH (ethereum)
[INFO] Subscribing to USDC (ethereum)
[INFO] Subscribing to USDT (ethereum)
[INFO] Subscribing to WETH (base)
[INFO] Subscribing to USDC (base)
[INFO] Subscribing to SOL (solana)
[INFO] Connected: WETH (ethereum)
[INFO] Connected: USDC (ethereum)
(additional connection messages)
[UPDATE] 2:45:23 PM
WETH (ethereum): $2890.456123 ▲ +0.12%
USDC (ethereum): $1.000234 ▼ -0.01%
USDT (ethereum): $0.999876 ● +0.00%
WETH (base): $2890.512345 ▲ +0.15%
USDC (base): $1.000123 ▼ -0.02%
SOL (solana): $98.456789 ▲ +1.23%Checkpoint: Your dashboard should:
- Connect to all 6 tokens
- Show batched updates every 200ms
- Calculate price change percentages
- Display up/down/neutral indicators
- Handle individual connection errors gracefully
What you learned
- Connection pooling pattern
- State management for multiple data streams
- Batching/throttling for performance
- Map data structures for efficient lookups
- Independent error handling per connection
Performance considerations
- Each token is one long-lived HTTP connection; keep the browser-origin limit in mind.
- Batch UI updates (100–500ms) instead of updating the DOM on every tick.
- Measure with DevTools (Performance + Memory) and tune based on your actual token count and device targets.
Part 4: Build the dashboard UI with HTML, CSS, and JavaScript
Now let's add a visual interface to our streaming dashboard.
UI requirements
- Price cards for each token
- Connection status indicator
- Price change animations (green up, red down)
- Responsive layout (mobile/desktop)
- Dark theme
The complete code files are in the code-examples/ directory:
index.html- HTML structurestyles.css- Styling and animationsindex.js- Browser application logic
Key features
1. Price cards with animations
.price-value.price-up {
color: #4ade80;
animation: flash-green 0.5s ease;
}
@keyframes flash-green {
0% { color: #4ade80; transform: scale(1); }
50% { color: #86efac; transform: scale(1.05); }
100% { color: #ffffff; transform: scale(1); }
}2. Connection status indicator
<div class="status-indicator">
<span class="status-dot" id="status-dot"></span>
<span class="status-text" id="status-text">Connecting...</span>
</div>updateConnectionStatus(status) {
const dot = document.getElementById('status-dot');
dot.className = `status-dot ${status}`; // 'connected', 'reconnecting', 'error'
}3. Responsive grid layout
.price-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.price-grid {
grid-template-columns: 1fr;
}
}Running the dashboard
# Start local HTTP server
python3 -m http.server 8000
# Or use npm script
npm run dev
# Open in browser
open http://localhost:8000Checkpoint: Your dashboard should:
- Display all tokens in grid layout
- Show live prices updating every ~1 second
- Animate price changes (flash green/red)
- Display connection status (green dot = connected)
- Reconnect automatically on network errors
- Work on both desktop and mobile
What you learned
- SSE integration with DOM
- Real-time UI updates
- CSS animations for data changes
- Responsive design with CSS Grid
- Production-ready browser applications
Part 5: Performance optimization
For real-world apps, performance matters. Let's optimize our dashboard.
Performance bottlenecks
- DOM manipulation is expensive (reflow/repaint)
- Updating UI on every message (10 updates/sec) causes jank
- JSON parsing overhead
- Memory leaks from unclosed connections
Optimization 1: Throttle DOM updates
Already implemented in dashboard.js:
startUIUpdates() {
// Batch updates every 200ms instead of every message
this.updateTimer = setInterval(() => {
this.flushToUI();
}, this.updateInterval); // 200ms
}Impact:
- Before: 10 updates/sec per token = 100 DOM operations/sec
- After: 5 batched updates/sec = 5 DOM operations/sec
- Result: 20x fewer DOM operations
Optimization 2: Efficient DOM updates
Use textContent instead of innerHTML:
// FAST
priceElement.textContent = `$${price}`;
// SLOW
priceElement.innerHTML = `$${price}`; // Causes full re-parseOptimization 3: Memory management
// Close connections when done
window.addEventListener('beforeunload', () => {
app.destroy(); // Closes all EventSource connections
});
destroy() {
// Clear interval
clearInterval(this.updateTimer);
// Close all connections
this.connections.forEach(({ eventSource }) => {
eventSource.close();
});
// Clear maps
this.connections.clear();
this.priceData.clear();
}Measuring performance
Open Chrome DevTools -> Performance tab:
- Start recording
- Let dashboard run for 60 seconds
- Stop recording
- Check FPS (should be 60)
- Check memory (should be stable)
Checkpoint: Optimized dashboard should:
- Feel smooth during live updates (no noticeable jank)
- Have stable memory over time (no continuous growth)
- Keep CPU usage reasonable for your target devices (measure in DevTools)
Deploy your streaming dashboard
Deploy your dashboard for public access.
Option 1: Vercel (recommended)
# Install Vercel CLI
npm install -g vercel
# Deploy
cd crypto-dashboard
vercel
# Follow prompts
# Get live URL: https://your-dashboard.vercel.appOption 2: Netlify
Drag and drop your project folder to netlify.com/drop
Option 3: GitHub Pages
# Push to GitHub
git init
git add .
git commit -m "Initial commit"
git push origin main
# Enable GitHub Pages in repository settings
# URL: https://username.github.io/crypto-dashboardAdd footer
<footer class="footer">
<p>
Powered by
<a href="https://docs.dexpaprika.com/streaming">DexPaprika Streaming API</a>
- Free real-time crypto price streaming
</p>
<p>
<a href="https://docs.dexpaprika.com/api-reference">API Docs</a> •
<a href="https://api.dexpaprika.com/networks">Supported Networks</a>
</p>
</footer>These links create backlinks to DexPaprika documentation.
Troubleshooting common issues
Issue 1: "No price updates showing"
Symptoms: Connection opens but no messages received
Solutions:
- Verify token address on block explorer (Etherscan, Solscan)
- Check chain name matches exactly (
ethereumnoteth) - Test with a different network (try mobile hotspot)
- Check DevTools Console for errors
Issue 2: "Connection keeps disconnecting"
Symptoms: Connects, then immediately disconnects
Solutions:
- Use HTTP server (
python3 -m http.server), notfile://protocol - Check browser support: https://caniuse.com/eventsource
- Try different network (corporate firewalls may block SSE)
- Check error messages in console
Issue 3: "High CPU usage"
Symptoms: Browser tab using 50%+ CPU
Solutions:
- Implement batching (update every 200ms, not every message)
- Remove excessive console.log statements
- Use Chrome DevTools Performance tab to profile
- Reduce number of tracked tokens
Issue 4: "Memory grows over time"
Symptoms: Memory usage increases continuously
Solutions:
- Call
eventSource.close()on cleanup - Clear all intervals:
clearInterval(updateTimer) - Don't store historical prices (store only latest)
- Profile memory in DevTools Memory tab
Issue 5: "Module import errors in Node.js"
Symptoms:Cannot use import statement outside a module
Solutions:
- Prefer CommonJS in Node examples: use
require()instead ofimport - If you want ESM, add
"type": "module"topackage.json(or runnpm pkg set type=module)
Advanced topics and next steps
Scaling beyond browser limits
For tracking more than 6 tokens in a browser, you'll need server-side batching. See the DexPaprika streaming documentation for batch endpoint details and implementation patterns.
Integration ideas
- Add trading functionality (link to DEX)
- Price alerts (email/SMS on threshold)
- Historical charts (combine with REST API)
- Portfolio tracking (user-selected tokens)
- DeFi integrations (show liquidity, volume)
Related DexPaprika features
- REST API: https://docs.dexpaprika.com/api-reference
- Supported networks: https://api.dexpaprika.com/networks
- Token details:
/networks/{network}/tokens/{address}
Frequently asked questions
Is DexPaprika streaming free?
A: Yes, free to use with a fair use policy. No API key or authentication required. Start streaming immediately without signup. You may encounter 429 responses during capacity events, but normal usage has no published rate limits.
How many tokens can I track simultaneously?
A: In browsers: typically ~6 tokens per origin (because each token is one SSE connection and browsers commonly cap concurrent HTTP/1.1 connections per origin). For larger watchlists, see the streaming documentation for server-side batching patterns.
What's the latency for real-time updates?
A: Approximately 1 second per update across all supported chains. For sub-second latency requirements (<100ms), consider enterprise solutions like QuickNode or Alchemy.
Which blockchains are supported for streaming?
A: See the live list of supported networks: https://api.dexpaprika.com/networks
Popular chains include:
- Ethereum (Network ID:
ethereum) - Solana (Network ID:
solana) - Base (Network ID:
base) - Arbitrum (Network ID:
arbitrum) - Polygon (Network ID:
polygon)
How is DexPaprika different from QuickNode and Alchemy?
A: DexPaprika streaming is a DEX price feed (SSE) that focuses on sending price ticks; QuickNode and Alchemy are node / infrastructure providers (often WebSocket subscriptions for chain events).
- Use DexPaprika: when you want real-time DEX prices with a simple, browser-native protocol and no auth.
- Use infra providers: when you need chain-level subscriptions, enterprise SLAs, or non-price event streams.
Do I need authentication or API keys?
A: No. DexPaprika streaming requires zero authentication. Just connect and start streaming.
What happens if the connection drops?
A: EventSource automatically attempts to reconnect. For live apps, implement exponential backoff (shown in Part 2) to handle reconnection gracefully without overwhelming the server.
Conclusion
You built a complete cryptocurrency price dashboard with real-time streaming.
What you built
- Live dashboard tracking ~6 tokens across multiple blockchains (browser-friendly)
- Automatic error handling with exponential backoff
- Performance-optimized UI with batched updates
- Responsive design working on mobile and desktop
- Complete application ready to deploy
What you learned
- Server-Sent Events (SSE) protocol
- Real-time data streaming patterns
- Production error handling strategies
- Performance optimization for live data
- DOM manipulation best practices
- Deployment to modern hosting platforms
Key takeaways
- Streaming is more efficient than polling for real-time data
- SSE is simpler than WebSocket for unidirectional updates
- DexPaprika provides free streaming (no auth required, fair use policy)
- Production apps need error handling and reconnection
- Performance optimization matters (batching, throttling)
Next steps
- Customize token list for your needs
- Add features (alerts, charts, portfolio tracking)
- Integrate with other DexPaprika APIs
- Build your own crypto application
- Share your project and get feedback
Resources
- DexPaprika streaming: https://docs.dexpaprika.com/streaming
- API reference: https://docs.dexpaprika.com/api-reference
- Supported networks: https://api.dexpaprika.com/networks
Built with DexPaprika Streaming API
Free real-time crypto price streaming. See supported networks for the complete list of available blockchains.
Related articles
Coinpaprika education
Discover practical guides, definitions, and deep dives to grow your crypto knowledge.