Live Prices
This page covers showing prices that move: LivePrices, a stream of every market’s composed Pyth
price with freshness and venue flags; reading Pyth Pro directly; and PriceService, the engine under
both the stream and the keeper.
Prices on the chain and prices on screen differ. A snapshot reports the pair’s stored price, which moves only when a transaction stores a new one; a live price is Pyth’s newest print, composed the way the pair’s oracle would compose it. Every on-chain price check uses the print a call carries (see The 3-Second Price Window).
The samples assume bz connected as in Connecting with a Pyth Pro price source, and
eth a market. pythToken is the page’s source of short-lived tokens
(Tokens for a Browser).
LivePrices
const prices = await bz.livePrices(); // every listed market, each streamed as its own set of feeds
await prices.ready; // the first request for each market's latest print has answered
const unsubscribe = prices.subscribe((changed) => {
for (const [, p] of changed) {
const flag = p.venueClosed ? "closed" : p.fresh ? "live" : `${p.age} s old`;
render(`${formatUnits18(p.spot, 2)} ± ${p.confidenceBp.toFixed(1)} bp (${flag})`);
}
}); // every watched market; pass [eth] as a second argument for one
const now = prices.price(eth); // from memory: null while a feed has no print yet
prices.fresh(eth); // inside the pair's 3 s window, and the confidence clears the 2% cap
prices.venueClosed(eth); // a trading-hours market whose feed stopped generating prices
// When the view goes away:
unsubscribe(); // one subscriber
prices.stop(); // the stream itself, for good
bz.livePrices(options?) lists the connection’s markets and streams them through the connection’s
Pyth Pro client: one subscription per market, on every stream endpoint at once, so each market’s
signed update carries exactly its own feeds (both legs of a composite in one update) and a dropped
endpoint interrupts nothing. Before the stream it makes one request for every feed’s latest print, so
every market shows a price at once; ready settles when that request has answered or failed. When
the stream fails three times in a row, it polls for a minute, then tries the stream again.
The stream runs over a WebSocket. Browsers and Node 22 and later have one; on Node 20 give the client
a constructor (new ProClient({ WebSocket }), with the ws package’s, say).
Everything a render needs is read from memory:
| Method | Returns |
|---|---|
price(market) | The market’s MarketPrice, or null while a feed has no print yet (or the market is not watched). |
all() | A Map from address to MarketPrice, for every market with one. |
fresh(market) | Whether the price is inside the pair’s 3-second window and its confidence clears the 2% cap. |
venueClosed(market) | Whether a trading-hours market’s venue is closed. Always false for a 24/7 market. |
subscribe(listener, markets?) | Calls listener with a Map of the markets whose price changed after each batch of prints, and of those whose fresh or venueClosed flipped as time passed (checked every tickMs). Only markets when given. Returns the function that unsubscribes. |
add(markets) | Streams these markets too (already watched ones are ignored). |
markets() | The markets being streamed. |
stop() | Ends the stream, the ticker and every subscription, for good. |
service | The PriceService underneath: its healthy, lastError and raw latest prints are public. |
A listener is called synchronously, with a map it must not keep; a listener that throws does not stop
the others or the stream. Call stop() when the view unmounts (in a React effect’s cleanup, say).
Each MarketPrice (18 decimals):
| Field | Meaning |
|---|---|
market | The pair’s address. |
spot | The composed price. |
low, high | Spot less and plus the confidence: what a long and a short are valued at when they withdraw. |
confidence | The band’s wider half: max(high − spot, spot − low). |
confidenceBp | confidence / spot in basis points, a number for display. |
confidenceOk | Every leg, and the composed band, sit inside the oracle’s 2% confidence cap. |
publishTime | The second the older leg’s price was generated in (Unix seconds): what the pair’s window judges. |
age | Seconds since publishTime, at the service’s clock. |
fresh | Every leg’s price was generated at most 3 seconds from now, and confidenceOk: a call carrying this print would be accepted if it landed now. |
venueClosed | A trading-hours market whose feed has generated no price for closedAfter seconds while the endpoint delivers. |
legs | The feeds’ prints (base first for a composite). |
A price’s age is when it was generated. While a venue is shut Pyth keeps sending updates, each
stamped with the current time and carrying the last price forward. The update’s own timestamp says
nothing about the price: publishTime is the second the price was generated in (Pyth’s feed update
timestamp), which is what the pair’s 3-second window judges and what fresh, age and venueClosed
use. A carried-forward price reads as old, as it should.
How the oracle composes a price, and why a closed venue matters, is in Oracle.
Options
const tuned = await bz.livePrices({
closedAfter: 30, // seconds without a newly generated price before a trading-hours venue reads closed (default 15)
tickMs: 500, // how often fresh/venueClosed flips are re-checked for subscribers (default 1000)
reuseAge: 1, // a price generated this recently is reused as a call's update (default 1 s)
stream: true, // the WebSocket stream, falling back to polling (default true)
pollInterval: 2, // seconds between polls while not streaming (default 1)
maxFutureSkew: 120, // seconds a print may be dated ahead of this clock (default here 120)
logger: null, // silence endpoint warnings (default console)
});
// Some markets only, with a Pyth Pro client of their own, without a connection:
const some = await livePrices([eth], { pro: { token: pythToken, channel: "fixed_rate@1000ms" } });
| Option | Default | Meaning |
|---|---|---|
closedAfter | 15 | Seconds without a newly generated price after which a trading-hours market reads closed. |
tickMs | 1000 | Milliseconds between re-checks of fresh and venueClosed for subscribers (0: never). |
reuseAge | 1 | A stream update whose price was generated at most this many seconds ago is reused for a call (see Prices for Calls). |
start | true | Start streaming at once; with false, call start(). |
stream | true | Follow the stream, falling back to polling when it fails. false polls only. |
streamIdleTimeout | 10 | Seconds the stream may bring no new update before it is dropped and reconnected. null or 0 waits forever. |
pollInterval | 1 | Seconds between polls when not streaming. |
maxFutureSkew | 120 | Seconds a print may be dated ahead of this device’s clock before it is ignored. See below. |
clock | the system clock | The time freshness is judged at (Unix seconds). |
logger | console | Where endpoint failures are reported (warn); null for silence. |
pro | the connection’s | Options for a ProClient of the stream’s own (another token, another channel). |
client | the connection’s | A ProClient (or any PriceSource) to share. |
PriceService’s sendMargin, ethUsdMaxAge and ethUsdBounds are accepted too; they do not change
what LivePrices reports.
The channel. A client streams on one channel (default fixed_rate@200ms: an update every 200 ms
per subscription and endpoint). A page that only shows prices can stream fixed_rate@1000ms and still
fetch a fresh update when it builds a call. When the connection checks signers (the default), each
new update costs one signature recovery, so a slower channel is also lighter on a phone.
The future-print skew. A print dated far ahead of this device’s clock would hold its feed until a
newer print arrived, so prints dated more than maxFutureSkew seconds ahead are ignored, with a
warning once per feed. The keeper’s bound is 5 seconds; a browser’s clock can run a minute slow, so
LivePrices allows 120 seconds (BROWSER_FUTURE_SKEW). A device clock that is far off also makes
age and fresh wrong: fresh judges against this device’s clock, the pair against the block’s.
Without a Connection
livePrices(markets, options) streams the given markets (MarketInfos, from a market list you
already have) through client, or through a client it makes from pro; with a context instead of
markets it lists them first, as bz.livePrices does. new LivePrices(markets, source, options) is
the constructor underneath. A connection whose price source is not a Pyth Pro source has no client to
stream from: pass pro or client, or livePrices throws a ConnectError.
Prices for Snapshots
Pass a LivePrices as the prices option of marketSnapshot, accountSnapshot, portfolio,
watchMarket and watchAccount: they then value positions at the live price and judge venues from
the stream, with no request of their own.
Prices for Calls
A LivePrices is also a PriceUpdateSource. Its latest(feeds) returns the stream’s own update for
those feeds when every price in it was generated at most reuseAge seconds ago (no request), and
fetches one otherwise. Give it to a facade over the same context, and that facade’s builders attach
the stream’s updates:
// A LivePrices is also a PriceUpdateSource: give it to a second facade over the same context, and its
// order builders reuse the stream's updates instead of asking the endpoint again.
const trading = Bazaar.from({ ...bz.ctx, prices: tuned });
// ...then trading.trade.createOrder(...) attaches the stream's update when its price is at most 1 s old.
The copied context has its own caches (it lists the markets again once), and its price source is not
a proPriceSource: keep the original bz for reads, which then still get the symbols registry and
venue checks from the connection’s client. updateFor(market) returns the stream’s current update
for one market, for code that encodes calls itself.
Pyth Pro Directly
// Pyth Pro directly: prints read from the signed update itself, never from the JSON beside it.
const source = proPriceSource({ token: pythToken });
const [print] = await source.prints(eth.legs); // feedId, price, conf, expo, publishTime, generatedUs, blob
const update = await source.latest(eth.legs); // { update: Hex[], publishTime }: what a call attaches
const client = new ProClient({ token: pythToken, timeout: 5 });
const ctl = new AbortController();
// One set of feeds: every update carries exactly the market's legs, from whichever endpoint is first.
for await (const batch of client.stream(eth.legs, ctl.signal, { idleTimeout: 10 })) {
render(`${batch.length} prints`);
ctl.abort(); // or break: the stream ends at once
}
proPriceSource(options | client) is the connection’s price source (see
Connecting); its prints(feedIds) returns the parsed prints, for showing a
price without a stream. ProClient is the client underneath:
| Member | What it does |
|---|---|
new ProClient(options) | The options of Connecting: token, channel, the URLs, WebSocket, isTrustedSigner. A token or header with a line break is refused at once. |
latest(ids) | The latest prints for these feeds, all from one signed update (a feed without a usable live price is left out). |
latestUpdate(ids) | The same update whole, as a ProUpdate: blob, signer, timestampUs, channel, every feed’s entry (feeds), and its prints. |
atTick(ids, second) | The one-second channel’s update stamped exactly second, or null when there is none: what settlement proofs are made of. |
symbols({ query?, assetType? }) | Pyth Pro’s symbols registry: each feed’s number, symbol, asset type, fastest channel, state and schedule. Needs no token. |
stream(feeds, signal?, { idleTimeout?, onNotice? }) | An async iterator of print batches from every stream endpoint, each update yielded once. feeds is one set of feeds or a list of sets; each set is its own subscription. Aborting signal, breaking out of the loop or close() ends it. It throws a ProError when no endpoint can be reached, and when no new update arrives for idleTimeout seconds (default 10). |
close() | Ends every open stream. |
displayUrl | The request API’s base URL with any key masked. |
A Print has feedId (the bytes32 form), price, conf, expo, publishTime (the second the price
was generated in), generatedUs (the same instant in microseconds), timestampUs and channel (the
carrying update’s), signer (when signers are checked), blob (the signed update that carries it)
and received. Every field but received and signer is read from the update’s signed payload,
never from the JSON an endpoint sends beside it; signer is recovered from its signature.
A feed gets a print only when its entry holds a usable live price: a price, an exponent, a confidence
above zero and a generation time. The oracle refuses a live update without any of them, so the SDK
never offers one to a call; whyNotLive(entry) says which is missing.
A response over 16 MiB, or a stream message over 1 MiB, is refused. Errors mask the token, header secrets and any key in a URL.
PriceService
// PriceService: the engine under LivePrices and the keeper, for code that wants the raw parts.
const service = new PriceService(client, { stream: true, maxFutureSkew: 120 });
const model = marketFromStatic(eth); // the per-market functions take the SDK's Market model
service.watchMarkets([model]);
const stopListening = service.onPrints((feeds) => {
const pp = service.price(model); // spot, low, high, publishTime, confidenceOk
if (pp) render(`${feeds.size} feeds updated, spot ${formatUnits18(pp.spot, 2)}, fresh: ${service.fresh(model, null, 0)}`);
});
void service.run(); // follows the stream (or polls) until stop(); never rejects
// ... later
stopListening();
service.stop();
PriceService is the engine under LivePrices and the keeper’s price loop: it holds the newest
print per feed and the newest update per watched set of feeds, and builds per-market prices and
updates from them. Use it when LivePrices is too much (a server that tracks a few feeds) or too
little (custom freshness margins).
| Member | What it does |
|---|---|
new PriceService(source, options?) | source is a ProClient or any PriceSource (latest and stream). Options as in the table above, plus sendMargin (default 1). The default maxFutureSkew here is 5 seconds. |
watchMarkets(markets), watchSets(sets), watch(feeds) | Watch each market’s legs (or each set) as one subscription, or each feed on its own; the stream resubscribes at once. ETH/USD is always watched (the keeper prices gas with it). |
run() | Follow the stream (or poll) until stop(), through any failure: three in a row mark it unhealthy and switch to polling for a minute. Never rejects. |
fetch(feeds?) | One request for the latest prints, outside the loop. |
ingest(prints) | Take in prints from anywhere. |
onPrints(listener) | Called after each batch with the feeds that got a newly generated price; returns the remover. |
nextPrints() | Waits for the next newly generated price (one consumer only). |
price(market), legs(market) | The composed PairPrice (spot, low, high, publishTime, confidenceOk) and the prints, or null. |
fresh(market, now?, margin?) | Every leg’s price generated within 3 − margin seconds of now (default margin: sendMargin), and the confidence clears the cap. |
closed(market, now?, after?) | No price generated for after seconds (default 15) while the service is healthy. |
updateFor(market) | The signed update a call on the market attaches: the one update that carries its legs. |
printsFor(feeds) | The prints a call reading these feeds together would attach. |
ethUsd(now?) | USD per ETH from the newest ETH/USD print, or null when stale or out of bounds. |
missing() | Watched feeds with no print yet. |
healthy, lastError, lastUpdate, latest | State, readable. |
stop() | Ends run and its stream. |
A market prices from its own set’s update, so a call attaches one update and pays one verification fee. Only while its set has none, or lags another update’s price for one of its legs by a second or more, does it fall back to each leg’s newest print (one update per leg).
The per-market methods take the SDK’s Market model: marketFromStatic(info) makes one from a
MarketInfo.
What Can Go Wrong
| What | Why |
|---|---|
ConnectError: live prices need a Pyth Pro source: ... | The connection’s price source is not a Pyth Pro source and no pro or client was given. |
ProError: this runtime has no WebSocket ... | Node before 22: give the client a WebSocket constructor. |
price() stays null | A feed has no print: Pyth has no usable price for it (a warning names the feed), or ready has not settled. |
Warning: price endpoint sent no print for feed 0x... | The feed ID is wrong, the token does not cover the feed, the feed is not served on the channel, or Pyth has no price or no confidence for it. Other feeds are unaffected. |
Warning: price stream refused the subscription for feed(s) ... | What Pyth answered to a subscription, passed on once. |
Warning: price stream endpoint ... dropped (...): reconnecting | One endpoint went away; the others keep delivering. |
Warning: ignored a print for feed 0x... dated N s ahead of the clock | This device’s clock is behind, or the endpoint is wrong. |
fresh is false while prices move | This device’s clock is off, or the confidence is above 2% (confidenceOk). |
ProError: price endpoint refused the request (HTTP 401); check the token (prices.token) ... | The token is wrong, expired, or not entitled to these feeds or this channel. |
ProError: a price update is signed by 0x..., which is not a signer Pyth's verifier trusts: refusing it | The update did not come from Pyth, or the deployment’s verifier no longer trusts that signer. |
service.healthy is false | Three failures in a row; service.lastError says why. It keeps retrying. |