Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

History

bz.history reads Bazaar’s subgraph: the trade tape and candles, an account’s orders, positions and cash movements, fees and keeper rewards, liquidations and ADL, insurance, terminations, governance and listings. Use it for everything a UI shows about the past: a trade list, a chart, an order history with the reason each order was canceled, a portfolio’s realized PnL, an integrator’s revenue.

Use the chain, not the subgraph, for anything you act on. The subgraph lags the chain by a few blocks (more when its indexer is behind), and it does not index values the contracts do not emit: the mark price, the insurance share price, the vault’s inventory and an account’s health at the live price come from Markets and Accounts & Portfolios. For live events as they happen, see Live Updates.

Connecting a Subgraph

History needs a subgraph source on the connection. Pass a URL, { url, apiKey }, or any object with a query(query, variables) method:

const bz = await Bazaar.connect({
  transport: window.ethereum,
  factory: FACTORY,
  prices: { token: pythToken },
  // A URL, { url, apiKey }, or any object with query(query, variables).
  subgraph: "https://subgraph.example.com/subgraphs/name/bazaar-arbitrum-one",
});

Every function below is on bz.history with the connection bound, and on the history namespace with the context as its first argument (history.trades(bz.ctx, filter)). Calling one on a connection made without a subgraph throws ConnectError (“history comes from Bazaar’s subgraph: connect with a subgraph source”).

With { url, apiKey } the key is sent as Authorization: Bearer <key>. A key in a browser bundle is visible to every user: in a frontend, point subgraph at a proxy that adds the key on your server (see Security Notes).

httpSubgraph

The URL forms build their source with history.httpSubgraph(url, apiKey?, options?). Call it yourself to set its options, then pass the result as subgraph:

// A source of your own: a proxy that adds the key server-side, a longer timeout, extra headers.
const sg = history.httpSubgraph("https://your-app.example.com/api/subgraph", null, {
  timeoutMs: 10_000,
  headers: { "x-client": "my-frontend" },
});
const bz2 = await Bazaar.connect({ transport: window.ethereum, factory: FACTORY, subgraph: sg });
// The same functions without the facade: history.trades(ctx, filter).
const page = await history.trades(bz2.ctx, { pair: eth.address, first: 10 });
console.log(page.items.length);
OptionDefaultMeaning
timeoutMs30000Per-request timeout in milliseconds.
headersnoneExtra request headers.
fetchglobalThis.fetchThe fetch to use (a test double, or one with a custom agent).

Each query is a JSON POST of {query, variables}. The source works in browsers and in Node 20+. Its errors never carry the raw URL: The Graph’s gateway accepts the key in the URL path, so the URL is redacted (see redactUrl in Low-Level Building Blocks) in every message.

Units

Results are converted to SDK units on the way out:

KindTypeNotes
Prices, sizes, notionals, collateral, fees, PnL, shares, funding indices, bountiesbigintBAZAAR precision: 1e18 is $1 or one unit. Format with formatUnits18.
Fields whose names end in Usdc (seedRefundUsdc, seedRefundOwedUsdc)bigintUSDC units (6 decimals), as the subgraph stores them.
Rates whose names end in Bp (imrBp, mmrBp, maxSlippageBp, levyBp)bigintBasis points.
Payout ratios (principalRatio, profitRatio)bigint1e18 is 100%.
TimestampsnumberUnix seconds.
Block numbersnumberArbitrum L2 blocks: the numbers order expiry uses.
Countsnumber
Enums (order type, status, cancel reason, …)stringThe contract enum’s name, e.g. "StopLoss", "Canceled", "Margin".
AddressesAddressChecksummed.
Other bytes (ids, hashes, feed ids)HexLowercase.

An order’s triggerPrice is the trigger price alone: the contract stores the keeper bounty in the same word, and the SDK masks it off (the bounty is reported in bountyEscrowed, bountyHeld, bountyPaidToKeepers and bountyRefunded).

A field the subgraph returns empty where the type does not allow it throws SubgraphError (“the subgraph returned no …”); fields typed | null are null when the subgraph has no value.

Pages

Every list returns a Page<T>:

interface Page<T> {
  readonly items: T[];
  readonly next: string | null; // the cursor for the following page; null at the end
}

Pass page.next back as after, with the same filters, to read the next page:

// The 50 newest fills on ETH/USD.
const tape = await bz.history.trades({ pair: eth.address, first: 50 });
for (const t of tape.items) {
  const side = t.takerIsLong ? "buy" : "sell";
  console.log(new Date(t.timestamp * 1000).toISOString(), side, formatUnits18(t.size), "@", formatUnits18(t.price));
}

// The next 50: pass the page's cursor back as `after`.
if (tape.next) {
  const older = await bz.history.trades({ pair: eth.address, first: 50, after: tape.next });
  console.log(older.items.length);
}

Every list takes these options:

OptionDefaultMeaning
first100Rows per page, 1 to 1000. Anything else throws InputError.
afternoneThe next cursor of the previous page. A cursor from another list throws InputError (“is not a cursor this list returned”).
atBlockthe subgraph’s headRead the subgraph as of this block. Pin every page of a multi-page read to one block for a consistent snapshot.

Lists ordered by time also take:

OptionDefaultMeaning
fromnoneInclusive lower bound on the list’s time field, in Unix seconds.
tononeExclusive upper bound on the list’s time field.
order"desc" for event lists, "asc" for candles and day data"asc" (oldest first) or "desc" (newest first).

The time field is the list’s order field: the event’s timestamp for most lists, the period start for candles, the day for day data, and the creation, submission or opening time where the table below says so.

A cursor is the value of the order field on the page’s last row. Lists ordered by id page with an id_gt (or id_lt) filter; lists ordered by time page with a bound on the time and skip only the rows that share the last time of the previous page, so a page never repeats or drops a row while new history is appended at the head. Treat a cursor as opaque, and use it only with the list, filters and order that produced it.

To read a whole range, follow next until it is null:

/** Every row of a paginated history list, following `next` until it is null. */
async function readAll<T>(read: (after: string | null) => Promise<history.Page<T>>, maxPages = 50): Promise<T[]> {
  const out: T[] = [];
  let after: string | null = null;
  for (let i = 0; i < maxPages; i++) {
    const page: history.Page<T> = await read(after);
    out.push(...page.items);
    if (!page.next) return out;
    after = page.next;
  }
  throw new Error(`more than ${maxPages} pages: narrow the time range`);
}

// Every trade the user took part in (as maker or taker) in the last 7 days.
const now = Math.floor(Date.now() / 1000);
const myWeek = await readAll((after) =>
  bz.history.trades({ account: user, from: now - 7 * 86_400, to: now, first: 1000, after }),
);
console.log(`${myWeek.length} fills this week`);

For a list that keeps growing while you page through it (open positions, say), pin every page to one block. subgraphLag().indexedBlock is a block the subgraph has indexed:

// Pin every page to one block, so rows indexed while you page do not shift the pages.
const lag = await bz.history.subgraphLag();
const atBlock = lag.indexedBlock;
const openPositions = await readAll((after) =>
  bz.history.positions({ pair: eth.address, openOnly: true, first: 1000, after, atBlock }),
);
console.log(`${openPositions.length} open positions as of block ${atBlock}`);

An atBlock the subgraph has not indexed yet, or has pruned, is refused by the subgraph: that comes back as SubgraphError (“the subgraph refused the query: …”).

Market Data

FunctionFiltersOrderReturns
trades(filter?)pair, account (as maker or taker), maker, taker, time optionsnewest first by timestampPage<Trade>
candles(pair, interval, opts?)time options (bound the period start)oldest first by periodStartPage<Candle>
pairDayData(pair, opts?)time options (bound the day)oldest first by datePage<PairDayData>
protocolDayData(opts?)time optionsoldest first by datePage<ProtocolDayData>
bookLevels(pair, { side, ...page })side: "bid" or "ask"best price firstPage<BookLevel>

Trades. One Trade per fill: maker, taker, makerOrderId, takerOrderId, takerIsLong, size, price (the maker’s limit price), notional, each side’s fee (integrator and insurance legs together), what each order had left after the fill, each side’s integrator, and txFrom (the transaction’s sender). When the vault unloads liquidated inventory into a resting order, the fill names the pair itself as taker, isVaultFill is true and takerOrderId is null; txFrom is then the keeper that ran the unload. The account filter uses the subgraph’s or filter, which needs graph-node 0.30 or later.

Candles. interval is one of history.CANDLE_INTERVALS: 60, 300, 900, 3600, 14400 or 86400 seconds. Any other value throws InputError. Each Candle has open, high, low, close, volumeSize, volumeNotional and tradeCount. A period without fills has no candle; a chart expects one, so pass the rows through history.fillCandleGaps:

// One day of 15-minute candles, with the quiet periods filled in for the chart.
const interval = 900;
const to = now - (now % interval) + interval; // the end of the current period
const from = to - 86_400;
const rows = await readAll((after) => bz.history.candles(eth.address, interval, { from, to, first: 1000, after }));
// The last candle before the range, so a quiet start is filled at its close rather than left empty.
const before = await bz.history.candles(eth.address, interval, { to: from, order: "desc", first: 1 });
const chart = history.fillCandleGaps([...before.items, ...rows], interval, from, to);
for (const c of chart) {
  console.log(c.periodStart, formatUnits18(c.open), formatUnits18(c.high), formatUnits18(c.low), formatUnits18(c.close), c.tradeCount);
}

fillCandleGaps(rows, interval, from?, to?) returns the candles from from (rounded down to the period) to to (exclusive; by default the period after the last row), with each missing period filled by a flat candle at the previous close and zero volume. It is a pure function: it needs no connection. Periods before the first known candle stay empty, which is why the sample also reads the last candle before the range. It throws InputError when interval is not a candle interval, when from or to is not a whole non-negative number of seconds, when a row does not start on the interval’s grid, or when the span would take more than history.MAX_FILLED_CANDLES (100,000) candles: ask for a shorter range or a longer interval.

Day data. PairDayData holds one UTC day of a pair: volume, trade count, maker and taker fees, the day’s open, high, low and close (null on a day without fills), liquidations, and, as of the day’s last update, open interest on each side, the funding index and insurance deposited and withdrawn. ProtocolDayData holds the protocol’s totals for the day, including uniqueTraders.

const days = await bz.history.pairDayData(eth.address, { from: now - 30 * 86_400 });
for (const d of days.items) {
  console.log(new Date(d.date * 1000).toISOString().slice(0, 10), "volume $" + formatUnits18(d.volumeNotional, 0), "trades", d.tradeCount);
}

Book levels. Aggregated resting size and order count per price on one side, bids high to low and asks low to high. The subgraph counts resting orders whose expiry block has passed until a transaction removes them, and it lags: for what an order would fill against now, read the book from the chain (bz.markets.getBook, see Markets). side must be "bid" or "ask"; anything else throws InputError. Book levels take the page options only.

Orders

FunctionFiltersOrderReturns
orders(filter?)account, pair, status, openOnly, orderType, integrator, time options (bound the creation time)newest first by createdTimestampPage<HistoryOrder>
order(pair, orderId, atBlock?)HistoryOrder | null
orderEvents(filter?)account, pair, orderId (needs pair), action, time optionsnewest first by timestampPage<OrderEvent>

status is a list of history.ORDER_STATUSES names: Waiting (a trigger order not yet fired), Pending, Resting, Fired, Filled, Canceled. openOnly keeps the orders that can still trade (Waiting, Resting, Fired, which is history.OPEN_ORDER_STATUSES); with status as well it keeps the open statuses among those you named. orderType is a list of Market, Limit, StopLimit, TakeProfit, StopLoss. An unknown name, or an empty list, throws InputError; so does openOnly with a status list that names no open status.

// The user's orders that can still trade, newest first.
const open = await bz.history.orders({ account: user, openOnly: true });
for (const o of open.items) {
  console.log(o.orderId, o.orderType, o.isLong ? "long" : "short", formatUnits18(o.remaining), "left", o.status);
}

// Orders the protocol canceled, and why, in a sentence for the owner.
const canceled = await bz.history.orders({ account: user, status: ["Canceled"], first: 20 });
for (const o of canceled.items) {
  if (o.cancelReason === "Owner") continue;
  console.log(`Order ${o.orderId}: ${history.CANCEL_REASON_TEXT[o.cancelReason]}`);
}

A HistoryOrder holds the order as created (orderType, isLong, isPostOnly, timeInForce, integrator, triggerPrice, limitPrice, maxSlippageBp, expiryBlock), its progress (size, filledSize, remaining, status, inBook, fillCount, amendCount, triggerCount), its times (createdTimestamp, lastTriggeredTimestamp, canceledTimestamp, filledTimestamp, updatedTimestamp, and the matching blocks), its trigger bounty (bountyEscrowed, bountyHeld, bountyPaidToKeepers, bountyRefunded), and cancelReason. A TakeProfit or StopLoss reports the size its owner set at creation, and the size capped at the position once it fires. A resting order past its expiryBlock stays in the book until a transaction removes it.

Cancel Reasons

cancelReason is one of history.CANCEL_REASONS, and history.CANCEL_REASON_TEXT has a sentence for the owner for each:

ReasonMeaning
NoneNot canceled.
OwnerThe owner canceled it.
ExpiredIt reached its expiry block.
UnfilledThe book could not fill all of it on arrival; a market or immediate-or-cancel order cancels what is left.
BelowMinimumWhat was left was worth less than the $5 order minimum, so it could not rest.
PostOnlyIt was post-only and would have filled on arrival.
SelfMatchIt would have traded against another order of the same owner.
StepLimitIt crossed more resting orders than one transaction may walk; the rest was canceled.
MarginThe owner’s collateral could not cover the fill (its margin or its fee) when it was reached.
PriceBandWhile the market was closed it would have filled more than 10% away from the last price.
PositionClosedThe position it protected was closed or flipped.
LiquidatedThe position was liquidated.
DeleveragedThe position was closed in full by auto-deleveraging.

history.cancelReasonName(code) turns the numeric code an OrderUpdated log carries into its name (a code outside the list throws RangeError); use it for the numeric cancelReason of the order updates Live Updates delivers, or when you decode logs yourself. The protocol’s rules behind each reason are in Bazaar’s Orders.

Order Events

orderEvents lists one OrderEvent per OrderUpdated, OrderAmended or OrderAmendSkipped log. Its action is one of Created, Triggered, Canceled, Amended, AmendSkipped; a Canceled event carries the cancelReason, and an AmendSkipped event the amendSkipReason (Inactive, Expired, SizeFilled, BelowMinimum, WouldCross: why a best-effort amendment left its order as it was). Each event also has the order’s size, filledSize, limitPrice, triggerPrice and prioritySeq as the log reported them. For one order’s lifecycle pass pair and orderId (without pair, orderId throws InputError):

// One order's lifecycle: created, triggered, amended, amend skipped, canceled.
const orderId = 42n;
const events = await bz.history.orderEvents({ pair: eth.address, orderId, order: "asc" });
for (const e of events.items) {
  const why =
    e.action === "Canceled" ? `: ${history.CANCEL_REASON_TEXT[e.cancelReason]}`
    : e.action === "AmendSkipped" ? ` (${e.amendSkipReason})`
    : "";
  console.log(e.timestamp, e.action + why);
}
// The order itself, or null when the subgraph has not seen it yet.
const one = await bz.history.order(eth.address, orderId);
console.log(one?.status ?? "not indexed yet");

Positions and Collateral

FunctionFiltersOrderReturns
positions(filter?)account, pair, openOnlyby idPage<HistoryPosition>
positionChanges(filter?)account, pair, time optionsnewest firstPage<PositionChange>
positionSnapshots(filter?)account, pair, time optionsnewest firstPage<PositionSnapshot>
collateralTransfers(filter?)account, pair, time optionsnewest firstPage<CollateralTransfer>

positions returns one HistoryPosition per account and pair, open or closed: the stored position (isLong, size, entryValue, entryPrice, collateral, entryFundingIndex, imrBp, mmrBp, takeProfitOrderId, stopLossOrderId) and lifetime figures (realizedPnl, feesPaid, tradeCount, volumeSize, volumeNotional, collateralDeposited, collateralWithdrawn, liquidationCount, adlCount, openedAtTimestamp, closedAtTimestamp). realizedPnl is the price PnL realized by fills plus ADL credits; funding settles into collateral and is not counted in it. entryPrice is entryValue / size, and 0 when flat. positions takes the page options only.

positionChanges lists each trade’s effect on a position: the maker’s once per fill, the taker’s once per order walk at its volume-weighted price. sizeDelta is the new size less the old (both unsigned), so a flip reports less than it traded. positionSnapshots lists the full stored position after every change (each PositionBucketUpdated log), including liquidations, ADL and terminal settlement; imrBp and mmrBp are 0 when the log reported no margin rates (terminal settlement).

// The user's position on ETH/USD: lifetime figures and each trade's effect on it.
const [pos] = (await bz.history.positions({ account: user, pair: eth.address })).items;
if (pos) {
  console.log("realized PnL", formatUnits18(pos.realizedPnl), "fees paid", formatUnits18(pos.feesPaid));
}
const changes = await bz.history.positionChanges({ account: user, pair: eth.address, first: 20 });
for (const c of changes.items) {
  console.log(c.timestamp, formatUnits18(c.newSize), "@", formatUnits18(c.executionPrice), "pnl", formatUnits18(c.realizedPnl));
}
const cash = await bz.history.collateralTransfers({ account: user, pair: eth.address });
for (const t of cash.items) console.log(t.isDeposit ? "deposit" : "withdrawal", formatUnits18(t.amount));

Fees, Rewards and Relaying

FunctionFiltersOrderReturns
feeBalances(filter?)recipient, pair, claimableOnlyby idPage<FeeBalance>
feeAccruals(filter?)recipient, pair, time optionsnewest firstPage<FeeMovement>
feeClaims(filter?)recipient, pair, time optionsnewest firstPage<FeeMovement>
integratorStats(integrator, atBlock?)IntegratorStats | null
accountStats(account, atBlock?)AccountStats | null
accountDayData(account, opts?)time optionsoldest first by datePage<AccountDayData>
metaTransactions(filter?)user, relayer, pair, time optionsnewest firstPage<MetaTransaction>
permitFailures(filter?)owner, pair, time optionsnewest firstPage<PermitFailure>

A claimable balance on a pair belongs to a recipient: an integrator’s fees, a relayer’s fees, a keeper’s rewards, a refunded trigger bounty. feeBalances reports each one’s accrued, claimed and claimable; claimableOnly keeps those with something to claim. It takes the page options only: its type accepts from, to and order, but they are ignored. feeAccruals lists each credit (FeeAccrued) and feeClaims each claim (FeesClaimed).

integratorStats reports what one integrator address has routed: orderCount, fillCount (fill sides that carried it: a fill with it on both sides counts twice), maker and taker volume, firstOrderTimestamp and lastFillTimestamp. Its revenue is its feeBalances. It is null when the address has carried no order.

// What a frontend has routed, and what it can claim.
const routed = await bz.history.integratorStats(MY_FRONTEND);
if (routed) {
  console.log(routed.orderCount, "orders,", routed.fillCount, "fill sides");
  console.log("maker volume $" + formatUnits18(routed.makerVolumeNotional, 2), "taker volume $" + formatUnits18(routed.takerVolumeNotional, 2));
}
const balances = await bz.history.feeBalances({ recipient: MY_FRONTEND, claimableOnly: true });
for (const b of balances.items) console.log(b.pair, "claimable $" + formatUnits18(b.claimable, 2));

accountStats reports an account’s lifetime totals across every pair: trades, maker and taker volume, fees paid, realized PnL, liquidations suffered and executed, trigger bounties earned and executions, ADL batches executed, every credit and claim of a claimable balance, and seedRefundOwedUsdc (a listing seed owed back, in USDC units). It is null when the subgraph has not seen the account. accountDayData gives the same account’s figures per UTC day.

metaTransactions lists relayed (gasless) actions: the user, the relayer, the action’s EIP-712 typehash as functionId, the nonce and the relayerFee. permitFailures lists the ERC-2612 permits a pair could not execute (the call went on with the existing allowance). See Gasless Trading & Relayers and Collateral, Approvals & Permits.

Liquidation and ADL

FunctionFiltersOrderReturns
liquidations(filter?)account (the liquidated trader), liquidator, pair, time optionsnewest firstPage<Liquidation>
adlWindows(filter?)pair, time options (bound the opening time)newest first by openedAtPage<AdlWindow>
adlBatches(filter?)pair, window, transactionHash, time optionsnewest firstPage<AdlBatch>
adlFills(filter?)account, pair, transactionHash, time optionsnewest firstPage<AdlFill>

A Liquidation has the seized size, side, bankruptcyPrice, the price it ran at, notional and collateralSeized. An AdlWindow (id "<pair>-<epoch>") runs from the AdlTriggered that opened it to AdlResolved or the pair’s termination (endedByTermination). An AdlBatch is one executeAdl call: the bucket each leg closed, its settlement price (0 when that leg did not run) and what it netted. Join adlFills on the batch’s transactionHash for the winners it closed: each AdlFill has the closedSize, the credit to the winner’s collateral (PnL at the settlement price less withheld funding), adlScore and withheldFunding. window must be a window id; transactionHash must be 32 bytes of hex; anything else throws InputError.

Insurance

FunctionFiltersOrderReturns
insuranceProviders(filter?)pair, account, activeOnlyby idPage<InsuranceProvider>
insuranceEvents(filter?)account, pair, kind, time optionsnewest firstPage<InsuranceEvent>

An InsuranceProvider is one LP on one pair: live shares (a dead-pool reset sets every balance to 0), deposited, withdrawn, the withdrawal request in force (requestedShares, requestedAt), what the first execute call lockedAmount the shares at, when the paying call opens (claimOpensTs), and levies charged and received. activeOnly keeps providers holding shares. kind is a list of Deposit, WithdrawalRequested, WithdrawalLocked, WithdrawalExecuted, LevyCharged, LevyReceived; an event’s amount is 0 for requests and levies. The share price and what a withdrawal would pay now come from the chain: see Insurance.

Termination

FunctionFiltersOrderReturns
pairTerminations(filter?)pair, time optionsnewest firstPage<PairTermination>
terminalRatioUpdates(filter?)pair, time optionsnewest firstPage<TerminalRatioUpdate>
terminationProposals(filter?)pair, proposer, status, time options (bound submission)newest first by submittedAtPage<TerminationProposal>
insurerProposals(filter?)pair, proposer, status, time options (bound creation)newest first by createdAtPage<InsurerProposal>
insurerVotes(filter?)proposal, pair, voter, time optionsnewest firstPage<InsurerVote>
insurerLevies(filter?)proposal, pair, voter, time optionsnewest firstPage<InsurerLevy>

A PairTermination is a finalized termination with its payout figures: settlementPrice, owedToTraders, registeredClaims, badDebt, estateValue, pot, the principalRatio and profitRatio (1e18 = 100%), the insurance fund after, and sweepComplete (false when finalize ran through the escape hatch with positions still open). A TerminalRatioUpdate records a post-finalize settlement that re-derived the ratios on the entitlements still unpaid.

A TerminationProposal is a UMA termination claim (its id is the UMA assertion id): category, lastTradingTs (the cutoff it proposed), reason, disputed, acceptedCutoffTs, submittedAt, livenessEndsAt (from when an undisputed claim can be settled) and status: Pending, Disputed, Accepted, ScheduleRefused, Superseded or Rejected. Every claim also opens an insurer vote under the same id. An InsurerProposal has status Open, Scheduled, Failed, Preempted, Voided or Rejected, its cutoffTs, yesShares against snapshotTotalShares, thresholdMet, outcome (1 TRUE, 2 FALSE, 3 UNJUDGED), levyBp and bondRefundFailed. proposal filters must be 32 bytes of hex. Render reason as text, never as HTML. How to act on any of these: Termination & Settlement.

Listings and Governance

FunctionFiltersOrderReturns
listingProposals(filter?)deployer, status, time options (bound the proposal time)newest first by proposedAtPage<ListingProposal>
identifierUpgradeProposals(filter?)proposer, status, time optionsnewest first by proposedAtPage<IdentifierUpgradeProposal>

A ListingProposal (id: the UMA assertion id) has the proposed pairId, baseFeedId, description, seedAmount, status (Pending, Disputed, Deployed, Rejected, Expired, Voided), the deployed pair once deployed, and seedRefundUsdc (USDC units). An IdentifierUpgradeProposal has the newIdentifier, status (Pending, Disputed, Queued, Upgraded, Canceled, Expired, Rejected), and effectiveTs (when a queued upgrade’s timelock ends). A listing’s description is written by whoever proposed it: render it as text. See Listing Markets and UMA Disputes & Governance.

How Current Is It

subgraphLag() compares the subgraph’s _meta.block with the chain’s head:

FieldMeaning
indexedBlockThe last block the subgraph has indexed.
indexedTimestampIts timestamp, or null when the indexer does not report one.
chainBlock, chainTimestampThe head as the connection’s RPC sees it.
blocksBehindBlocks the subgraph trails the head (0 when level, or ahead of this RPC).
secondsBehindSeconds behind, or null without indexedTimestamp.
hasIndexingErrorsThe indexer hit an error: its history may stop advancing.

Show the user that recent trades may be missing when it falls behind:

try {
  const l = await bz.history.subgraphLag();
  if (l.hasIndexingErrors) console.warn("The history index hit an error and may have stopped.");
  else if (l.blocksBehind > 20) {
    const behind = l.secondsBehind !== null ? `${l.secondsBehind} s` : `${l.blocksBehind} blocks`;
    console.warn(`History is ${behind} behind the chain: the latest trades may not be listed yet.`);
  }
} catch (e) {
  // The subgraph is unreachable or refused the query: say so, and keep the page working.
  console.warn("History is unavailable right now.", e instanceof Error ? e.message : e);
}

What Can Go Wrong

ErrorWhen
ConnectErrorThe connection has no subgraph source.
InputErrorAn argument was refused before any query was sent: an address that is not one, first outside 1 to 1000, an unknown enum name, a cursor from another list, a candle interval that is not one of the six, orderId without pair, a window id or hash of the wrong shape. Its field names the argument, and errorMessage(e) shows its message.
history.SubgraphErrorThe subgraph is unreachable, answered with an HTTP error (status holds it; 401 and 403 add “check the API key”), refused the query (GraphQL errors, such as an atBlock it has not indexed), did not answer with JSON, or returned no data or a row without a required field. The message never carries the raw URL.
try {
  await bz.history.trades({ pair: eth.address });
} catch (e) {
  if (e instanceof history.SubgraphError) {
    // unreachable, an HTTP error (e.status), a GraphQL error, or an answer without data
    console.error("history unavailable:", e.message, e.status);
  } else {
    // InputError (a bad argument: errorMessage shows it), or ConnectError (connected without a subgraph)
    console.error(errorMessage(e));
  }
}

errorMessage(e) is written for contract and wallet failures; for a SubgraphError or ConnectError it returns its generic sentence, so show your own text (“History is unavailable right now”) and log e.message.

Every value is validated and written into the query as a literal, so nothing a user typed can change the query’s text.