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

Core Concepts

The SDK is built around a handful of ideas that every page after this one assumes: a connection bound to one deployment, functions grouped by area, writes returned as prepared calls, one rule for units, a price window, errors written for users, and a short list of things it caches. This page explains each.

The Connection

A connection is a BazaarContext: one chain, one Bazaar deployment, and the optional services some functions need. Bazaar.connect(options) creates one (see Connecting); it is bz.ctx.

FieldWhat it is
chainThe SDK’s Chain over your transport: reads, batched reads, simulations, logs, receipts.
chainIdThe chain the transport is on, read at connect.
factoryThe deployment’s BazaarFactory, as you gave it.
lens, terminator, oracle, usdcThe deployment’s other contracts, read from the factory at connect.
pricesWhere signed Pyth updates come from, or null for a read-only connection. Every call that carries a price needs it.
subgraphBazaar’s subgraph, or null. Only history needs it.
integratorThe address every order this connection creates names as its integrator (the zero address for none).

Nothing in a context changes after it is made. A connection is bound to a chain, not to a user: every function takes the user’s address as an argument, so one connection serves any number of accounts. When the wallet switches chain, connect again.

The Facade and the Namespaces

Each area of the SDK is a module of plain functions that take the context first: markets.getMarket(ctx, "ETH/USD"), trade.createOrder(ctx, pair, user, order). The Bazaar object binds the context into every one of them, so the same calls read bz.markets.getMarket("ETH/USD") and bz.trade.createOrder(pair, user, order).

// The facade: one object, the connection bound into every function.
const bz = await Bazaar.connect({ transport: window.ethereum, factory: FACTORY });
const eth = await bz.markets.getMarket("ETH/USD");

// The same functions per area, taking the context first: what a library or a tree-shaken bundle uses.
const ctx = await connect({ transport: window.ethereum, factory: FACTORY });
const same = await markets.getMarket(ctx, "ETH/USD");

// Moving between them: bz.ctx is the context, and Bazaar.from wraps one.
const again = Bazaar.from(ctx);
console.log(bz.ctx.lens, again.deployment.usdc, same.label);

// Pure helpers, which need no connection, live only on the namespaces.
const expiry = (await bz.ctx.chain.blockNumber()) + orders.blocksIn(3600);
AreaOn bzWhat it covers
marketsbz.marketsThe market list, snapshots, the book
accountbz.accountAccount snapshots and portfolios
livebz.liveWatching a market or an account
tradebz.tradeCollateral, orders, fees, trigger execution
ordersbz.ordersOrder specs and their checks (mostly pure helpers)
allowance, permitsbz.allowance, bz.permitsUSDC balance, approvals, permits
signing, relaybz.signing, bz.relayGasless requests: signing, verifying, relaying
batchingbz.batchingSmart accounts and EIP-5792 batches
previewsbz.previewsWhat an order or a withdrawal would do
historybz.historyThe subgraph’s records
insurancebz.insuranceThe insurance fund
termination, listings, governance, umasameWinding markets down, listing them, UMA
priceFeeds(bz.livePrices())Pyth Pro sources and live prices
formatnoneFormatting amounts, prices, times

Use the facade in an application: it is shorter and cannot pass the wrong context. Use the namespaces in a library that takes a context from its caller, and for the pure helpers (packing, validation, formatting, type strings), which need no connection and are therefore not on bz: orders.blocksIn, orders.buildOrderSpec, format.formatUsd. Importing Bazaar brings every area into the bundle; an app that imports only a few namespace functions lets the bundler drop the rest. The facade also has bz.simulate, bz.send, bz.waitForReceipt, bz.livePrices, bz.chainId and bz.deployment.

The most used names (Bazaar, connect, errorMessage, the unit helpers, the constants, the error classes, the low-level chain and price clients) are exported flat from the package. The full list is in the API Index.

Prepared Calls: Simulate, Then Send

The SDK never sends anything and never holds a key. Every builder (trade.createOrder, trade.depositCollateral, allowance.approveUsdc, insurance.depositToInsurance, …) returns a PreparedCall, for any contract:

const call = await trade.createOrder(ctx, eth.address, user, {
  orderType: OrderType.Limit, isLong: false, size: parseUnits18("1"), limitPrice: parseUnits18("3100"), expirationBlock: expiry,
});
call.action; // "createOrder"
call.to; // the pair
call.data; // the calldata
call.value; // ETH for the Pyth update fee (wei)
call.from; // the user it acts for
call.summary; // "Sell 1 at 3100 (limit, GTC)"
call.priceTime; // the attached print's publish time (unix seconds)
call.pullsUsdc; // USDC it takes from the wallet (18 decimals): a trigger order's bounty, a deposit

const sim = await bz.simulate(call); // eth_call as `from`, then eth_estimateGas
if (sim.ok) console.log(`gas ${sim.gas}`);
else console.log(sim.error!.name, sim.error!.message); // the decoded custom error, and a sentence for the user
FieldMeaning
actionThe contract function, such as "createOrder".
to, dataThe contract and the calldata.
valueETH to send, in wei: the Pyth update fee when the call carries a price, else 0.
fromThe account the call acts for (the sender of a direct call), or null when unknown.
summaryOne line for a confirmation screen, with every amount printed exactly.
priceTimePresent when the call carries a Pyth update: the update’s publish time, in Unix seconds.
pullsUsdcPresent when known: the USDC the call takes from the wallet, in 18 decimals.

The flow is the same for every write:

  1. Build. The builder checks its inputs against what it can read (the account’s orders and position, the book, the live price) and throws an InputError naming the field for anything the contract would refuse. Before naming a market it asks the factory whether that address is one of its pairs (requirePair), so an address from a link cannot pass off another contract as a market.
  2. Simulate, as the user: bz.simulate(call) runs it with eth_call and estimates its gas. A revert comes back decoded, with a sentence for the user, before the wallet is asked anything.
  3. Show the summary, and what the call costs: value, pullsUsdc.
  4. Send with the user’s wallet: bz.send(wallet, call) over any EIP-1193 provider, or toTransactionRequest(call) for a viem or wagmi client, or a bot’s own key.
  5. Wait: bz.waitForReceipt(hash) throws a CallError if the transaction reverted.

Signatures follow the same principle: gasless requests and USDC permits are typed data the SDK asks the wallet to sign with eth_signTypedData_v4, so the wallet shows the user every field, and the SDK checks that the signature recovers to the signer before it uses it. See Sending Transactions and Gasless Trading & Relayers.

Units

One rule covers the whole SDK:

  • Protocol quantities are bigint: prices, sizes, notionals, collateral, fees, rewards, funding indices and rates, scores, basis-point rates.
  • Amounts are in 18 decimals (“BAZAAR precision”): 1e18 is $1, or one unit of the traded asset. parseUnits18("12.5") and formatUnits18(x, 2) convert to and from text.
  • USDC’s own calls are in 6 decimals, as the token counts: wallet balances and allowances (wallet.usdcBalance, usdcFunding, approveUsdc) and permits. The pair floors what it moves to whole micro-USDC. Convert with toUsdcUnits, toUsdcUnitsCeil (for an allowance that must cover a pull) and fromUsdcUnits. A name says USDC when it is in 6 decimals.
  • Times, blocks, counts and enum values are number: Unix seconds, block numbers, order types. The one exception is an order spec’s expirationBlock, a bigint because a take-profit or stop-loss carries 2^64 − 1 (“never”), which a number cannot hold. Order inputs accept either.
  • Ratios for display are number: a margin ratio, a leverage, a confidence in basis points.
const size = parseUnits18("0.25"); // 250000000000000000n: 0.25 ETH
const price = parseUnits18("3012.5"); // $3,012.50
const notional = (size * price) / 10n ** 18n; // $753.125, still 18 decimals
formatUnits18(notional, 2); // "753.12" (rounded toward zero)
toUsdcUnits(notional); // 753125000n: what USDC's own calls count (6 decimals), floored
fromUsdcUnits(1_000_000n); // 1000000000000000000n: one USDC back in 18 decimals

Every conversion and constant is on Units & Constants.

Price Updates and the 3-Second Window

Bazaar prices every market from Pyth. A call that needs the price (creating or amending an order, firing a trigger, a withdrawal with a position, an insurance exit, liquidations) carries a signed Pyth Pro update in its calldata, and the pair accepts a price generated at most 3 seconds from the block’s time (MAX_PRICE_STALENESS). The pair has the attached update verified and uses its price; a call that carries none runs on the pair’s own stored price while that is at most 3 seconds old, and otherwise reverts (or, on a market with trading hours, runs on its stored price where that call allows it). See Oracle.

What this means in code:

  • Builders that attach a price need a price source on the connection (prices); without one they throw a ConnectError.
  • call.value is the fee Pyth charges to verify the update; the pair refunds what it does not spend.
  • call.priceTime is the print’s publish time. The clock starts when the call is built: build right before sending, and rebuild a call whose print is more than a second or two old.
  • A human approving in a wallet can be slower than 3 seconds; on a quiet market such a call may revert on chain. Gasless calls avoid this: the relayer attaches the price when it submits.

Reads are different: they never need a print to be fresh. A market snapshot reports the pair’s stored price and its age; an account snapshot values the position at the freshest price it can get (the caller’s, a live stream’s, one Pyth Pro request, or the stored one) and says which (price.source). Live Prices streams the composed price of every market with a fresh flag.

User-Facing Errors

Failures come in a few shapes, and errorMessage(e) turns any of them into one sentence for the user:

ErrorWhenWhat it carries
InputErrorA builder refused an input before building anything.field, and a message prefixed with it (“size: Enter a size greater than zero.”).
DecodedErrorsimulate() found a revert: sim.error.The custom error’s name and args, and message, a sentence for the user. Every custom error the contracts declare has one.
CallErrorA send was rejected or the transaction reverted.message, and decoded when the revert could be decoded.
wallet errorsThe user declined, the wallet is on another network, has no ETH for gas, …errorMessage recognizes the EIP-1193 codes and viem’s and ethers’ error shapes.
ConnectErrorA connection problem: the wrong chain, not a factory, a missing price source or subgraph.A message for the developer.
ProError, SubgraphError, HttpError, RpcErrorAn endpoint failed.A message with any token, and any URL’s key, masked.
try {
  const c = await bz.trade.createOrder(eth.address, user, { orderType: OrderType.Market, isLong: true, size: parseUnits18("2") });
  const s = await bz.simulate(c);
  if (!s.ok) throw new CallError(s.error!.message, s.error);
  await bz.waitForReceipt(await bz.send(window.ethereum, c));
} catch (e) {
  if (e instanceof InputError) console.log(`field ${e.field} was refused before anything was built`);
  console.log(errorMessage(e)); // here: "maxSlippageBp: Set a slippage limit between 0.01% and 10%."
}

errorMessage falls back to “Something went wrong. Please try again.” for anything it does not recognize (a ConnectError, an endpoint failure, a UnitError from parsing a number), and never shows a raw URL. Log the error itself for the detail. See Errors & Messages.

Browser Safety

The SDK is written to run in a page an attacker can link to and feed data:

  • No secrets in the bundle. A Pyth Pro API key or a subgraph key in browser code is public. Give prices short-lived tokens your backend mints from the key (see Connecting), or leave prices to a relayer.
  • Only Pyth’s signature counts. Prices are read from the signed update itself, and a connection refuses updates not signed by a signer the deployment’s Pyth verifier trusts.
  • Keys are masked. Every error and display string built from an RPC, price or subgraph URL masks the parts that may be a key (redactUrl), and a Pyth Pro token never appears in one.
  • Untrusted addresses are checked. Every builder confirms a pair with the factory (requirePair) before building a call, an approval, a permit or a gasless request for it.
  • Untrusted text stays text. Market descriptions and proposal texts come from the chain or the subgraph: render them as text, never as HTML. Market labels come from Pyth’s feed symbols, never from a description.
  • Approvals are exact unless you ask for unlimited ones explicitly.
  • Signatures are checked. A signature that does not recover to the signer (a smart-contract wallet signing through ERC-1271, say) is refused before use, since the pair would act for whatever address it recovers.
  • Clocks are checked. Deadlines for permits and gasless requests are counted from the lower of the latest block’s time and this device’s clock, and the SDK refuses to ask for a signature (ClockSkewError) when the two differ by more than two minutes (local development chains excepted).

More in Security Notes for Integrators.

What Is Cached

The SDK reads the chain when you ask and keeps very little. What it keeps, it keeps per connection (per context object), except where noted:

WhatKeptRefreshed
The market list and each market’s static facts (pair id, feeds, calendar, label)Per connection, after the first listMarketslistMarkets({ refresh: true }); getMarket refreshes once by itself for a name it does not know. Static facts never change.
A pair’s Pyth feedsPer connectionNever (they cannot change); a failed read is not kept.
“This address is one of the factory’s pairs”Per connectionA refusal or a failed read is not kept, so a pair listed later is asked again.
Pyth feed metadata (symbols, about 1 MB)Per endpoint, for the whole page or processA failed fetch is not kept.
Whether the node supports state overrides, and whether Multicall3 existsPer ChainRe-detected if a node behind a load balancer stops honoring overrides.

Nothing else is cached. Snapshots, books, portfolios, nonces, balances and allowances are read fresh on every call, at one block, and returned as plain objects your UI can keep. A LivePrices stream holds the latest print per feed in memory, and a watcher holds the last snapshots it delivered; both are released when you stop them.

Next