# Building a Real-Time Price Dashboard with the eToro API

An end-to-end tutorial combining the WebSocket API and Market Data endpoints to build a live price dashboard.

---


Live dashboards are where REST meets push: you use **Market Data** endpoints once to resolve instrument metadata, then keep the UI fresh with a **WebSocket** subscription to quote or trade topics. This tutorial shows a small **Node.js** service that connects to the eToro streaming API, subscribes to a handful of instrument IDs, reconnects cleanly after network blips, and merges streaming ticks with REST-backed labels for a simple console or web UI.

The REST base URL for examples is `https://public-api.etoro.com/api/v1/`. Streaming uses a separate WebSocket origin at `wss://ws.etoro.com/ws`—refer to the [API Portal](https://api-portal.etoro.com) for the latest connection details.

## Bootstrapping shared headers

Whether you call REST or open a WebSocket, reuse the same API identity: `x-api-key`, and when required `x-user-key`. Generate a fresh `x-request-id` per HTTP request; for WebSockets, send an identifying header or query parameter as described in your portal’s auth section.

<!-- skip-test -->
```javascript
import { randomUUID } from "node:crypto";
import WebSocket from "ws";

const REST = "https://public-api.etoro.com/api/v1";
const WS_URL = "wss://ws.etoro.com/ws";

function restHeaders() {
  return {
    "x-api-key": process.env.ETORO_API_KEY,
    "x-user-key": process.env.ETORO_USER_KEY ?? "",
    "x-request-id": randomUUID(),
    accept: "application/json",
  };
}
```

## Loading instrument metadata over REST

Before displaying prices, fetch human-readable names, precision, and exchange session rules so your UI formats decimals correctly and shows **bid/ask** labels consistently.

<!-- skip-test -->
```javascript
async function loadInstrumentMeta(instrumentId) {
  const res = await fetch(`${REST}/market-data/instruments/${instrumentId}`, {
    headers: restHeaders(),
  });
  if (!res.ok) throw new Error(`instrument ${instrumentId}: ${res.status}`);
  const { data } = await res.json();
  return {
    id: instrumentId,
    symbol: data.symbol,
    name: data.displayName,
    pipSize: data.pipSize ?? 0.0001,
  };
}

export async function loadWatchlist(ids) {
  return Promise.all(ids.map(loadInstrumentMeta));
}
```

You can batch multiple IDs if your portal exposes a bulk instruments route; the pattern stays the same—one REST round-trip, then cache in memory keyed by instrument ID.

## Connecting and subscribing over WebSocket

Open the socket with the same API key headers your portal specifies (some deployments use subprotocols or a short-lived ticket in the query string). After `open`, send a **subscribe** message listing topics such as `quotes.{instrumentId}` or `trades.{instrumentId}`.

<!-- skip-test -->
```javascript
function connectSocket({ instrumentIds, onTick }) {
  const socket = new WebSocket(WS_URL, {
    headers: {
      "x-api-key": process.env.ETORO_API_KEY,
      "x-user-key": process.env.ETORO_USER_KEY ?? "",
      "x-request-id": randomUUID(),
    },
  });

  socket.on("open", () => {
    socket.send(
      JSON.stringify({
        type: "subscribe",
        channels: instrumentIds.map((id) => `quotes.${id}`),
      })
    );
  });

  socket.on("message", (data) => {
    const msg = JSON.parse(data.toString());
    if (msg.type === "quote") onTick(msg.instrumentId, msg.bid, msg.ask, msg.ts);
  });

  return socket;
}
```

## Reconnection with backoff

Networks drop. Wrap the client in a small **reconnect loop**: on `close` or `error`, wait with exponential backoff (1s, 2s, 4s, cap at 30s), then instantiate a new `WebSocket`. Persist the latest known quotes so the UI does not flash empty during reconnect, and resend the subscribe payload on every successful `open`.

<!-- skip-test -->
```javascript
export function resilientStream(instrumentIds, onTick) {
  let attempt = 0;
  let socket;

  const connect = () => {
    socket = connectSocket({ instrumentIds, onTick });
    socket.on("close", scheduleReconnect);
    socket.on("error", () => socket.close());
  };

  const scheduleReconnect = () => {
    const delay = Math.min(30000, 1000 * 2 ** attempt++);
    setTimeout(() => {
      connect();
    }, delay);
  };

  connect();
  return () => socket?.close();
}
```

## Combining REST metadata with live prices

The dashboard model merges static metadata with the latest tick. Below, a tiny in-memory store suitable for pushing to a web client via Server-Sent Events or WebSocket fan-out from your own server.

<!-- skip-test -->
```javascript
const state = new Map();

export function createDashboard(instrumentIds) {
  const metaPromise = loadWatchlist(instrumentIds);

  const stop = resilientStream(instrumentIds, (id, bid, ask, ts) => {
    const row = state.get(id) ?? {};
    state.set(id, { ...row, bid, ask, ts });
  });

  return {
    async snapshot() {
      const meta = await metaPromise;
      for (const m of meta) {
        const live = state.get(m.id) ?? {};
        state.set(m.id, { ...m, ...live });
      }
      return [...state.values()];
    },
    dispose: stop,
  };
}
```

## Display and next steps

For a browser UI, expose `snapshot()` on an interval for a table grid, or push diffs when `onTick` fires. Add staleness warnings if `Date.now() - ts` exceeds a few seconds, and fall back to the last REST **mid** price if the stream is quiet during off-hours.

You now have a repeatable pattern: **REST for reference data**, **WebSocket for ticks**, **reconnect logic for resilience**. Extend it with order-book depth channels or authenticated user streams when you graduate from read-only market data.
