Connecting
Every use of the SDK starts with a connection: one chain, one Bazaar deployment, and optionally a
price source and Bazaar’s subgraph. This page covers Bazaar.connect and connect() with every
option, the transports they accept, price sources (and how to keep a price API key out of a browser),
the subgraph, the integrator address, the chain check, checking market addresses, and the errors a
connection can raise.
What a connection is, and what the SDK caches on it, is on Core Concepts.
Bazaar.connect
const bz = await Bazaar.connect({
transport: window.ethereum, // an EIP-1193 provider, a viem client, a JsonRpc, or a JSON-RPC URL
factory: FACTORY, // the deployment: everything else is read from it
chainId: 42161, // optional: refuse any other chain
prices: { token: pythToken }, // optional: Pyth Pro client options, a ProClient, or any source
subgraph: "https://subgraph.example.com/bazaar", // optional: a URL, { url, apiKey }, or a source
integrator: MY_FEE_ADDRESS, // optional: named on every order this connection creates
});
bz.chainId; // 42161
bz.deployment; // { factory, lens, terminator, oracle, usdc }
bz.ctx; // the BazaarContext every function runs on
Bazaar.connect(options) does four things:
- Registers the user-facing error messages (
installUserMessages()), so a decoded revert reads as a sentence from then on. - Wraps the transport in a
Chain: a URL becomes the SDK’s own HTTP client (JsonRpc), anything else is used as given. - Reads the chain id and, in one batched call alongside it, the factory’s
lens,pairTerminator,oracleandusdc. (The first batch also detects how this node batches; see Transports.) - Checks the chain id against
chainId, when given, and that the factory answered all four reads.
It returns a Bazaar with the context bound into every area. Nothing else is read until you ask:
the market list loads on the first listMarkets (or anything that resolves a market by name).
| Option | Type | Default | Meaning |
|---|---|---|---|
transport | Transport | string | required | An EIP-1193 provider (a browser wallet’s, a viem client, a JsonRpc) or a JSON-RPC URL. See Transports. |
factory | Address | required | The deployment’s BazaarFactory. Everything else is read from it. Take it from Bazaar’s deployments page: anyone can deploy a fork. |
chainId | number | none | Refuse to connect on any other chain. |
prices | ProClientOptions | ProClient | PriceUpdateSource | null | null | Where signed Pyth Pro updates come from. Options ({ token, ... }) or a ProClient are made into a proPriceSource. Without one the connection is read-only for prices: every call that carries a price throws a ConnectError. See Prices. |
checkSigners | boolean | true | Have the price source use only updates signed by a signer the deployment’s Pyth verifier trusts. See Prices. |
subgraph | string | { url, apiKey? } | SubgraphSource | null | null | Bazaar’s subgraph. A string or { url, apiKey } is made into an httpSubgraph. Needed only for history (and for market descriptions in the market list). See The Subgraph. |
integrator | Address | null | the zero address | Named on every order this connection creates; earns the integrator fee on their fills. See The Integrator Address. |
The returned object has:
| Member | What it is |
|---|---|
bz.ctx | The BazaarContext: chain, chainId, factory, lens, terminator, oracle, usdc, prices, subgraph, integrator. |
bz.chainId | The chain id the transport reported. |
bz.deployment | { factory, lens, terminator, oracle, usdc }. |
bz.markets, bz.account, bz.trade, … | Every area’s context-first functions, bound. |
bz.simulate(call, opts?), bz.send(wallet, call, opts?), bz.waitForReceipt(hash, opts?) | The write path (Sending Transactions). |
bz.livePrices(opts?) | A live price stream for every listed market (Live Prices). |
connect() and Bazaar.from
connect(options) is what Bazaar.connect calls. It returns the bare BazaarContext, for code that
uses the per-area functions (markets.getMarket(ctx, ...)), and takes sources only: its prices must
be a PriceUpdateSource and its subgraph a SubgraphSource, not URLs. Bazaar.from(ctx) wraps a
context in the facade.
// connect() is what Bazaar.connect calls: it takes sources, not URLs, and returns the bare context.
const ctx = await connect({
transport: rpc,
factory: FACTORY,
chainId: 42161,
prices: proPriceSource({ token: pythToken }),
subgraph: history.httpSubgraph("https://subgraph.example.com/bazaar"),
integrator: null, // zero address: no integrator fee on this connection's orders
});
const wrapped = Bazaar.from(ctx);
| Option | Type | Default |
|---|---|---|
transport | Transport | string | required |
factory | Address | required |
chainId | number | none |
prices | PriceUpdateSource | null | null |
checkSigners | boolean | true |
subgraph | SubgraphSource | null | null |
integrator | Address | null | the zero address |
The SDK keeps its caches per context object. Two contexts built from the same options (or a copy made
with { ...ctx, prices }) each list the markets once.
Transports
A Transport is anything with EIP-1193’s request({ method, params }). Four kinds are common:
// A browser wallet: reads, and the wallet the user signs with.
const viaWallet = await Bazaar.connect({ transport: window.ethereum, factory: FACTORY });
// A viem client (wagmi's clients are viem clients too).
const client = createPublicClient({ chain: arbitrum, transport: http("https://arb1.arbitrum.io/rpc") });
const viaViem = await Bazaar.connect({ transport: client, factory: FACTORY });
// A URL: the SDK builds a JsonRpc with its defaults.
const viaUrl = await Bazaar.connect({ transport: "https://arb1.arbitrum.io/rpc", factory: FACTORY });
// A JsonRpc of your own, to change those defaults.
const rpc = new JsonRpc(`https://arb-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, {
perSecond: 10, // requests per second (default 25)
timeoutMs: 15_000, // per request (default 30 s)
retries: 5, // after transient failures, doubling from 500 ms (default 3); never for sends
headers: { "x-client": "my-bot" }, // sent with every request
maxResponseBytes: 32 * 1024 * 1024, // default 64 MiB
});
const viaJsonRpc = await Bazaar.connect({ transport: rpc, factory: FACTORY });
console.log(rpc.displayUrl); // "https://arb-mainnet.g.alchemy.com/v2/***": safe to log
- A browser wallet’s provider (
window.ethereum, or one from an EIP-6963 picker). Reads go through the wallet’s own RPC. The same object is thewalletyou send and sign with. - A viem client. A
PublicClientorWalletClienthas a matchingrequest; wagmi’s clients are viem clients. - A URL,
http(s)://. The SDK makes aJsonRpcwith its defaults. - A
JsonRpcof your own, to change those defaults:
JsonRpc option | Default | Meaning |
|---|---|---|
perSecond | 25 | Requests per second the client allows itself; extra requests queue in order. |
timeoutMs | 30,000 | Per request. |
retries | 3 | Retries after a failed request (HTTP errors, connection errors, timeouts, unreadable answers), waiting 500 ms, then 1 s, 2 s, … Never for a send (eth_sendRawTransaction, eth_sendTransaction, …). A JSON-RPC error answer (a revert) is not retried. |
headers | none | Sent with every request. A value HTTP cannot carry (a line break) is refused when the client is made. |
fetch | globalThis.fetch | The fetch to use. |
maxResponseBytes | 64 MiB | A larger answer is refused, not retried. |
A JsonRpc never puts its URL or a header’s value into an error message: displayUrl is the URL
with every part that may be a key masked. Its errors are HttpError (a non-2xx answer, with
status), RpcError (the node’s JSON-RPC error, with code and revertData) and
ResponseTooLargeError.
What the node needs. Batched reads go through the SDK’s probe contract placed with an eth_call
state override where the node honors overrides, through Multicall3 where it is deployed, and as
parallel eth_calls otherwise; the SDK detects which once per connection. Any node works. Live
updates use eth_getLogs, and wake on eth_subscribe("newHeads") when the transport emits EIP-1193
"message" events (a JsonRpc does not, so a URL connection polls). See
Live Updates.
Prices
Calls that need the price (order creation and amendment, trigger execution, a withdrawal with a
position, an insurance exit) carry a signed Pyth Pro update, fetched from the connection’s
PriceUpdateSource when the call is built. Reads use it too: the market list takes labels from Pyth
Pro’s symbols registry, and snapshots ask the source for the price of trading-hours markets to tell
whether their venue is open.
Pyth Pro needs a token on every request and every stream connection. There are two kinds: the long-lived API key, and short-lived tokens minted from it (they live at most 15 minutes). A server (a bot, a keeper, a relayer) may hold the key. A browser must not: code in a bundle is public. A frontend therefore either gets short-lived tokens from its own backend (below), or leaves prices out and routes orders through a relayer that attaches the update (Gasless Trading).
prices takes three forms:
- Options for a client:
{ token }, wheretokenis the API key (servers), or a function that returns a short-lived token and is called for each request and each connection attempt. The otherProClientoptions go in the same object. - A
ProClientyou made, to share it with a live stream. - A
PriceUpdateSourceyou build:proPriceSource(...), aLivePricesstream, or your own.
// The options form of `prices` builds this source; build it yourself to keep hold of its client.
const prices = proPriceSource({
token: process.env.PYTH_PRO_API_KEY, // sent as "Authorization: Bearer <key>": the key itself, on servers only
channel: "fixed_rate@200ms", // the default; also "real_time", "fixed_rate@50ms", "fixed_rate@1000ms"
timeout: 5, // seconds per request (default 10)
});
const withPrices = await Bazaar.connect({ transport: rpc, factory: FACTORY, prices });
// One ProClient can serve both the calls' updates and a live stream.
const pro = new ProClient({ token: pythToken });
const shared = await Bazaar.connect({ transport: window.ethereum, factory: FACTORY, prices: pro });
const live = await shared.livePrices(); // streams through the same client
// Connecting has the source refuse any update not signed by a signer the deployment's Pyth verifier
// trusts (one cached read per signer). The chain checks every update a call carries either way.
const unchecked = await Bazaar.connect({ transport: rpc, factory: FACTORY, prices: { token: pythToken }, checkSigners: false });
proPriceSource(options | ProClient) and new ProClient(options) take:
| Option | Default | Meaning |
|---|---|---|
token | none | A string, or () => string | Promise<string>. Sent as Authorization: Bearer <token>; on a stream connection in a browser, in the subprotocol list instead, since a browser cannot set a WebSocket’s headers. |
channel | "fixed_rate@200ms" | The channel streamed and read by latest: real_time, fixed_rate@50ms, fixed_rate@200ms or fixed_rate@1000ms. A feed is served on its registry min_channel and every slower one. |
streamUrls | Pyth’s three | The WebSocket endpoints. The stream connects to all of them, as Pyth requires, and yields each update once. |
restUrl | Pyth’s | Base URL of the request API (/v1/latest_price, /v1/price). A URL with a query or a fragment is refused: a key goes in token or headers. |
symbolsUrl | Pyth’s | Base URL of the symbols registry, which needs no token. No query or fragment either. |
headers | none | Extra request headers for the request API and the stream (an endpoint behind your proxy), on a stream connection wherever the runtime lets a WebSocket set headers (not in a browser). Never sent to the symbols registry. |
timeout | 10 | Seconds a request may take (the stream has no limit). |
fetch | globalThis.fetch | The fetch to use. |
WebSocket | the global one | The WebSocket constructor to stream with. Node before 22 has none: pass a package’s, such as ws. |
streamAuth | by runtime | "header" or "subprotocol": how a stream connection presents the token. Defaults to the subprotocol in a browser or a web worker, the header elsewhere. |
isTrustedSigner | none | (signer) => boolean: when set, the signer of each update the client takes is recovered and an update it refuses is never used. connect sets it for you (below). |
allowInsecure | false | Send credentials over plain http:// or ws:// to an endpoint that is not this machine: the token and headers, and a user name, password or query inside an endpoint’s URL. Refused otherwise. Only a host that is exactly localhost, an address in 127.0.0.0/8 or [::1] counts as this machine (a name under .localhost does not: it may be resolved through DNS). |
clock | the system clock | Unix seconds a streamed update’s own timestamp is judged against (a keeper passes the chain’s). |
maxUpdateAhead | 60 (MAX_UPDATE_AHEAD) | Seconds a streamed update may be stamped ahead of clock; an endpoint sending one further ahead is dropped and reconnected later. |
The source has latest(feedIds) (one request for exactly those feeds, so the update carries them and
nothing else, both legs of a composite in one; and the second the oldest of their prices was generated
in), atTick(feedIds, second) (the one-second channel’s update for a past second, which settlement
proofs are made of), prints(feedIds) (the parsed prints) and client, its ProClient. Every price is
read from the signed update itself, never from the JSON an endpoint sends beside it, so a dishonest
endpoint can only withhold prices, not change them, and every update is kept exactly as Pyth’s
verifier reads it: bytes an endpoint appends to one never reach a call’s calldata.
The client keeps its credentials out of sight: the token, the headers and the configured URLs are
private fields, so printing or serializing a client (or a context, or a PriceService) shows none of
them; every message it produces has them masked and every URL in it redacted; and a request carrying
a token or headers never follows a redirect.
Whose signature. Pyth signs every update, and the chain accepts one only from a signer its
verifier contract trusts. connect gives the source the same test: it reads the verifier from the
deployment’s oracle (pythLazer()) and asks it isValidSigner(signer) for each new signer, keeping
the answer for a minute. An update signed by anyone else is refused before it becomes a price your
page shows or a call carries. Checking costs one signature recovery per update; pass
checkSigners: false to leave the source as you gave it. The chain verifies every update a call
carries either way.
Feed IDs. A Pyth Pro feed is a number (BTC/USD is 1). The contracts hold it in a bytes32
(0x00…01), and the SDK uses that form everywhere a feed ID appears; feedIdHex(1) and
proFeedId(id) convert. A composite market’s ID is a hash, and its two legs are raw feed IDs.
Tokens for a Browser
Your backend exchanges the API key for a short-lived token and hands that to your page:
const KEY = process.env.PYTH_PRO_API_KEY ?? ""; // stays on the server
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN ?? "https://app.example.com";
// One token serves every visitor until a minute before it expires (Pyth's tokens live at most 15 minutes).
let held: MintedToken | null = null;
async function currentToken(): Promise<MintedToken> {
const now = Date.now() / 1000;
if (!held || (held.expiresAt ?? 0) - 60 < now) held = await mintProToken(KEY);
return held;
}
createServer(async (req, res) => {
res.setHeader("access-control-allow-origin", ALLOWED_ORIGIN);
if (req.method !== "GET" || req.url !== "/api/pyth-token") {
res.writeHead(404).end();
return;
}
// Gate this like any endpoint of yours (a session, a rate limit): whoever is handed a token can
// read prices on your key until it expires.
try {
const { token, expiresAt } = await currentToken();
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
res.end(JSON.stringify({ token, expiresAt }));
} catch (e) {
console.error(e instanceof Error ? e.message : e); // the SDK's errors never hold the key
res.writeHead(502).end();
}
}).listen(8787);
The page asks for one when it needs it, and keeps it until shortly before it expires:
// In a browser: short-lived Pyth Pro tokens from your own backend, which holds the API key. The
// endpoint answers { token, expiresAt }; a new one is fetched a minute before the held one expires.
const pythToken = cachedToken(() => fetch("/api/pyth-token").then((r) => r.json()));
cachedToken(mint, { margin }) calls mint only when it holds no token or the one it holds expires
within margin seconds (default 60), and shares one call among concurrent requests. A token whose
expiry the endpoint does not give is reused for five minutes. Gate the endpoint like any other of
yours: whoever is handed a token can read prices on your key until it expires.
Your Own Source
A PriceUpdateSource is one method, and two optional ones:
| Method | Returns |
|---|---|
latest(feedIds) | { update: Hex[], publishTime }: the update a call’s priceUpdate takes, carrying those feeds, and the second the oldest of their prices was generated in (Unix seconds). |
atTick?(feedIds, second) | The one-second channel’s update stamped exactly second, as a ProUpdate, or null when there is none. Only termination settlement uses it. |
useSignerCheck?(check) | Called by connect with the deployment’s signer test. Leave it out if your source cannot check signers. |
A source that is not a proPriceSource gives the SDK no symbols registry and no parsed prints: the
market list then takes labels from Pyth’s own registry (PYTH_PRO.symbolsUrl), and snapshots judge a
trading-hours venue only from live prices you pass them.
The Subgraph
History (trades, candles, orders and their events, positions, fees, liquidations, insurance, proposals and votes) comes from Bazaar’s subgraph. The market list also takes each market’s listing description from it, when it can; every other read comes from the chain.
const withHistory = await Bazaar.connect({
transport: rpc,
factory: FACTORY,
subgraph: history.httpSubgraph("https://gateway.thegraph.com/api/subgraphs/id/...", process.env.GRAPH_API_KEY, {
timeoutMs: 10_000, // per query (default 30 s)
}),
});
const lag = await withHistory.history.subgraphLag(); // how far the index trails the chain
subgraph takes a GraphQL URL, { url, apiKey }, or a SubgraphSource (anything with
query(query, variables)). The URL forms build history.httpSubgraph(url, apiKey?, options?), which
POSTs { query, variables } and sends the key as Authorization: Bearer <key> (The Graph’s gateway
also takes it in the URL path; such a URL is masked in errors). Its options:
| Option | Default | Meaning |
|---|---|---|
timeoutMs | 30,000 | Per query. |
headers | none | Extra request headers. |
fetch | globalThis.fetch | The fetch to use. |
The same rule about keys applies as for prices. Without a subgraph, every history function throws a
ConnectError; the rest of the SDK is unaffected. See History.
The Integrator Address
An order names an integrator, an address that earns 0.25 bp of each fill’s notional from the side of
the fill that carries it (see Bazaar’s integrator guide).
The connection’s integrator is written into every order trade.createOrder and trade.prepareOrder
build (and so into gasless order requests). It is your users’ surcharge as well as your revenue:
leave it unset (the zero address) to charge none. Collect what it earns with trade.claimFees on each
market.
Chain Checks
With chainId given, connect throws a ConnectError when the transport is on another chain. Give
it in a frontend: a wallet can be on any network, and a factory address means nothing on the wrong
one. The SDK does not follow the wallet’s chainChanged events: listen for them yourself and connect
again.
Signing (permits, gasless requests) also checks the clocks: deadlines are counted from the lower of
the latest block’s time and this device’s clock, and a ClockSkewError refuses to sign when they
differ by more than two minutes (except on local development chains, ids 31337 and 1337). Its message
tells the user to set their clock to update automatically.
Checking Market Addresses
A market address that reaches your code from a link, a query string or a subgraph row is untrusted.
Every builder checks its pair with the factory (factory.isPair) before building anything for it,
and requirePair makes the same check for your own code:
// An address from a link or a query string is untrusted: ask the factory before using it.
const fromLink = new URLSearchParams(location.search).get("market") as Address;
const pair = await requirePair(bz.ctx, fromLink); // checksummed, or an InputError on "pair"
requirePair(ctx, address) resolves to the checksummed address, or rejects with an InputError on
pair (“… is not a market of this Bazaar deployment, so nothing will be built or signed for it.”).
A confirmation is cached per connection; a refusal is not, so a market listed later is accepted.
markets.getMarket resolves only the factory’s own markets, so a market found through it needs no
further check.
What Can Go Wrong
try {
await Bazaar.connect({ transport: window.ethereum, factory: FACTORY, chainId: 42161 });
} catch (e) {
if (e instanceof ConnectError) {
// "connected to chain 421614, expected 42161", or
// "0x... does not answer as a Bazaar factory on chain 42161"
console.error(e.message);
} else {
console.error(errorMessage(e)); // the wallet or the RPC failed
}
}
| Error | Message | Why |
|---|---|---|
ConnectError | connected to chain 421614, expected 42161 | The transport (the wallet) is on another chain. |
ConnectError | 0x... does not answer as a Bazaar factory on chain 42161 | The factory’s reads reverted: the address is some other contract. |
viem’s AbiDecodingZeroDataError | Cannot decode zero data ("0x") ... | The factory address has no contract on this chain at all (the reads “succeed” with empty answers). |
viem’s InvalidAddressError | Address "..." is invalid. | factory or integrator is not an address. |
HttpError | HTTP 401 from https://.../v2/***: the provider refused the request; check the key in the URL | The RPC refused the request. |
TypeError | JsonRpc: the ... header's value holds a line break, ... | A header value (often from an .env file) has a stray character. |
ProError | the price token (prices.token) holds a line break ... | The same, for the Pyth Pro token. Raised when the client is made, or when a token provider returns one. |
ConnectError | this call needs a fresh Pyth Pro price: connect with a price source (prices) | Later, from a builder: the connection has no prices. |
ConnectError | history comes from Bazaar's subgraph: connect with a subgraph source | Later, from history: the connection has no subgraph. |
errorMessage(e) shows these as “Something went wrong. Please try again.” (they are not the user’s
doing); log e.message for the detail.