Esc

    Integrate

    JavaScript SDK

    Read verified prices, venue reports, order books, funding and node reports from oracle-proxy in a browser or in Node.js, with typed messages and automatic reconnects.

    @markoutlabs/oracle is a client for the web proxy API. It runs in a browser and in Node.js 22 or newer. The dashboard of this site uses it.

    The SDK does the work that the protocol leaves to a client:

    • It keeps one set of wanted topics and sends it again after each reconnect.
    • It reconnects with backoff, and it pings the proxy to find a dead socket.
    • It keeps each book from snapshots and deltas, and it asks for a new snapshot after a gap.
    • It keeps the latest price, venue report and node report.
    • It types every message, and it turns venue mantissas into exact decimals.

    Important

    The proxy, not the SDK, checks the node signatures and the quorum. A web client trusts the proxy operator. Trust model explains what that means.

    Install

    The package is not on npm yet. It is in the docs repository, in packages/oracle-js, as a workspace package. Copy that folder into your project, or add it as a workspace package.

    It has two dependencies: partysocket for the reconnecting socket, and big.js for exact decimals.

    Connect and subscribe

    ts
    import { OracleClient, fetchCatalog } from '@markoutlabs/oracle';
    
    const url = 'wss://oracle.example.com/v1/ws';
    const oracle = new OracleClient({ url });
    
    // Ask the proxy what it serves, then subscribe to all of it.
    const catalog = await fetchCatalog(url);
    oracle.subscribe({ prices: catalog.prices, nodes: true });
    
    oracle.on('price', (price) => {
      if (price.status === 'halted') return; // Never act on a halted price.
      console.log(price.symbol, price.price, '±', price.conf, price.status);
    });

    subscribe and unsubscribe change the wanted set. The SDK collects the calls for 50 ms and sends one message, because the proxy closes a socket that sends more than 10 messages a second. A name that the proxy does not serve comes back in the subscribed event, in unknown.

    Call oracle.close() to close the socket for good.

    Options

    Option Default Contents
    url — The proxy socket, such as wss://oracle.example.com/v1/ws.
    WebSocket global A WebSocket class, for a runtime without a global WebSocket.
    pingIntervalMs 15000 How often the SDK pings the proxy.
    pongTimeoutMs 10000 How long it waits for a pong before it connects again.
    minReconnectDelayMs 1000 The first wait before a reconnect.
    maxReconnectDelayMs 30000 The longest wait. The wait doubles after each failure.

    Events and state

    oracle.on(type, listener) returns a function that removes the listener.

    Event Value When
    status ClientStatus The link changes: connecting, open, reconnecting or closed.
    price PriceMessage A price for a subscribed feed.
    venues VenuesMessage One node's venue report for a subscribed feed.
    book OrderBook A book changed: a snapshot, a delta, or a reset after a gap or a drop.
    funding FundingMessage One node's funding of a subscribed perpetual.
    nodes NodesMessage The node reports, once a second.
    subscribed SubscribedMessage The proxy answered a subscribe or an unsubscribe.
    error ErrorMessage The proxy refused a request. rate_limited also closes the socket.

    The client also keeps the latest state, so a page can read it at any time:

    Property Contents
    status The link now.
    prices Map from feed symbol to the latest PriceMessage.
    venues Map from feed symbol to a Map from node to VenuesMessage.
    books Map from book name to OrderBook.
    funding Map from perpetual name to a Map from node to FundingMessage.
    nodes The latest NodesMessage, or undefined.

    After a reconnect, the proxy sends the latest state of each topic at once. Until then, status is reconnecting, and the kept values are old. Show that to your users.

    Prices

    A PriceMessage is the proxy's price message. price describes each field.

    • Keep price, conf and spread_bps as strings. For maths, use a decimal library such as big.js.
    • Read status on every message. ok and degraded prices are usable; act on conf. A halted price has no value.
    • publish_time_ns is nanoseconds as a string. nsToDate and ageMs read it without a loss of precision.

    Books

    ts
    oracle.subscribe({ books: ['binance:spot:BTCUSDT'] });
    
    oracle.on('book', (book) => {
      if (!book.synced || !book.available) return; // Wait for a snapshot with data.
      console.log(book.name, 'bid', book.bestBid(), 'ask', book.bestAsk(), 'mid', book.mid());
    });

    Book names come from fetchCatalog(url).books. An OrderBook has these members:

    Member Contents
    synced true after a snapshot, until a gap or a disconnect.
    available false when no node has a valid book.
    node The node whose book this is. The proxy serves one node's whole book.
    bids(limit?), asks(limit?) Levels as [price, size] decimal strings, best first.
    bestBid(), bestAsk() The best level of each side.
    mid(), spreadBps() The midpoint, and the spread in basis points of the mid, as exact decimals.

    The SDK orders levels by exact decimal value. On a gap in seq, it drops the levels and asks the proxy for a new snapshot. You do not handle the gap yourself.

    Venue reports

    Venue entries carry integer mantissas, not decimals. venueQuote turns one entry into decimals:

    ts
    import { venueQuote } from '@markoutlabs/oracle';
    
    oracle.subscribe({ venues: ['CRYPTO.BTC/USDT'] });
    oracle.on('venues', ({ symbol, node, report }) => {
      for (const entry of report.entries) {
        const quote = venueQuote(report, entry);
        console.log(symbol, node, entry.venue, entry.status, quote.bid, quote.ask, quote.funding);
      }
    });

    Prices use report.expo, sizes use −8 (QTY_EXPO), and funding uses −9 (RATE_EXPO). fromMantissa(mantissa, expo) does one value.

    Funding

    ts
    import { FUNDING_EXPO, fromMantissa } from '@markoutlabs/oracle';
    
    oracle.subscribe({ funding: ['*'] }); // or names from fetchCatalog(url).funding
    oracle.on('funding', ({ funding, node, update }) => {
      const rate = fromMantissa(update.rate, FUNDING_EXPO);
      console.log(funding, node, update.basis, rate, update.next_funding_time_ns);
    });

    Each message carries the whole state of one perpetual, as one node signed it. funding describes each field.

    • Rates are mantissas at −12 (FUNDING_EXPO), and open interest at −8 (OPEN_INTEREST_EXPO).
    • basis says what the rate is a fraction over: interval (each funding payment) or eight_hours (Deribit).
    • A field that the venue does not send is null. Compare venues only with their basis and interval.

    Node reports

    ts
    oracle.subscribe({ nodes: true });
    oracle.on('nodes', ({ quorum, nodes }) => {
      const connected = nodes.filter((node) => node.connection.state === 'connected');
      console.log(`${connected.length} of ${nodes.length} nodes connected, quorum ${quorum}`);
      for (const node of nodes) {
        for (const feed of node.feeds) {
          if (feed.state === 'stale') console.warn(node.address, 'is stale on', feed.symbol);
        }
      }
    });

    nodes describes each field. The report is for monitoring: the quorum alone decides a price.

    HTTP routes

    Function Route Returns
    fetchCatalog(url) /v1/catalog The prices, venue feeds, books and perpetuals that the proxy serves.
    fetchHealth(url) /health The proxy's upstream client: status, nodes, frames, web clients. Also on a 503.
    fetchNodes(url) /v1/nodes The latest node reports, without a socket. It throws before the first report.
    fetchHistory(url, feed, { from, to }) /v1/history One feed's past prices and venue values from the archive. Times are unix milliseconds.

    Each function takes the socket URL and finds the HTTP base: wss://host/v1/ws becomes https://host. The routes send Access-Control-Allow-Origin: *, so a page on any origin can call them. Each function takes an optional RequestInit, for example with an AbortSignal.

    In Node.js

    Node.js 22 and newer have a global WebSocket, so the SDK works as it is. For an older runtime, pass a class:

    ts
    import WebSocket from 'ws';
    
    const oracle = new OracleClient({ url, WebSocket });

    Test against a local oracle

    scripts/dev.sh in the oracle-proxy repository starts the registry canister on a local network, three nodes and the proxy, on ws://127.0.0.1:8792/v1/ws. The SDK's live test runs against it:

    bash
    ORACLE_PROXY_URL=ws://127.0.0.1:8792/v1/ws pnpm test