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
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,confandspread_bpsas strings. For maths, use a decimal library such asbig.js. - Read
statuson every message.okanddegradedprices are usable; act onconf. Ahaltedprice has no value. publish_time_nsis nanoseconds as a string.nsToDateandageMsread it without a loss of precision.
Books
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:
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
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). basissays what the rate is a fraction over:interval(each funding payment) oreight_hours(Deribit).- A field that the venue does not send is
null. Compare venues only with their basis and interval.
Node reports
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:
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:
ORACLE_PROXY_URL=ws://127.0.0.1:8792/v1/ws pnpm test