Stream crypto prices with DexPaprika's free SSE streaming tutorial

Mateusz Sroka

29 Dec 2025 (about 1 month ago)

22 min read

Share:

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 SSE streaming tutorial

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

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:EventSource API 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

  1. Client opens long-lived HTTP connection to server
  2. Server keeps connection open indefinitely
  3. Server pushes data as text/event-stream whenever prices update
  4. Client receives updates via an event listener (e.g. addEventListener('t_p', ...))
  5. Connection automatically reconnects on network errors

API endpoints

GET /stream?method=t_p&chain=ethereum&address=0x...

Parameters:

  • method=t_p - Token price streaming
  • chain - Blockchain network (ethereum, solana, base, etc.)
  • address - Token contract address
  • limit - (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 structure
  • styles.css - Visual styling
  • index.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

  1. Basic connection (10 lines, works immediately)
  2. Add error handling and reconnection (robust)
  3. Multi-token tracking (scale to ~6 tokens in-browser)
  4. Build UI (HTML/CSS integration)
  5. Optimize performance (throttling, batching)
  6. 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.js

Open 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.js

Create 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 method
  • chain=ethereum - Which blockchain
  • address=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.js

Expected 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

  1. Automatic reconnection on disconnect
  2. Exponential backoff (don't overwhelm server during outages)
  3. Retry limits (stop after N failed attempts)
  4. Error logging (for debugging)
  5. 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.js

Test reconnection:

  1. Let it connect successfully
  2. Disconnect your WiFi for 5 seconds
  3. You should see:
    [ERROR] Connection reconnecting (readyState: 0)
    [INFO] Reconnecting in 1000ms (attempt 1/10)...
    [INFO] Reconnecting in 2000ms (attempt 2/10)...
  4. Reconnect WiFi
  5. Should resume: [INFO] Connected successfully

Test graceful shutdown:

Press Ctrl+C:

^C
[INFO] Shutting down gracefully...
[INFO] Connection closed

No 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:

  1. Multiple GET connections (simple, max ~6 tokens per domain)
  2. 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.js

Expected 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 structure
  • styles.css - Styling and animations
  • index.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:8000

Checkpoint: 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

  1. DOM manipulation is expensive (reflow/repaint)
  2. Updating UI on every message (10 updates/sec) causes jank
  3. JSON parsing overhead
  4. 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-parse

Optimization 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:

  1. Start recording
  2. Let dashboard run for 60 seconds
  3. Stop recording
  4. Check FPS (should be 60)
  5. 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.app

Option 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-dashboard

Add 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:

  1. Verify token address on block explorer (Etherscan, Solscan)
  2. Check chain name matches exactly (ethereum not eth)
  3. Test with a different network (try mobile hotspot)
  4. Check DevTools Console for errors

Issue 2: "Connection keeps disconnecting"

Symptoms: Connects, then immediately disconnects

Solutions:

  1. Use HTTP server (python3 -m http.server), not file:// protocol
  2. Check browser support: https://caniuse.com/eventsource
  3. Try different network (corporate firewalls may block SSE)
  4. Check error messages in console

Issue 3: "High CPU usage"

Symptoms: Browser tab using 50%+ CPU

Solutions:

  1. Implement batching (update every 200ms, not every message)
  2. Remove excessive console.log statements
  3. Use Chrome DevTools Performance tab to profile
  4. Reduce number of tracked tokens

Issue 4: "Memory grows over time"

Symptoms: Memory usage increases continuously

Solutions:

  1. Call eventSource.close() on cleanup
  2. Clear all intervals: clearInterval(updateTimer)
  3. Don't store historical prices (store only latest)
  4. Profile memory in DevTools Memory tab

Issue 5: "Module import errors in Node.js"

Symptoms:Cannot use import statement outside a module

Solutions:

  1. Prefer CommonJS in Node examples: use require() instead of import
  2. If you want ESM, add "type": "module" to package.json (or run npm 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

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

Built with DexPaprika Streaming API
Free real-time crypto price streaming. See supported networks for the complete list of available blockchains.

Related articles

Latest articles

Coinpaprika education

Discover practical guides, definitions, and deep dives to grow your crypto knowledge.

Go back to Education