# Cross-Chain Quote Client > An isomorphic TypeScript library that asks several cross-chain bridge > aggregators for live quotes at once, merges the answers, and helps you > route a transfer for the best delivered amount at the fewest wallet > signatures. It runs unchanged in a browser tab or on a Node server. It > carries no host name, no chain policy, and no secret of its own — you > supply all three through one configuration object. ## Quick orientation - This package runs in three modes: **from your own backend** (with real provider keys), **straight from a browser** (with no secret, or a public identifier only), or **against your own hosted quote service, with a browser fallback**. Pick one mode below and use its configuration shape. - It also runs in React Native 0.77 or later with no polyfill, not even for `URL`: React Native's own `URL` and `URLSearchParams` are partial, and no entry point reads them. - The wire contract below (`POST /v1/display-quotes`) is what a hosted quote service and a direct-from-browser client both speak. If you run your own service with the server-side pieces of this package, your server and your browser client already agree on the same request and response shapes. - A short list of rules never bend. Read them before you build anything that signs a transaction from a quote this library returns. ## Install Add the package as a dependency with your usual package manager, then import from its root: ```ts import { createQuoteClient, type QuoteClientConfig } from '' ``` Replace `` with the name you installed this under — you already know it from your own `package.json` dependency entry. ## Configuration: `QuoteClientConfig` Every fact about who is asking — provider keys, allowed origins, chain policy — lives in one object. Nothing else in this library carries a default that assumes a particular deployment. ```ts type QuoteClientConfig = { providers?: { lifi?: { apiKey?: string; integrator?: string; baseUrl?: string; fee?: number } relay?: { apiKey?: string; referrer?: string; baseUrl?: string } nearIntents?: { apiKey?: string; referral?: string; baseUrl?: string } } serviceUrl?: string fallback: 'direct' | 'none' probeAddresses?: Partial> neverSendChainIds?: readonly number[] cache?: { ttlMs?: number; maxEntries?: number } limits?: { concurrencyByCarrier?: Record; defaultRetryAfterMs?: number } fetch?: typeof fetch now?: () => number } ``` `fallback` is required on purpose. Every caller states what happens when `serviceUrl` cannot be reached, instead of inheriting a silent default. ## Mode 1: your own backend, with real secrets Run this on a server you control. Provider keys stay on that server and are never sent to a browser. ```ts const client = createQuoteClient( { providers: { lifi: { apiKey: process.env.LIFI_API_KEY, integrator: 'your-app' }, relay: { apiKey: process.env.RELAY_API_KEY }, }, fallback: 'none', neverSendChainIds: [/* any chain id your own bridge must never expose to an aggregator */], }, { carriers: [/* built-in and/or your own Carrier implementations — see below */] }, ) const response = await client.displayQuotes({ asks: [/* ... */] }) ``` ## Mode 2: straight from the browser, no service Send no secret to the browser. Omit every `providers.*.apiKey`, or send only a public, rate-limit-budget identifier — the same way a browser-safe value already works today. Leave `serviceUrl` unset. ```ts const client = createQuoteClient( { providers: { relay: { referrer: 'your-app.example' } }, fallback: 'none', }, { carriers: [/* your carriers */] }, ) ``` Every request this client makes goes straight from the visitor's browser to each provider. ## Mode 3: your own hosted service, with a browser fallback Point the browser at a service you run — built from this same package's server-side pieces — and fall back to Mode 2 automatically when that service cannot be reached. ```ts const client = createQuoteClient({ serviceUrl: 'https://quotes.your-app.example', fallback: 'direct', providers: { relay: { referrer: 'your-app.example' } }, // used only during a fallback }) ``` `fallback: 'direct'` opens a 60-second breaker after one failed call to `serviceUrl`, so a flapping service does not fail every single request on its way down; `fallback: 'none'` instead answers every ask with a `could-not-ask` refusal and never falls back to a direct call. ## Wire contract: `POST /v1/display-quotes` If you run your own service (Mode 3), have it answer this exact shape — the browser client this package builds already speaks it. Request body: `{ "asks": DisplayAsk[] }`, 1 to 24 asks, answered in order. ```ts type DisplayAsk = { from: { chainId: number; token: string } to: { chainId: number; token: string } amount: string // decimal string, base units of from.token recipientEcosystem?: 'evm' | 'svm' | 'bvm' | 'tvm' | 'tonvm' | 'hypevm' | 'zcash' // default: derived from to.chainId providers?: ('lifi' | 'relay' | 'near-intents')[] // default: every eligible provider precision?: 'estimate-ok' | 'exact' // default: 'estimate-ok' } ``` Response: `{ "answers": [{ "providers": { lifi?, relay?, "near-intents"? } }] }`, one answer per ask, in ask order. Each provider slot is one of three shapes: - **`quoted`** — `basis: 'exact'` or `basis: 'scaled-from-nearby-amount'`, plus `askedAmount`, `expectedAmount`, an optional `minimumAmount`, `quotedAt`, `ageMs`, and `cache: 'miss' | 'hit' | 'joined'`. A `scaled-from-nearby-amount` answer also carries `scaledFrom`: the real amount and the real answer it was scaled from. - **`refused`** — `kind: 'no-quote' | 'could-not-ask' | 'cannot-carry' | 'invalid-api-key'`, a `reason` (or null), and `minimumAmount` when the provider named one. - **`pending`** — the provider's own call is still running. Retry after `retryAfterMs`. The call keeps running in the background and fills the cache for the next ask that asks the same question. `precision: 'exact'` asks for a genuine answer to the exact amount you asked for. It never accepts a scaled answer — see "Rules that never bend," below. ## Route search: carriers The route-search engine asks every registered *carrier* — a built-in aggregator (LI.FI, Relay, NEAR Intents) or a local read you register yourself (a direct on-chain swap, your own bridge contract) — what it can deliver, then scores every combination it can build for the best delivered amount at the fewest wallet signatures. Register your own carrier by implementing the `Carrier` interface: ```ts const myOwnCarrier: Carrier = { id: 'my-carrier', edgesFrom: (from, to) => [/* every edge this carrier can move funds across right now */], isRateLimited: false, // true for anything that calls a rate-limited network API ask: async ({ edge, inputAmount }) => { // return { ok: true, deliveredAmount, minimumDeliveredAmount } or // { ok: false, refusal: { kind, reason, minimumAmount } } }, // fusesWithNext?: (nextHop) => boolean — set this only when arriving via // this carrier ALSO executes the next hop, so the two cost one signature. } ``` "Fewest signatures for the most delivered" is the engine's actual scoring order, not a slogan: it maximizes delivered amount first, then minimizes how many wallet signatures a route costs among routes that tie. A carrier that fuses two hops into a single signature beats an otherwise-equal route that costs an extra step. Every network call a carrier makes — built in or your own — passes through this library's shared guards (a keyed concurrency limiter, a Retry-After cooldown, and your `neverSendChainIds` tripwire) before it ever reaches `fetch`, once you wrap it through the client's upstream funnel. ## Rules that never bend - **`neverSendChainIds` is enforced everywhere, including forwarding.** If you run a chain that must never reach a public aggregator, name its id here. Every outbound call this library makes, direct or forwarded, refuses any request naming it. - **A binding quote is never cached or shared.** A quote that reserves a real user address, real calldata, or a live deposit address answers fresh every time — never from this library's own cache. - **A scaled figure never reaches an execution builder.** `expectedAmount` and `minimumAmount` on a `basis: 'scaled-from-nearby-amount'` answer carry a distinct type brand (`EstimatedDelivery`) from a genuine `basis: 'exact'` answer (`ExactAmount`). The function that decodes an amount for spending only accepts the exact brand, so mixing them up is a compile error, not a rule you have to remember to check. - **Provider keys never run in a browser.** If your frontend needs a display quote and you hold a real key, put it behind your own service (Mode 3) — never Mode 2 with a real `apiKey` set on a browser build. - **Verify a route's execution calldata before signing it**, independent of what a display quote showed. A display quote exists to show a reader what to expect; it is never the thing that gets signed. Build and check the real execution transaction separately, immediately before asking a wallet to sign it.