# 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.*

**Author:** Mateusz Sroka
**Category:** Developer Guides
**Source:** [View on Coinpaprika](/pt/education/stream-crypto-prices-with-DexPaprika%27s-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](#part-1-connect-to-the-streaming-api-with-basic-sse) to get streaming working in 5 minutes.



## Table of contents



- [Why use streaming instead of polling?](#why-use-streaming-instead-of-polling-for-crypto-prices)

- [How does DexPaprika streaming work?](#how-does-dexpaprika-streaming-api-work)

- [Architecture & planning](#architecture-planning)

- [Part 1: Connect to the streaming API (basic quickstart)](#part-1-connect-to-the-streaming-api-with-basic-sse)

- [Part 2: Robust connection](#part-2-robust-connection-with-error-handling)

- [Part 3: Stream multiple tokens](#part-3-stream-multiple-tokens-with-dashboard-logic)

- [Part 4: Build the UI](#part-4-build-the-dashboard-ui-with-html-css-and-javascript)

- [Part 5: Performance optimization](#part-5-performance-optimization)

- [Deployment](#deploy-your-streaming-dashboard)

- [Troubleshooting](#troubleshooting-common-issues)

- [FAQ](#frequently-asked-questions)




#### 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](https://api.dexpaprika.com/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



- Client opens long-lived HTTP connection to server

- Server keeps connection open indefinitely

- Server pushes data as `text/event-stream` whenever 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 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](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



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



- 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.js`
**Test 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 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:**



- **Multiple GET connections** (simple, max ~6 tokens per domain)

- **Server-side batching** (for larger watchlists, see [DexPaprika docs](https://docs.dexpaprika.com/streaming))



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



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



- 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.app`
### Option 2: Netlify


Drag and drop your project folder to [netlify.com/drop](https://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:**



- Verify token address on block explorer (Etherscan, Solscan)

- Check chain name matches exactly (`ethereum` not `eth`)

- 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`), not `file://` protocol

- Check browser support: [https://caniuse.com/eventsource](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 of `import`

- 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](https://docs.dexpaprika.com/streaming) 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](https://docs.dexpaprika.com/api-reference)

- Supported networks: [https://api.dexpaprika.com/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](https://docs.dexpaprika.com/streaming) 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](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](https://docs.dexpaprika.com/streaming)

- API reference: [https://docs.dexpaprika.com/api-reference](https://docs.dexpaprika.com/api-reference)

- Supported networks: [https://api.dexpaprika.com/networks](https://api.dexpaprika.com/networks)



 



**Built with DexPaprika Streaming API**
Free real-time crypto price streaming. See [supported networks](https://api.dexpaprika.com/networks) for the complete list of available blockchains.

