Skip to main content

Building a Trading Dashboard with HTML, CSS & JavaScript

This tutorial guides you through building a real-time trading dashboard for DeepBook using Surflux APIs and Server-Sent Events. You'll create a professional interface that displays live order books, price charts, market depth, and real-time trade execution.

What You'll Build

A complete trading dashboard featuring:

  • Real-time candlestick price charts with multiple timeframes
  • Live order book with depth visualization
  • Recent trade history with color-coded buy/sell indicators
  • Market depth chart showing cumulative bid/ask liquidity
  • Server-Sent Events for instant data updates

Prerequisites

  • Basic HTML, CSS, and JavaScript knowledge
  • Understanding of REST APIs
  • Familiarity with trading concepts (order books, candlestick charts)
  • A Surflux API key
Production Note

This tutorial demonstrates a proof of concept using direct frontend API calls. For production applications, proxy all Surflux API requests through your backend to protect your API key and add authentication, rate limiting, and caching.

Architecture Overview

The dashboard connects to multiple Surflux endpoints:

REST APIs (for initial data):

  • /deepbook/get_pools — Pool metadata and trading parameters
  • /deepbook/{poolName}/ohlcv/{timeframe} — Historical candlestick data
  • /deepbook/{poolName}/order-book-depth — Current order book snapshot
  • /deepbook/{poolName}/trades — Recent trade history

SSE Stream (for real-time updates):

  • /deepbook/{poolName}/live-trades — Live order book and trade events

Project Setup

Create a new HTML file and include the required libraries:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepBook Trading Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lightweight-charts.standalone.production.js"></script>
</head>
<body>
<!-- Dashboard content goes here -->
</body>
</html>

We use Tailwind CSS for styling and Lightweight Charts for the price chart visualization.

Fetching Pool Information

First, retrieve pool metadata to get decimals and trading parameters:

// Surflux API endpoints
const API_URL = 'https://api.surflux.dev'; // REST API for data queries
const STREAM_URL = 'https://flux.surflux.dev'; // SSE for real-time updates

let poolData = null;
let baseDecimals = 9;
let quoteDecimals = 6;

async function apiRequest(endpoint, params = {}) {
const url = new URL(API_URL + endpoint);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});

const response = await fetch(url);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}

async function fetchPoolInfo(apiKey, poolName) {
const pools = await apiRequest('/deepbook/get_pools', { 'api-key': apiKey });
poolData = pools.find(p => p.pool_name === poolName);

if (!poolData) {
throw new Error(`Pool ${poolName} not found`);
}

baseDecimals = poolData.base_asset_decimals;
quoteDecimals = poolData.quote_asset_decimals;

console.log(`Loaded pool: ${poolData.pool_name}`);
console.log(`Base: ${poolData.base_asset_symbol} (${baseDecimals} decimals)`);
console.log(`Quote: ${poolData.quote_asset_symbol} (${quoteDecimals} decimals)`);
}

The decimals are crucial for converting between on-chain values (smallest units) and human-readable amounts.

Loading Historical Price Data

Fetch OHLCV (candlestick) data for the price chart:

let chart = null;
let candlestickSeries = null;
let currentTimeframe = '1m';

async function fetchOHLCV(apiKey, poolName, timeframe) {
const to = Math.floor(Date.now() / 1000);
const from = to - 24 * 60 * 60; // Last 24 hours

const data = await apiRequest(`/deepbook/${poolName}/ohlcv/${timeframe}`, {
'api-key': apiKey,
from,
to,
limit: 500
});

const candleData = data.map(candle => ({
time: Math.floor(new Date(candle.timestamp).getTime() / 1000),
open: parseFloat(candle.open) / Math.pow(10, quoteDecimals),
high: parseFloat(candle.high) / Math.pow(10, quoteDecimals),
low: parseFloat(candle.low) / Math.pow(10, quoteDecimals),
close: parseFloat(candle.close) / Math.pow(10, quoteDecimals),
})).reverse();

candlestickSeries.setData(candleData);
chart.timeScale().fitContent();

if (candleData.length > 0) {
const latest = candleData[candleData.length - 1];
const first = candleData[0]?.close || latest.close;
updatePrice(latest.close, first);
}
}

function updatePrice(currentPrice, previousPrice) {
const priceElement = document.getElementById('currentPrice');
const changeElement = document.getElementById('priceChange');

priceElement.textContent = currentPrice.toFixed(4);

const change = ((currentPrice - previousPrice) / previousPrice) * 100;
const isPositive = change >= 0;

priceElement.className = `text-lg lg:text-xl font-semibold ${isPositive ? 'text-green' : 'text-red'}`;
changeElement.textContent = `${isPositive ? '+' : ''}${change.toFixed(2)}%`;
changeElement.className = `text-sm px-2 py-1 rounded ${isPositive ? 'bg-green/10 text-green' : 'bg-red/10 text-red'}`;
}

Available timeframes: 1m, 5m, 15m, 1h, 4h, 1d

Initializing the Price Chart

Set up Lightweight Charts with a candlestick series:

function initCharts() {
const chartOptions = {
layout: {
background: { color: '#13171D' },
textColor: '#848E9C'
},
grid: {
vertLines: { color: '#1E2329' },
horzLines: { color: '#1E2329' }
},
timeScale: {
timeVisible: true,
borderColor: '#1E2329'
},
rightPriceScale: { borderColor: '#1E2329' },
handleScroll: {
mouseWheel: true,
pressedMouseMove: true
}
};

const chartContainer = document.getElementById('chart');
chart = LightweightCharts.createChart(chartContainer, {
...chartOptions,
width: chartContainer.offsetWidth,
height: chartContainer.offsetHeight || 400
});

candlestickSeries = chart.addCandlestickSeries({
upColor: '#0ECB81',
downColor: '#F6465D',
borderVisible: false,
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
});

window.addEventListener('resize', () => {
const container = document.getElementById('chart');
chart.applyOptions({
width: container.offsetWidth,
height: container.offsetHeight || 400
});
});
}

Loading Order Book Data

Fetch the current order book snapshot:

async function fetchOrderBook(apiKey, poolName) {
const data = await apiRequest(`/deepbook/${poolName}/order-book-depth`, {
'api-key': apiKey,
limit: 15
});

updateOrderBook(data);
}

function updateOrderBook(orderBook) {
if (!orderBook.asks || !orderBook.bids) return;

const maxAmount = Math.max(
...orderBook.asks.map(a => parseFloat(a.total_quantity)),
...orderBook.bids.map(b => parseFloat(b.total_quantity))
);

const createOrderRow = (order, isBid) => {
const price = parseFloat(order.price) / Math.pow(10, quoteDecimals);
const amount = parseFloat(order.total_quantity) / Math.pow(10, baseDecimals);
const total = price * amount;
const depth = (parseFloat(order.total_quantity) / maxAmount) * 100;
const colorClass = isBid ? 'text-green' : 'text-red';
const bgClass = isBid ? 'bg-green/10' : 'bg-red/10';

return `
<div class="relative grid grid-cols-3 gap-2 px-2 py-1 text-xs font-mono">
<div class="absolute inset-y-0 right-0 ${bgClass} opacity-60" style="width: ${depth}%"></div>
<div class="relative ${colorClass}">${price.toFixed(4)}</div>
<div class="relative text-right">${amount.toFixed(2)}</div>
<div class="relative text-right">${total.toFixed(2)}</div>
</div>
`;
};

const headerHtml = `
<div class="grid grid-cols-3 gap-2 px-2 py-1 text-text-secondary text-xs uppercase border-b border-gray-border">
<div>Price</div>
<div class="text-right">Amount</div>
<div class="text-right">Total</div>
</div>
`;

const asksHtml = headerHtml + orderBook.asks.slice(0, 15).reverse()
.map(ask => createOrderRow(ask, false)).join('');

const bidsHtml = headerHtml + orderBook.bids.slice(0, 15)
.map(bid => createOrderRow(bid, true)).join('');

document.getElementById('asksList').innerHTML = asksHtml;
document.getElementById('bidsList').innerHTML = bidsHtml;

// Calculate and display spread
if (orderBook.asks.length > 0 && orderBook.bids.length > 0) {
const bestAsk = parseFloat(orderBook.asks[0].price) / Math.pow(10, quoteDecimals);
const bestBid = parseFloat(orderBook.bids[0].price) / Math.pow(10, quoteDecimals);
const spread = bestAsk - bestBid;
const spreadPercent = (spread / bestAsk) * 100;

document.getElementById('spreadRow').textContent =
`${spread.toFixed(4)} (${spreadPercent.toFixed(2)}%)`;
}

updateDepthChart(orderBook);
}

Each order row includes:

  • Price: Execution price in quote asset
  • Amount: Total quantity at this price level in base asset
  • Total: Price × Amount in quote asset
  • Depth bar: Visual representation of liquidity

Loading Recent Trades

Fetch initial trade history:

let lastPrice = null;

async function fetchInitialTrades(apiKey, poolName) {
const data = await apiRequest(`/deepbook/${poolName}/trades`, {
'api-key': apiKey
});

document.getElementById('tradesList').innerHTML = '';

// Display trades in reverse order (newest first)
data.reverse().forEach(trade => addTrade(trade));
}

function addTrade(trade) {
const price = parseFloat(trade.price) / Math.pow(10, quoteDecimals);
const amount = parseFloat(trade.base_quantity) / Math.pow(10, baseDecimals);
const time = new Date(parseInt(trade.checkpoint_timestamp_ms)).toLocaleTimeString();
const isBuy = trade.taker_is_bid;
const priceClass = isBuy ? 'text-green' : 'text-red';

const tradeHtml = `
<div class="grid grid-cols-3 gap-2 px-2 py-1.5 text-xs font-mono border-b border-gray-border/50 hover:bg-gray-border/30">
<div class="${priceClass}">${price.toFixed(4)}</div>
<div class="text-right">${amount.toFixed(2)}</div>
<div class="text-right text-text-secondary text-xs">${time}</div>
</div>
`;

const tradesList = document.getElementById('tradesList');
tradesList.insertAdjacentHTML('afterbegin', tradeHtml);

// Keep only last 50 trades
while (tradesList.children.length > 50) {
tradesList.removeChild(tradesList.lastChild);
}

updatePrice(price, lastPrice || price);
lastPrice = price;
}

Connecting to Live Updates

Establish an SSE connection for real-time data:

let eventSource = null;

function connectLiveTrades(apiKey, poolName) {
// Close existing connection
if (eventSource) {
eventSource.close();
}

const url = new URL(`${STREAM_URL}/deepbook/${poolName}/live-trades`);
url.searchParams.append('api-key', apiKey);

eventSource = new EventSource(url);

eventSource.onopen = () => {
console.log('SSE connection established');
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-green';
};

eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);

if (data.type === 'deepbook_order_book_depth' && data.data?.bids) {
updateOrderBook(data.data);
} else if (data.type === 'deepbook_live_trades') {
addTrade(data.data);
}
} catch (error) {
console.error('SSE parse error:', error);
}
};

eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-text-secondary';
};
}

The SSE stream delivers two event types:

  • deepbook_order_book_depth — Order book updates
  • deepbook_live_trades — New trade executions

Market Depth Visualization

Create a cumulative depth chart:

function updateDepthChart(orderBook) {
if (!orderBook.bids || !orderBook.asks ||
orderBook.bids.length === 0 || orderBook.asks.length === 0) {
return;
}

const barsContainer = document.getElementById('depthBars');
const xAxisLabels = document.getElementById('xAxisLabels');
const yAxisLabels = document.getElementById('yAxisLabels');

barsContainer.innerHTML = '';
xAxisLabels.innerHTML = '';
yAxisLabels.innerHTML = '';

const maxLevels = 10;
const bids = orderBook.bids.slice(0, maxLevels);
const asks = orderBook.asks.slice(0, maxLevels);

// Calculate cumulative amounts
let bidCumulative = 0;
let askCumulative = 0;

const bidAmounts = bids.map(bid => {
const amount = parseFloat(bid.total_quantity) / Math.pow(10, baseDecimals);
bidCumulative += amount;
return bidCumulative;
});

const askAmounts = asks.map(ask => {
const amount = parseFloat(ask.total_quantity) / Math.pow(10, baseDecimals);
askCumulative += amount;
return askCumulative;
});

const maxAmount = Math.max(
bidAmounts[bidAmounts.length - 1] || 0,
askAmounts[askAmounts.length - 1] || 0
);

// Create Y-axis labels (amounts)
for (let i = 4; i >= 0; i--) {
const label = document.createElement('div');
const value = (maxAmount * (i + 1)) / 5;
label.textContent = value > 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(0);
label.className = 'text-right';
yAxisLabels.appendChild(label);
}

const allPrices = [];

// Create bid bars (left side, reversed)
for (let i = bids.length - 1; i >= 0; i--) {
const price = parseFloat(bids[i].price) / Math.pow(10, quoteDecimals);
allPrices.push(price);

const height = (bidAmounts[i] / maxAmount) * 100;
const bar = document.createElement('div');
bar.className = 'flex-1 bg-green/40 hover:bg-green/60 transition-colors cursor-pointer';
bar.style.height = `${height}%`;
bar.title = `Price: ${price.toFixed(4)}, Amount: ${bidAmounts[i].toFixed(2)}`;
barsContainer.appendChild(bar);
}

// Create ask bars (right side)
for (let i = 0; i < asks.length; i++) {
const price = parseFloat(asks[i].price) / Math.pow(10, quoteDecimals);
allPrices.push(price);

const height = (askAmounts[i] / maxAmount) * 100;
const bar = document.createElement('div');
bar.className = 'flex-1 bg-red/40 hover:bg-red/60 transition-colors cursor-pointer';
bar.style.height = `${height}%`;
bar.title = `Price: ${price.toFixed(4)}, Amount: ${askAmounts[i].toFixed(2)}`;
barsContainer.appendChild(bar);
}

// Create X-axis labels (prices)
const minPrice = Math.min(...allPrices);
const maxPrice = Math.max(...allPrices);
const midPrice = (minPrice + maxPrice) / 2;

const strategicPrices = [
minPrice,
(minPrice + midPrice) / 2,
midPrice,
(midPrice + maxPrice) / 2,
maxPrice
];

strategicPrices.forEach(price => {
const label = document.createElement('div');
label.textContent = price.toFixed(4);
label.className = 'text-center';
xAxisLabels.appendChild(label);
});

document.getElementById('bidTotal').textContent = bidCumulative.toFixed(2);
document.getElementById('askTotal').textContent = askCumulative.toFixed(2);
}

Connecting Everything

Main initialization function:

async function connect() {
const poolName = document.getElementById('poolName').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();

if (!poolName || !apiKey) {
alert('Please enter pool name and API key');
return;
}

const btn = document.getElementById('connectBtn');
btn.disabled = true;
btn.textContent = 'Connecting...';

try {
// Initialize chart first
if (!chart) {
initCharts();
}

// Load initial data
await fetchPoolInfo(apiKey, poolName);
await fetchOHLCV(apiKey, poolName, currentTimeframe);
await fetchOrderBook(apiKey, poolName);
await fetchInitialTrades(apiKey, poolName);

// Connect to live stream
connectLiveTrades(apiKey, poolName);

document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-green';
btn.textContent = 'Connected';
} catch (error) {
alert(`Connection failed: ${error.message}`);
btn.disabled = false;
btn.textContent = 'Connect';
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-red';
}
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initCharts();
});

Testing the Dashboard

Local Development

Serve the HTML file using a local server:

npx serve .

Open http://localhost:3000 in your browser.

Connection Steps

  1. Enter a pool name (e.g., "SUI_USDC")
  2. Enter your Surflux API key
  3. Click "Connect"
  4. Watch the dashboard populate with live data

Verification Checklist

  • Price chart displays historical candlesticks
  • Order book shows bids and asks with depth bars
  • Recent trades appear in the list
  • New trades stream in real-time
  • Order book updates automatically
  • Timeframe buttons switch chart periods
  • Market depth chart visualizes liquidity

Production Considerations

Security

Never expose API keys in production. Implement a backend proxy:

// Frontend calls your backend
const response = await fetch('/api/deepbook/pools');

// Backend proxies to Surflux
app.get('/api/deepbook/pools', async (req, res) => {
// Add authentication, rate limiting, caching
const response = await fetch(
'https://api.surflux.dev/deepbook/get_pools?api-key=' + SURFLUX_API_KEY
);
res.json(await response.json());
});

Performance

  • Cache pool metadata — Pool configurations rarely change
  • Throttle DOM updates — Use requestAnimationFrame for smooth rendering
  • Implement virtual scrolling — For large trade lists
  • Debounce chart updates — Avoid excessive redraws

Error Handling

eventSource.onerror = (error) => {
console.error('SSE error:', error);

// Attempt reconnection after 5 seconds
setTimeout(() => {
console.log('Reconnecting...');
connectLiveTrades(apiKey, poolName);
}, 5000);
};

Monitoring

Track key metrics:

  • SSE connection uptime
  • API response latency
  • Event processing rate
  • Client-side errors

Next Steps

  • Add order placement functionality
  • Implement user authentication
  • Create mobile-responsive layout
  • Add technical indicators (RSI, MACD, Bollinger Bands)
  • Build portfolio tracking
  • Integrate wallet connections for trading