# spanDEX > DEX meta-aggregator library for optimal token swaps ## Advanced #### Configuration Deadlines, timeouts, retries, etc #### Overrides #### Promise Chaining ## Examples If you build something cool with spanDEX, let us know. * [spanDEX Swap Demo](https://demo.spandex.sh) - Swap interface which compares performance of multiple aggregators and continuously provides the best price * [QuoteBench](https://benchmark.withfabric.xyz) - A benchmarking tool to compare stability, latency, accuracy, and price across aggregators and chains over time * [Hono Proxy](https://github.com/withfabricxyz/spandex/tree/main/examples/hono) - A minimal meta-aggregator API server built with Hono and spanDEX ## Fees This page moved to [Configuration > Fees](/configuration/fees). import { Button } from 'vocs/components' ## Getting Started Depending on your project stack and state, there are multiple ways to get started with spanDEX. ## Existing Projects See [installation instructions](/installation.mdx) for details on how to add spanDEX to your existing project. For react applications, see the [React getting started guide](/react/getting-started.mdx). Otherwise, see the [Core getting started guide](/core/getting-started.mdx). ## New Projects We currently don't have a CLI available, but we're working on it! In the meantime, you can follow the instructions in the [Core getting started guide](/core/getting-started.mdx) to set up spanDEX in your project. ## Installation ### Core `@spandex/core` provides all the functionality to interact with multiple aggregators, simulate quotes onchain, and execute swaps. It depends on `viem` for blockchain interactions. :::code-group ```bash [npm] npm i viem @spandex/core ``` ```bash [bun] bun i viem @spandex/core ``` ```bash [pnpm] pnpm i viem @spandex/core ``` ::: ### React `@spandex/react` provides React hooks for integrating spanDEX into your React applications. It depends on `viem`, `wagmi`, and TanStack Query. :::code-group ```bash [npm] npm i viem wagmi @tanstack/react-query @spandex/react ``` ```bash [bun] bun i viem wagmi @tanstack/react-query @spandex/react ``` ```bash [pnpm] pnpm i viem wagmi @tanstack/react-query @spandex/react ``` ::: ### Security Considerations Given that spanDEX presents unsigned transaction data for execution, it's crucial have a security stance. Some recommended practices include: * Lock or pin dependencies to specific versions to reduce supply chain attack risk. * Use a package manager that supports integrity checks, such as npm or yarn, to verify the authenticity of packages. * Require packages have a minimum age before updating, using pnpms minimumReleaseAge or similar features in other package managers. * Ensure web apps have Content Security Policies (CSP) in place to mitigate risks from malicious scripts. * Remember that supply chain attacks are real and the EVM is a ripe target. ## Why spanDEX Let's clarify some key concepts in DeFi: DEX aggregators and meta aggregators. ### Why Aggregators? Aggregators monitor liquidity across multiple DEXes. The benefits are numerous: * **Better Prices**: By indexing liquidity from various sources, aggregators find better prices for swaps than any single DEX could offer. * **Reduced Slippage**: Aggregators can split trades across multiple DEXes to minimize slippage, resulting in more favorable execution prices. * **Atomic Swaps**: Aggregators can facilitate complex trades that involve multiple tokens or routes, allowing users to swap tokens that may not have direct liquidity pools. ### Why Meta Aggregators? Meta aggregators automate the process of querying multiple DEX aggregators. The advantages include: * **Expanded Liquidity**: Not all aggregators cover the same DEXes or tokens. By using a meta aggregator, users can access a broader range of liquidity sources. * **Optimal Pricing**: Meta aggregators can compare quotes from different aggregators to ensure users receive the best possible price for their swaps. * **Redundancy**: If one aggregator is down or experiencing issues, a meta aggregator can fall back to other aggregators, ensuring continuous service. ### And spanDEX? spanDEX is an open-source meta aggregator library provides all the benefits of a meta aggregator in a developer-friendly package without any middlemen. Benefits: * **Open Source**: spanDEX is fully open source, allowing developers to inspect, modify, and contribute to the codebase. * **No Middlemen**: spanDEX interacts directly with DEX aggregators, eliminating a centralized meta-aggregation service that could introduce latency, fees, censorship, or single points of failure. * **Customizable**: Developers can choose which aggregators to include, configure settings, and extend functionality to suit their specific use cases. * **Onchain Simulation**: spanDEX supports onchain simulation of swaps, allowing developers to verify quote accuracy, gas costs, and potential reverts before executing transactions. **The world is yours.** ## Onchain Simulation ### Overview While fetched quotes provide a good estimate of swap costs, simulating a swap can provide a more accurate picture of how the swap will perform when executed onchain. Simulation performs the swap and tracks state changes as if the transaction was executed onchain. This allows us to: * Confirm the accuracy of quotes by verifying that their quotes aren't stale or otherwise incorrect * Include gas costs to compare real costs across different aggregators * Verify swaps won't revert due to slippage, approvals, or insufficient balance Additionally, there are edge cases such as contracts that have allowlists or other unexpected behavior that can only be uncovered through simulation or a real transaction. Ultimately, simulation allows us to compare quotes against their simulated onchain output, not just quoted estimates. ### Configuration > ⚠️ Some RPC providers do not support the underlying > [`eth_simulateV1`](https://github.com/ethereum/execution-apis/pull/484) method required for > simulation. Refer to your RPC provider's documentation to ensure that simulation is supported. To enable onchain simulation, you'll need: 1. a viem `PublicClient` connected to the target chain 2. a `MetaAggregator` instance from the core spanDEX package with your desired aggregators configured import ConfigParams from "../../snippets/params/config.mdx"; ## ConfigParams ```ts import type { ConfigParams } from "@spandex/core"; ``` import SwapParams from "../../snippets/params/swap.mdx"; ## SwapParams ```ts import type { SwapParams } from "@spandex/core"; ``` ## Getting Started - React To get started we will create a shared meta aggregator instance with three providers, default settings, and support for base. Then, we will fetch the best quote for a swap and execute it. ### 1. Install Install the core library: :::code-group ```bash [npm] npm i viem wagmi @tanstack/react-query @spandex/react ``` ```bash [bun] bun i viem wagmi @tanstack/react-query @spandex/react ``` ```bash [pnpm] pnpm i viem wagmi @tanstack/react-query @spandex/react ``` ::: ### 2. Configure See the [configuration reference](/configuration) for all options. ```ts import { WagmiProvider } from "wagmi"; import { wagmiConfig } from "./wagmiConfig.js"; import { SpandexProvider } from "@spandex/react"; import { fabric, zeroX } from "@spandex/core"; const config = { providers: [fabric({ appId: "YOUR_FABRIC_APP_ID" }), zeroX({ apiKey: "YOUR_0X_API_KEY" })], }; export function App() { return ( ); } ``` ### 3. Fetch Quotes ```ts import { useQuotes } from "@spandex/react"; export function Quotes() { const { data, isLoading, error } = useQuotes({ mode: "exactIn", inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC outputToken: "0x4200000000000000000000000000000000000006", // WETH inputAmount: 1000000n, // 1 USDC slippageBps: 100, // 1% }); if (isLoading) return
Fetching quotes...
; if (error) return
Error: {error.message}
; return (
{quotes?.map((quote) => (
{quote.provider}: {quote.outputAmount.toString()}
))}
); } ``` ## useQuote Fetch quotes from all configured aggregators, simulate them, and select a winner using a strategy. ### Example ```ts twoslash import { useQuote } from "@spandex/react"; function App() { const { data, isLoading, error } = useQuote({ swap: { inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance }, strategy: "bestPrice", }); } ``` ### Returns ```ts { data: SuccessfulSimulatedQuote | null | undefined; isLoading: boolean; error: Error | null; // ...other React Query return values } ``` ### Params #### swap The swap parameters, with optional `chainId` and `swapperAccount` (inferred from the current connection if omitted) and optional `recipientAccount` (defaults to `swapperAccount`). ##### swap params * `inputToken: string` - The input token address. * `outputToken: string` - The output token address. * `mode: "exactIn" | "targetOut"` - The swap mode. * `inputAmount?: bigint` - The input amount (required if mode is "exactIn"). * `outputAmount?: bigint` - The desired output amount (required if mode is "targetOut"). * `slippageBps: number` - The slippage tolerance in basis points. * `recipientAccount?: string` - Optional output recipient address. #### strategy Defines how to choose the winning quote. Built-in strategies are `"fastest"`, `"bestPrice"`, `"estimatedGas"`, and `"priority"`. You can also supply a serializable strategy plan or a custom selection function. For strategy tradeoffs and collector/ranker composition examples, see [Strategies](/core/strategies). When using function strategies, or strategy plans that contain custom `collect` or `rank` functions, memoize them so the query key stays stable across renders. #### query Optional React Query overrides (for example `enabled`, `staleTime`, or `select`). queryKey and queryFn are managed by spanDEX and cannot be provided. import { Callout } from 'vocs/components' ## useQuoteExecutor Coming soon ## useQuotes Fetch quotes from all configured aggregators and simulate each quote. ### Example ```ts twoslash import { useQuotes } from "@spandex/react"; function App() { const { data, isLoading, error } = useQuotes({ swap: { inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance } }); } ``` ### Returns ```ts { data: SimulatedQuote[] | undefined; isLoading: boolean; error: Error | null; // ...other React Query return values } ``` ### Params #### swap The swap parameters, with optional `chainId` and `swapperAccount` (inferred from the current connection if omitted) and optional `recipientAccount` (defaults to `swapperAccount`). ##### swap params * `inputToken: string` - The input token address. * `outputToken: string` - The output token address. * `mode: "exactIn" | "targetOut"` - The swap mode. * `inputAmount?: bigint` - The input amount (required if mode is "exactIn"). * `outputAmount?: bigint` - The desired output amount (required if mode is "targetOut"). * `slippageBps: number` - The slippage tolerance in basis points. * `recipientAccount?: string` - Optional output recipient address. import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## 0x
### Configuration 0x requires an API key for access. You can obtain one by [signing up](https://dashboard.0x.org/create-account). Use the `zeroX` factory since JavaScript identifiers cannot start with a number. ```ts twoslash import { createConfig, zeroX } from "@spandex/core"; export const config = createConfig({ providers: [ zeroX({ // required! apiKey: "YOUR_0X_API_KEY", // If you have negotiated surplus sharing with 0x, override negotiatedOptions // negotiatedFeatures: ["integratorSurplus"], }), // ... ], }); ``` ### Fee Token Preference 0x supports fee-token selection through `swapFeeToken`. spanDEX forwards dynamic `tokenPreference` from `integratorFeeFn` to that field when an integrator swap fee is configured. 0x requires the token to be either the sell token or buy token. import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## Fabric
### Configuration ```ts twoslash import { createConfig, fabric } from "@spandex/core"; export const config = createConfig({ providers: [ fabric({ appId: "YOUR_FABRIC_APP_ID", // Optional, use if you have negotiated service apiKey: "YOUR_FABRIC_API_KEY", // Optional, only set if you have a private Fabric deployment url: undefined, }), // ... ], }); ``` import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## KyberSwap
### Configuration ```ts twoslash import { createConfig, kyberswap } from "@spandex/core"; export const config = createConfig({ providers: [ kyberswap({ // Required, you choose the value clientId: "myawesomeapp", }), // ... ], }); ``` import { Button } from 'vocs/components' ## LiFi
### Configuration ```ts twoslash import { createConfig, lifi } from "@spandex/core"; export const config = createConfig({ providers: [ lifi({ apiKey: undefined, // Optional. Use it if you got it }), // ... ], }); ``` import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## Nordstern
### Configuration Nordstern supports `exactIn` quotes and integrator fees (forwarded as Nordstern's `convenienceFee`). It does not support exact-output quotes. ```ts twoslash import { createConfig, nordstern } from "@spandex/core"; export const config = createConfig({ providers: [ nordstern({ // Optional, override the API base URL baseUrl: undefined, }), // ... ], }); ``` ### Notes * Nordstern is live on 140+ EVM-compatible chains. Use the [`/chains` endpoint](https://api.nordstern.finance/chains) to see current support. * Slippage is sent to Nordstern as a percentage. spanDEX converts the standard `slippageBps` (basis points) value automatically (`slippageBps / 100`). * When `integratorSwapFeeBps` and `integratorFeeAddress` are set, spanDEX forwards them as Nordstern's `convenienceFee` and `convenienceFeeRecipient`. * Nordstern's response does not include a gas-price estimate, so `networkFee` is reported as `0n`. Estimate gas yourself before submission if you need it. * Approvals must target the Nordstern router (`tx.to` from the response); spanDEX surfaces this on the `approval` field of the returned quote. import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## Odos
### Configuration ```ts twoslash import { createConfig, odos } from "@spandex/core"; export const config = createConfig({ providers: [ odos({ // Optional. Use for attribution referralCode: undefined, // Optional. Use it if you got it apiKey: undefined, // Optional. Enable fees and surplus if negotiated with Odos. // negotiatedFeatures: ["integratorSurplus", "integratorFees"], }), // ... ], }); ``` import { Button } from 'vocs/components' ## Relay
### Configuration ```ts twoslash import { createConfig, relay } from "@spandex/core"; export const config = createConfig({ providers: [ relay({ // Optional apiKey: "YOUR_RELAY_API_KEY", // Optional, override the API base URL url: undefined, }), // ... ], }); ``` import { Button } from 'vocs/components' import { Callout } from 'vocs/components' ## Velora
### Configuration Velora supports both `exactIn` and `targetOut` quotes, plus integrator fee and surplus options. ```ts twoslash import { createConfig, velora } from "@spandex/core"; export const config = createConfig({ providers: [ velora({ // Optional, override the API base URL baseUrl: undefined, // Optional, partner slug for attribution partner: undefined, // Optional, forwarded as `isDirectFeeTransfer` when fee/surplus capture is enabled // Default: true isDirectFeeTransfer: true, }), // ... ], }); ``` When using global aggregation options: * `integratorSwapFeeBps` maps to Velora's `partnerFeeBps` * `integratorSurplusBps` enables Velora's `takeSurplus` * Velora uses a single partner recipient (`partnerAddress`) for both flows. If both fee and surplus are requested with different addresses, spanDEX prioritizes the fee recipient. * `isDirectFeeTransfer` is configured on `velora({...})` setup and is only sent when `partnerAddress` is present. import { Callout } from "vocs/components"; ## Cross-Chain Swaps **Experimental:** Cross-chain swaps are currently experimental. You must include relay in your provider list to yield cross-chain quotes. Set `outputChainId` on `SwapParams` to request a cross-chain quote. spanDEX will only select providers that support cross-chain swaps. ### Example :::code-group ```ts [quote.ts] import { buildCalls, getQuote, type QuoteCheck } from "@spandex/core"; import { config, submitCalls } from "./config.js"; const swap = { chainId: 8453, outputChainId: 10, inputToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", outputToken: "0x4200000000000000000000000000000000000006", mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quote = await getQuote({ config, swap, strategy: "fastest", }); if (!quote) { throw new Error("No cross-chain quote was returned"); } const calls = await buildCalls({ config, swap, quote, }); await submitCalls(calls); if (quote.execution === "async") { await pollCrossChainCheck(quote.check); } async function pollCrossChainCheck(check: QuoteCheck) { while (true) { const response = await fetch(check.endpoint, { method: check.method, }); if (!response.ok) { throw new Error(`Status check failed with ${response.status}`); } const payload = (await response.json()) as { status?: string }; if (payload.status === "success") { return payload; } if (payload.status === "failure") { throw new Error("Cross-chain swap failed"); } await new Promise((resolve) => setTimeout(resolve, 3_000)); } } ``` ```ts [config.ts] import { type BuiltCall, createConfig, relay } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const baseClient = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); export const config = createConfig({ providers: [relay({})], clients: [baseClient] as PublicClient[], }); export async function submitCalls(calls: BuiltCall[]) { // Replace this with your wallet or backend execution flow. // For example: submit approval first if present, then submit the swap call. console.log("Submit calls:", calls); } ``` ::: ### Notes * `outputChainId` is the switch that enables cross-chain routing. * `buildCalls` and transaction submission happen on the origin chain. * For async cross-chain quotes, spanDEX currently simulates only the origin-side approval and swap calls, then trusts the quoted output amount for price comparison. * Continue polling `quote.check` until your provider reports a terminal status. ## Getting Started - Core To get started we will create a shared meta aggregator instance with four providers, default settings, and support for base. Then, we will fetch the best quote for a swap and execute it. ### 1. Install Install the core library: :::code-group ```bash [npm] npm i viem @spandex/core ``` ```bash [pnpm] pnpm i viem @spandex/core ``` ```bash [bun] bun i viem @spandex/core ``` ::: ### 2. Configure See the [configuration reference](/configuration) for all options. ```ts filename="config.ts" twoslash import { createConfig, fabric, kyberswap, nordstern, odos } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; // Create a base client for quote simulation const baseClient = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); // The aggregator instance can be shared across your application export const config = createConfig({ providers: [ fabric({ appId: "test" }), odos({}), kyberswap({ clientId: "test" }), nordstern({}), ], clients: [baseClient] as PublicClient[], }); ``` ### 3. Fetch Quotes and Execute ```ts filename="quote.ts" twoslash import { config } from "./config.js"; import { createWalletClient, http } from "viem"; import { executeQuote, getQuote } from "@spandex/core"; import { base } from "viem/chains"; const swapParams = { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quote = await getQuote({ config, swap: swapParams, strategy: "bestPrice", }); if (!quote) { throw new Error("No providers responded in time"); } const walletClient = createWalletClient({ account: "0xdead00000000000000000000000000000000beef", chain: base, transport: http(), }); const { transactionHash } = await executeQuote({ config, swap: swapParams, quote, walletClient, }); console.log(`Swapped via ${quote.provider} @ ${transactionHash}`); ``` ## Strategies Selection strategies decide when spanDEX stops waiting for quotes and how it picks a winner. ### Built-In Strategies | Strategy | Waits for All | Selection Criteria | Best For | | -------------- | ------------- | --------------------------------- | -------------------------------- | | `fastest` | ✗ | First successful quote | Lowest latency | | `bestPrice` | ✓ | Highest simulated output | Price optimization | | `estimatedGas` | ✓ | Lowest simulated gas used | Gas efficiency | | `priority` | ✓ | Feature priority, then best price | Integrator-controlled preference | ### fastest Returns the first successful simulated quote. Pros: * Lowest latency. * Good default when execution speed matters more than quote quality. Cons: * Most exposed to adverse selection. * Ignores potentially better quotes that arrive slightly later. ### bestPrice Waits for every provider to finish, then picks the highest simulated output. Pros: * Best price discovery across the full provider set. * Most direct choice when output amount is the primary objective. Cons: * Highest latency. * A slow provider delays selection. ### estimatedGas Waits for every provider to finish, then picks the successful quote with the lowest simulated gas. Pros: * Useful when gas dominates the decision. * Avoids paying extra for marginal output improvements. Cons: * Can sacrifice output amount. * Same latency tradeoff as `bestPrice`. ### priority Waits for every provider to finish, then prefers quotes with more activated features before using best price as the tie-breaker. Pros: * Useful when integrator features must be respected. * Lets configuration influence selection without fully overriding quality. Cons: * Can intentionally choose a worse price. * Requires understanding feature-priority tradeoffs. ### Composing Collectors and Rankers For composed strategies, spanDEX supports a serializable plan object with two parts: * `collect` decides when enough successful quotes have arrived * `rank` decides how to choose a winner from the collected subset This keeps "when do we stop waiting?" separate from "how do we choose the winner?" while preserving the original custom function API. Each phase can use either a built-in spec or a custom function. #### Collector Types ```ts type QuoteSelectionCollector = | { type: "all" } | { type: "firstN"; count: number } | { type: "benchmark"; provider: ProviderKey; minQuotes?: number } | ((quotes: Array>) => Promise) ``` * `all` waits for all providers * `firstN` waits for the first `count` successful quotes * `benchmark` waits for a successful quote from a required provider and at least `minQuotes` successful quotes total #### Rankers ```ts type QuoteSelectionRanker = | "first" | "bestPrice" | "estimatedGas" | "priority" | ((quotes: SuccessfulSimulatedQuote[]) => SuccessfulSimulatedQuote | null); ``` The built-in `"fastest"` strategy is equivalent to: ```ts { collect: { type: "firstN", count: 1 }, rank: "first", } ``` #### Strategy Plan ```ts type QuoteSelectionPlan = { collect: QuoteSelectionCollector; rank: QuoteSelectionRanker; } ``` #### Example: firstN + bestPrice This is the simplest example of a partial-collection strategy. ```ts import { getQuote } from "@spandex/core"; const quote = await getQuote({ config, swap, strategy: { collect: { type: "firstN", count: 2 }, rank: "bestPrice", }, }); ``` Pros: * Balances speed with some price discovery. * Can mitigate adverse selection compared with `fastest`. * Adapts naturally to other rankers like `estimatedGas`. Cons: * More likely to return no winner than `fastest`. * Still does not wait for the full provider set. Example: * With 3 providers and `firstN + bestPrice`, spanDEX can wait for the first 2 successful quotes, then pick the better price between those 2. * If only 1 provider succeeds, no winner is selected and `getQuote` returns `null`. * If `count < 1` or `count` exceeds the number of providers, collection throws. #### Example: benchmark + bestPrice ```ts const quote = await getQuote({ config, swap, strategy: { collect: { type: "benchmark", provider: "fabric", minQuotes: 2, }, rank: "bestPrice", }, }); ``` This means: * wait for a successful `fabric` quote * also wait for at least 2 successful quotes total * then choose the best-priced quote among that collected subset If the benchmark provider never succeeds, no winner is selected and `getQuote` returns `null`. #### Example: custom rank function This ranker stays purely price-first, then uses `performance.accuracy` as the tie-breaker. That gives the integrator "best price, but if two quotes are tied, prefer the one that was more accurate." ```ts const quote = await getQuote({ config, swap, strategy: { collect: { type: "all" }, rank: (quotes) => { return [...quotes].sort((a, b) => { if (a.simulation.outputAmount !== b.simulation.outputAmount) { return a.simulation.outputAmount > b.simulation.outputAmount ? -1 : 1; } const accuracyA = a.performance.accuracy ?? Number.POSITIVE_INFINITY; const accuracyB = b.performance.accuracy ?? Number.POSITIVE_INFINITY; if (accuracyA === accuracyB) return 0; return accuracyA < accuracyB ? -1 : 1; })[0] ?? null; }, }, }); ``` ### Custom Selectors You can supply any custom selector function that matches the selection signature. ```ts type QuoteSelectionFn = ( quotes: Array> ) => Promise ``` In React, memoize function strategies so the query key stays stable across renders. Use this when the built-in strategies do not match your routing policy. ## buildCalls Build the transactions needed to execute a simulated quote. The return value is an array of calls in execution order: an ERC-20 approval first when required, followed by the swap transaction. ### Example ```ts import { buildCalls, getQuote } from "@spandex/core"; import { config } from "./config.js"; const swap = { chainId: 8453, inputToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC outputToken: "0x4200000000000000000000000000000000000006", // WETH mode: "exactIn", inputAmount: 500_000_000n, slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quote = await getQuote({ config, swap, strategy: "bestPrice", }); if (!quote) { throw new Error("No providers returned a successful quote"); } const calls = await buildCalls({ config, swap, quote, }); for (const call of calls) { console.log(call.type, call.txn); } ``` ### What It Does * Builds the swap transaction from `quote.txData`. * If the quote includes ERC-20 approval metadata, it can also build an `approve` call for the token spender. * Unless `force` is set, it does an onchain allowance check before adding that approval call. * If the current allowance already covers `quote.inputAmount`, the approval call is omitted. * Native-token swaps do not need an ERC-20 approval, so only the swap call is returned. ### Gas Limit When simulation gas metadata is available on the quote, `buildCalls` sets `txn.gasLimit` on the returned calls: * Swap calls use `quote.simulation.gasUsed`. * Approval calls use `quote.simulation.approvalGasUsed`. * Those estimates are padded by 33% before being written to `gasLimit`. * If that simulation metadata is missing, `gasLimit` is left undefined. The padding is there to reduce avoidable transaction failures from minor differences between simulation and live execution conditions. ### Params #### quote `SuccessfulSimulatedQuote` The simulated quote to convert into executable calls. This is typically the result of [`getQuote`](./getQuote.mdx) or one item from [`getQuotes`](./getQuotes.mdx). #### swap `SwapParams` Swap parameters associated with the quote. #### config `Config` Aggregator configuration created via [`createConfig`](./createConfig.mdx). Used to resolve a public client for allowance checks. #### publicClient `PublicClient | undefined` Optional client used for the ERC-20 allowance read. If omitted, the configured client for the swap chain is used. #### allowanceMode `"unlimited" | "exact" | undefined` Controls the approval amount when an approval call is needed: * `exact`: approve only `quote.inputAmount` * `unlimited`: approve the max `uint256` #### force `boolean | undefined` If `true`, include the approval call whenever the quote exposes approval metadata, without checking current allowance first. ### Returns `Promise` An ordered array of calls to execute: * `approval` if required * `swap` Each `BuiltCall` contains: * `type`: `"approval"` or `"swap"` * `txn`: the transaction payload to submit import ConfigParams from '../../../snippets/params/config.mdx' ## createConfig Create a configuration object to customize providers and options for all aggregation functions. ```ts import { createConfig, fabric, kyberswap, nordstern, odos, zeroX } from "@spandex/core"; export const config = createConfig({ providers: [ zeroX({ apiKey: "YOUR_0X_API_KEY" }), fabric({ appId: "YOUR_FABRIC_APP_ID" }), nordstern({}), odos({}), kyberswap({ clientId: "YOUR_KYBER_CLIENT_ID" }), ], options: { deadlineMs: 10000, numRetries: 2, initialRetryDelayMs: 200, integratorFeeAddress: "0xFee000", integratorSwapFeeBps: 25, }, clients: [ baseClient, mainnetClient ] as PublicClient[], }); ``` ### Proxy configuration Use a proxy when fetching quotes from a server (for example, to avoid browser CORS issues). For a full architecture walkthrough, see [Configuration > Proxy Mode](/configuration/proxy). If you want a managed proxy deployment, see [Configuration > Cloud Proxy](/configuration/cloud). When using `proxy(...)`, include at least one delegated action such as `prepareSimulatedQuotes`. ```ts import { createConfig, proxy } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); export const config = createConfig({ proxy: proxy({ pathOrUrl: "https://example.com/api", delegatedActions: ["prepareSimulatedQuotes"], }), clients: [client] as PublicClient[], }); ``` Or use the managed spanDEX cloud proxy: ```ts import { createConfig, spandexCloud } from "@spandex/core"; export const cloudConfig = createConfig({ proxy: spandexCloud({ apiKey: process.env.SPANDEX_CLOUD_KEY || 'demo' }), }); ``` ### Returns `Config` Aggregator configuration object to be passed to aggregation functions. ## decodeStream Decode a stream produced by `newStream` into an array of promises. ### Example ```ts import type { SimulatedQuote } from "@spandex/core"; import { decodeStream } from "@spandex/core"; const response = await fetch("/api/prepareSimulatedQuotes"); if (!response.body) { throw new Error("No response body"); } const quotePromises = await decodeStream(response.body); const quotes = await Promise.all(quotePromises); console.log(quotes.length); ``` ### Params #### stream `ReadableStream` Stream of serialized values produced by `newStream`. ### Returns `Promise>>` Promises that resolve to streamed values. ## executeQuote Execute a simulated quote onchain using a wallet client. Handles approval requirements and supports EIP-5792 batch calls when available. ### Example :::code-group ```ts [executeQuote] twoslash import { executeQuote, getQuote } from "@spandex/core"; import { config, publicClient, walletClient } from "./config.js"; const swap = { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quote = await getQuote({ config, swap, strategy: "bestPrice", }); if (!quote) { throw new Error("No providers returned a successful quote"); } const result = await executeQuote({ config, swap, quote, walletClient, publicClient, }); console.log(result.transactionHash); ``` ```ts [config] filename="config.ts" twoslash import { createConfig, fabric, odos } from "@spandex/core"; import { createPublicClient, createWalletClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const publicClient = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); const walletClient = createWalletClient({ account: "0xdead00000000000000000000000000000000beef", chain: base, transport: http(), }); export { publicClient, walletClient }; export const config = createConfig({ providers: [fabric({ appId: "your app id" }), odos({})], clients: [publicClient] as PublicClient[], }); ``` ::: ### Params #### swap `SwapParams` Swap parameters used to build the approval + swap transactions. #### quote `SimulatedQuote` The simulated quote to be executed. This should be obtained via [`getQuotes`](./getQuotes.mdx) or [`getQuote`](./getQuote.mdx). If the quote is not successful, `executeQuote` throws an `ExecutionError`. #### config `Config` Aggregator configuration object created via [`createConfig`](./createConfig.mdx). Used to resolve clients and options. #### walletClient `WalletClient` A Viem wallet client configured with the swapper's account and chain. This client is used to sign and send the transaction. #### publicClient `PublicClient | undefined` Optional public client for onchain reads (allowance checks, gas estimation, and receipt polling). If omitted, the configured client for the swap chain is used. ### Returns `Promise` A promise for an execution result containing the transaction hash. import { Callout } from 'vocs/components' ## getPricing **Experimental:** This utility is still considered experimental and may change. Extract pricing metadata from list of quotes to produce usd price estimates for input and output tokens. ### Example ```ts import { getPricing, getRawQuotes } from "@spandex/core"; import { config } from "./config.js"; const quotes = await getRawQuotes({ config, swap: { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }, }); const pricing = getPricing(quotes); console.log(pricing.sources); console.log(pricing.inputToken?.usdPrice, pricing.outputToken?.usdPrice); ``` ### Params #### quotes `Quote[]` Quotes to summarize. Failed quotes and quotes without pricing metadata are ignored. ### Returns `PricingSummary` Aggregated pricing summary including averaged USD prices and contributing providers. ## getQuote Fetch quotes, simulate them, and return a single winning quote based on a selection strategy. ### Example :::code-group ```ts [getQuote] twoslash import { getQuote } from "@spandex/core"; import { config } from "./config.js"; const quote = await getQuote({ config, swap: { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }, strategy: "bestPrice", }); if (!quote) { throw new Error("No providers returned a successful quote"); } console.log(quote.provider, quote.simulation.outputAmount); ``` ```ts [config] filename="config.ts" twoslash import { createConfig, fabric, odos } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); export const config = createConfig({ providers: [fabric({ appId: "your app id" }), odos({})], clients: [client] as PublicClient[], }); ``` ::: ### Params #### config `Config` Aggregator configuration object created via [`createConfig`](./createConfig.mdx). #### swap `SwapParams` Parameters defining the swap operation. See [SwapParams reference](/reference/SwapParams.mdx) for details. #### strategy `QuoteSelectionStrategy` Selection strategy used to pick a winning quote. | Strategy | Waits for All | Selection Criteria | Best For | | -------------- | ------------- | --------------------------------- | -------------------------------- | | `bestPrice` | ✓ | Highest simulated output | Price optimization | | `estimatedGas` | ✓ | Lowest simulated gas used | Gas efficiency | | `fastest` | ✗ | First success | Lowest latency | | `priority` | ✓ | Feature priority, then best price | Integrator-controlled preference | | Custom | Depends | Your logic | Special requirements | For tradeoffs and collector/ranker composition examples, see [Strategies](/core/strategies). Custom function signature: ```ts type QuoteSelectionFn = ( quotes: Array> ) => Promise ``` #### client `PublicClient | undefined` Optional custom PublicClient to use for onchain simulations. If not provided, the configured clients in the `config` will be used. #### simulate `typeof simulateQuote | undefined` Optional override for the simulation function used on each quote. ### Returns `Promise` Winning simulated quote, or `null` if no provider succeeds. ## getQuotes Fetches quotes from all configured providers and simulates the quote transactions to validate the swap operation and the final token output amount. ### Example :::code-group ```ts [getQuotes] twoslash import { getQuotes } from "@spandex/core"; import { config } from "./config.js"; const quotes = await getQuotes({ config, swap: { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", } }); for (const quote of quotes) { if (quote.success && quote.simulation.success) { console.log( `${quote.provider} yields ${quote.simulation.outputAmount}` ); } } /// @log: fabric yields 995000000n /// @log: odos yields 994500000n ``` ```ts [config] filename="config.ts" twoslash import { createConfig, fabric, odos } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); export const config = createConfig({ providers: [fabric({ appId: "your app id" }), odos({})], clients: [client] as PublicClient[], }); ``` ::: ### Params #### config `Config` Aggregator configuration object created via [`createConfig`](./createConfig.mdx). #### swap `SwapParams` Parameters defining the swap operation. See [SwapParams reference](/reference/SwapParams.mdx) for details. #### client `PublicClient | undefined` Optional custom PublicClient to use for onchain simulations. If not provided, the configured clients in the `config` will be used. ### Returns `Promise` List of simulated quotes from all providers. Each quote includes the provider name, raw quote data, and the simulated output amount after onchain validation. Quotes that fail simulation are denoted by `success=false` and include an error message. Simulation may also fail, due to a revert, etc. See `SimulatedQuote` for details. ## getRawQuotes Fetches quotes from all configured providers. This function does not perform any onchain simulation or validation. It is useful for scenarios where you plan to handle validation separately. Always simulate! ### Example :::code-group ```ts [getRawQuotes] twoslash import { getRawQuotes } from "@spandex/core"; import { config } from "./config.js"; const quotes = await getRawQuotes({ config, swap: { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", } }); ``` ```ts [config] filename="config.ts" twoslash import { createConfig, fabric, odos } from "@spandex/core"; export const config = createConfig({ providers: [fabric({ appId: "your app id" }), odos({})], }); ``` ::: ### Params #### config `Config` Aggregator configuration object created via [`createConfig`](./createConfig.mdx). #### swap `SwapParams` Parameters defining the swap operation. See [SwapParams reference](/reference/SwapParams.mdx) for details. ### Returns `Promise` List of quotes from all providers. Quotes that resulted in failure are denoted by `success=false` and include an error message. import { Callout } from 'vocs/components' ## netOutputs **Experimental:** This utility is still considered experimental and may be inaccurate for complex swaps. Computes net input/output token allocations per recipient based on Transfer logs. This is useful for spotting fee extractors (aggregator, app, relayer, etc.) by showing which accounts received how many tokens during a swap. Logs can come from a `TransactionReceipt` or `SuccessSimulationResult`. ### Example ```ts twoslash import { netOutputs, type SwapParams } from "@spandex/core"; import type { TransactionReceipt } from "viem"; const swap: SwapParams = {} as SwapParams; // ... const receipt: TransactionReceipt = {} as TransactionReceipt; // ... const allocations = netOutputs({ swap, logs: receipt.logs, }); for (const [account, amount] of allocations.outputToken) { console.log("output recipient", account, amount.toString()); } ``` ### Params #### swap `SwapParams` Swap parameters that identify the input/output tokens and swapper. This should match the swap that was used to build a quote, which was executed to produce logs. If `recipientAccount` is set on the swap, output attribution is evaluated against that recipient. #### logs `Log[]` Event logs from a transaction receipt or simulation output. ### Returns `Allocations` ```ts type Allocations = { inputToken: Map; outputToken: Map; }; ``` Each map is keyed by recipient address and contains the net amount of that token received by that address (base units). ## newStream Create a `ReadableStream` that emits serialized values as each promise resolves. ### Example ```ts import { newStream, prepareSimulatedQuotes, simulatedQuoteStreamErrorHandler, } from "@spandex/core"; export async function GET() { const promises = await prepareSimulatedQuotes({ config, swap, }); return new Response(newStream(promises, simulatedQuoteStreamErrorHandler), { headers: { "Content-Type": "application/octet-stream", }, }); } ``` ### Params #### promises `Array>` Promises to resolve and stream. #### onRejected `(error: unknown) => T` Handler used to normalize rejected promises into serializable values. ### Returns `ReadableStream` A stream of encoded value frames suitable for decoding with [`decodeStream`](./decodeStream.mdx). ## prepareQuotes Prepare a list of swap quote promise chains for quote fetching and secondary actions such as simulation. This is useful when custom behavior is required, such as racing quotes+simulation for multiple providers. ### Example :::code-group ```ts [getQuotes] import { prepareQuotes, type Quote } from "@spandex/core"; import { config } from "./config.ts"; // Identity fn async function mapFn(quote: Quote): Promise { return quote; } const promises = await prepareQuotes({ config, swap: { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", // WETH outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", // USDbC mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, // 1 WETH slippageBps: 50, // Tolerance swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }, mapFn, }); ``` ```ts [config.ts] filename="config.ts" twoslash import { createConfig, fabric, odos } from "@spandex/core"; import { createPublicClient, http, type PublicClient } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://base.drpc.org"), }); export const config = createConfig({ providers: [fabric({ appId: "your app id" }), odos({})], clients: [client] as PublicClient[], }); ``` ::: ### Params #### config `Config` Aggregator configuration object created via [`createConfig`](./createConfig.mdx). #### swap `SwapParams` Parameters defining the swap operation. See [SwapParams reference](/reference/SwapParams.mdx) for details. #### mapFn `(quote: Quote) => Promise` Function that maps each fetched quote to a promise of type `T`. This allows for custom processing of each quote, such as simulation or other enrichment. Identity function is also valid. ### Returns `Array>` List of promises, each resolving to type `T` as defined by the `mapFn`. ## selectQuote Pick the best simulated quote from a set of in-flight quote+simulation promises. This is useful when you want to stream quotes and simulations concurrently and only commit to the best result once enough data is available. ### Example :::code-group ```ts [selectQuote] import { prepareQuotes, selectQuote, simulateQuote } from "@spandex/core"; import { config, client, swap } from "./config.ts"; const quotes = await prepareQuotes({ config, swap, mapFn: (quote) => simulateQuote({ quote, swap, client }), }); const quote = await selectQuote({ quotes, strategy: "bestPrice", }); ``` ::: ### Params #### quotes `Array>` A list of promises each resolving to a simulated quote. These can be created via the [`prepareQuotes`](./prepareQuotes.mdx) function. #### strategy `QuoteSelectionStrategy` Strategy used to pick a winning quote. | Strategy | Waits for All | Selection Criteria | Best For | | -------------- | ------------- | --------------------------------- | -------------------------------- | | `bestPrice` | ✓ | Highest simulated output | Price optimization | | `estimatedGas` | ✓ | Lowest simulated gas used | Gas efficiency | | `fastest` | ✗ | First success | Speed/latency | | `priority` | ✓ | Feature priority, then best price | Integrator-controlled preference | | Custom | Depends | Your logic | Special requirements | For tradeoffs and collector/ranker composition examples, see [Strategies](/core/strategies). **Custom** ```ts type QuoteSelectionFn = ( quotes: Array> ) => Promise ``` ### Returns `Promise` A promise for a single simulated quote that offers the best outcome depending on the strategy. ## simulateQuote Simulate a single quote onchain and decorate it with simulation results. ### Example ```ts import { getRawQuotes, simulateQuote } from "@spandex/core"; import { config, client } from "./config.js"; const swap = { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quotes = await getRawQuotes({ config, swap, }); const first = quotes.find((quote) => quote.success); if (!first) { throw new Error("No successful quotes to simulate"); } const simulated = await simulateQuote({ quote: first, swap, client, }); if (simulated.success && simulated.simulation.success) { console.log(simulated.simulation.outputAmount); } ``` ### Params #### client `PublicClient` Client used to perform the simulation. #### swap `SwapParams` Swap parameters associated with the quote. See [SwapParams reference](/reference/SwapParams.mdx) for details. #### quote `Quote` Quote to simulate. ### Returns `Promise` The quote decorated with simulation results. Simulation failures are returned in the `simulation` field (for example `SimulationRevertError`). ## simulateQuotes Simulate a batch of quotes given shared swap parameters and a client. ### Example ```ts import { getRawQuotes, simulateQuotes } from "@spandex/core"; import { config, client } from "./config.js"; const swap = { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quotes = await getRawQuotes({ config, swap }); const simulated = await simulateQuotes({ client, swap, quotes, }); for (const quote of simulated) { if (quote.success && quote.simulation.success) { console.log(quote.provider, quote.simulation.outputAmount); } } ``` ### Params #### client `PublicClient` Client used to perform the simulations. #### swap `SwapParams` Swap parameters shared across all quotes. See [SwapParams reference](/reference/SwapParams.mdx) for details. #### quotes `Quote[]` Quotes to simulate. ### Returns `Promise` Quotes decorated with simulation results. Simulation failures are represented in the `simulation` field. ## sortQuotesByPerformance Sort successful simulated quotes by a selected performance metric. ### Example ```ts import { getRawQuotes, simulateQuotes, sortQuotesByPerformance, type SuccessfulSimulatedQuote, } from "@spandex/core"; import { config, client } from "./config.js"; const swap = { chainId: 8453, inputToken: "0x4200000000000000000000000000000000000006", outputToken: "0xd9AAEC86B65D86f6A7B5B1b0c42FFA531710b6CA", mode: "exactIn", inputAmount: 1_000_000_000_000_000_000n, slippageBps: 50, swapperAccount: "0x1234567890abcdef1234567890abcdef12345678", }; const quotes = await getRawQuotes({ config, swap }); const simulated = await simulateQuotes({ quotes, swap, client }); const successful = simulated.filter( (quote) => quote.success && quote.simulation.success, ) as SuccessfulSimulatedQuote[]; const sorted = sortQuotesByPerformance({ quotes: successful, metric: "accuracy", ascending: true, }); console.log(sorted[0]?.performance); ``` ### Params #### quotes `SuccessfulSimulatedQuote[]` Quotes to sort. Only successful simulated quotes include performance metrics. #### metric `keyof QuotePerformance` Metric to sort by. Supported metrics include `latency`, `gasUsed`, `outputAmount`, `priceDelta`, and `accuracy`. #### ascending `boolean | undefined` Sort order (default: `true`). When `false`, the list is sorted descending. ### Returns `SuccessfulSimulatedQuote[]` New array of quotes sorted by the selected performance metric. import { Callout } from "vocs/components"; ## spanDEX Cloud spanDEX cloud is a managed service which adheres to the same principles as proxy mode, but is hosted and maintained by the spanDEX team. It is intended for simple setup, testing, agents, and small deployments. It can easily be switched to and from proxy mode, so it's a great way to get started with spanDEX without needing to set up your own infrastructure. Cloud mode is currently experimental. If a chain you need isn't supported, or if you have any issues, please reach out to us on GitHub. ### Setup ```ts import { createConfig, spandexCloud } from "@spandex/core"; export const config = createConfig({ proxy: spandexCloud({ apiKey: process.env.SPANDEX_CLOUD_API_KEY || 'demo', }), }); ``` Note: The `demo` API key is a fake key, but still works. It is intended for testing and development. If you wish to use spanDEX cloud in production, let us know so we can set you up with a configured key. In React: ```tsx import { SpandexProvider } from "@spandex/react"; {children}; ``` ### How It Relates to Proxy Mode * Uses the same `AggregatorProxy` and wire protocol as [Proxy Mode](/configuration/proxy). * Runs on a managed deployment (`https://edge.spandex.sh/api/v1`) with API key auth. * Streams both raw quotes and simulated quotes from the managed service. If you need fully custom endpoints or custom infra, use `proxy({ pathOrUrl, delegatedActions: ["prepareSimulatedQuotes"] })` instead. import { Callout } from "vocs/components"; ## Fees There are several fee layers to consider in spanDEX, even though spanDEX itself does not impose fees: * **Provider fees**: costs imposed by the upstream provider and included in the quote. * **Integrator fee capture**: fee/surplus you choose to request because your product is sourcing order flow. This is activated by the provider on your behalf and included in the quote. * **LP fees**: fees paid to market makers (AMM, RFQ, etc) and included in the quote. * **Network fees**: gas costs, which scale with route complexity. They are not included in the quote output amount, but are estimated during simulation. ### Provider Fees These values represent default settings for each provider (new account or anonymous access) and are subject to negotiation. | Provider | Swap Fee | Surplus | More | | --------- | --------------------- | ------------- | ----------------------------------------------------------------------------------- | | Fabric | 0-0.1% | 0-100% | - | | 0x | 0.15% | 100% (1% Cap) | [Details](https://0x.org/pricing) | | KyberSwap | 0% | 100% | [Details](https://docs.kyberswap.com/kyberswap-solutions/fee-schedule) | | Odos | 0.03% | 100% | [Details](https://docs.odos.xyz/build/api_pricing) | | LI.FI | 0.25% | 100% | [Details](https://docs.li.fi/faqs/fees-monetization) | | Relay | 25% of Integrator Fee | 100% | [Details](https://docs.relay.link/references/api/api_core_concepts/fees) | | Velora | 15% of Integrator Fee | 100% (1% Cap) | [Details](https://docs.velora.xyz/intro-to-velora/velora-overview/protocol-revenue) | Additionally, each swap has network fees, which are not included in `outputTokens`. Gas estimates can still be factored in when selecting quotes. ### Integrator Fee Capture (Optional) spanDEX supports two integrator fee-capture controls for applications that source order flow from end users: * Fixed swap fee (`integratorSwapFeeBps`) * Surplus share (`integratorSurplusBps`) * Dynamic swap-time fee selection (`integratorFeeFn`) These requests are passed optimistically to providers. Each quote reports `activatedFeatures` so selection can prefer quotes that actually activated requested features. If you set `integratorSwapFeeBps` or `integratorSurplusBps`, you must also set `integratorFeeAddress`. ### Fixed Rate Swap Fees Use a fixed bps fee when you want predictable revenue on eligible swaps. ```ts import { createConfig, fabric, relay } from "@spandex/core"; const config = createConfig({ providers: [fabric({ appId: "YOUR_FABRIC_APP_ID" }), relay({})], options: { integratorFeeAddress: "0xFee00000000000000000000000000000000000fee", integratorSwapFeeBps: 25, // 0.25% }, }); ``` ### Surplus Capture Surplus can be requested with `integratorSurplusBps` for providers that support it. If `integratorSurplusAddress` is omitted, the fallback is typically `integratorFeeAddress`. This is not widely supported via API controls, but may be negotiated with providers. ```ts import { createConfig, fabric, velora } from "@spandex/core"; const config = createConfig({ providers: [fabric({ appId: "YOUR_FABRIC_APP_ID" }), velora({})], options: { integratorFeeAddress: "0xFee00000000000000000000000000000000000fee", integratorSwapFeeBps: 20, integratorSurplusBps: 1000, // 10% }, }); ``` ### Dynamic Fee Decisions Use `integratorFeeFn` when fee settings depend on the swap route, token pair, chain, user, or amount. The function runs once per swap request before provider quote requests are sent, and the returned settings are forwarded to providers using the same fee controls as static configuration. ```ts import { createConfig, zeroX } from "@spandex/core"; const baseTokenPreferenceStack = [ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", // USDT "0x4200000000000000000000000000000000000006", // WETH "0x0000000000000000000000000000000000000000", // ETH "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", // cbBTC ] as const; const config = createConfig({ providers: [zeroX({ apiKey: "YOUR_0X_API_KEY" })], options: { integratorFeeFn: async (swap) => { if (swap.chainId !== 8453) { return undefined; } const inputIndex = baseTokenPreferenceStack.findIndex( (token) => token.toLowerCase() === swap.inputToken.toLowerCase(), ); const outputIndex = baseTokenPreferenceStack.findIndex( (token) => token.toLowerCase() === swap.outputToken.toLowerCase(), ); const selectedToken = inputIndex >= 0 && (outputIndex < 0 || inputIndex <= outputIndex) ? baseTokenPreferenceStack[inputIndex] : baseTokenPreferenceStack[outputIndex]; if (!selectedToken) { return undefined; } return { feeAddress: "0xFee00000000000000000000000000000000000fee", swapFeeBps: 10, tokenPreference: selectedToken, }; }, }, }); ``` `integratorFeeFn` is mutually exclusive with static fee settings like `integratorFeeAddress` and `integratorSwapFeeBps`. Return `null` or `undefined` to request no integrator fee for a swap. ### Integrator Fee Support by Provider This table summarizes what a developer can control in the API today. Commercial enablement can still depend on provider plans/terms. | Provider | Swap Fee Control | Surplus Control | Negotiability | | --------- | ---------------- | --------------- | ------------- | | Fabric | ✓ | ✓ | ✓ | | 0x | ✓ | x | ✓ | | KyberSwap | ✓ | x | ✓ | | Odos | x | x | ✓ | | LI.FI | ✓ | x | ✓ | | Relay | ✓ | x | ✓ | | Velora | ✓ | ✓ | ✓ | Swap fee requests are configured with `integratorSwapFeeBps`, and surplus requests are configured with `integratorSurplusBps`. ### How Selection Uses Integrator Fee Requests * Requested fees are optimistic; providers without support can still return valid quotes. * `activatedFeatures` indicates which requested features were actually active. * Selection strategies (other than `fastest`) prioritize higher feature activation count before the secondary criterion (best price, lowest gas, etc). If you have negotiated features like surplus for a given provider, you can activate that feature for any provider using the `negotiatedFeatures` option. ## Configuration Use configuration to tune how your meta-aggregator behaves in production: which providers are active, how quote economics and optional integrator fee capture work, and what latency/retry behavior you want globally. ### What You Can Configure #### Provider Mix Choose one or more providers and define per-provider settings (keys, referral IDs, timeouts, negotiated features, and more). * [Provider strategy and comparison](/configuration/providers) * [Provider-specific setup docs](/providers/fabric) #### Fees and Integrator Capture Understand provider-imposed quote fees and configure optional integrator fee capture (swap fee and surplus) for supported providers. * [Fees and integrator fee capture](/configuration/fees) #### Runtime Controls Tune global aggregation behavior in `options`: * `deadlineMs` * `numRetries` * `initialRetryDelayMs` #### Execution Environment Set clients, logging, or proxy mode depending on your app architecture: * `clients` for simulation/validation support * `logging` for info/debug/trace * `proxy` when quote fetching runs server-side * [Proxy mode architecture and streaming](/configuration/proxy) * [Cloud proxy: managed proxy deployment](/configuration/cloud) ### API Reference * [ConfigParams reference](/reference/ConfigParams) * [createConfig function](/core/functions/createConfig) import { Callout } from "vocs/components"; import { Logo0x } from "../../components/logo/0x"; import { FabricLogo } from "../../components/logo/fabric"; import { KyberSwapLogo } from "../../components/logo/kyberswap"; import { LifiLogo } from "../../components/logo/lifi"; import { NordsternLogo } from "../../components/logo/nordstern"; import { OdosLogo } from "../../components/logo/odos"; import { RelayLogo } from "../../components/logo/relay"; import { VeloraLogo } from "../../components/logo/velora"; ## Providers Your provider set is the most important configuration decision in spanDEX. Think in terms of a portfolio: you want enough provider diversity to improve route quality, resilience, and throughput under real load. No single aggregator wins every trade or delivers 100% uptime. ### Supported Providers If you would like to include your aggregator in spanDEX, [submit a PR](https://github.com/withfabricxyz/spandex/blob/main/CONTRIBUTING.md). ### Why Use Multiple Providers * Better fill quality: different routers are stronger on different chains, pools, and token pairs. * Better uptime: provider outages, brownouts, or temporary API issues do not fully block quoting. * Better fee opportunities: providers differ in integrator fee/surplus support and commercial terms. * Better latency under load: requests can race in parallel and still respect a global deadline. ### What Differs Across Providers When choosing providers, compare these dimensions: * Functionality (`exactIn` only vs `exactIn` + `targetOut`) * Provider-imposed fees (surplus taking or swap fees) * Integrator fee and surplus support ([see fees page](/configuration/fees)) * Base API requirements (keys, partner IDs, referral codes) * Rate limits and plan capacity * Chain and token coverage ### Production Planning In production with real volume, configure multiple providers and choose provider plans that match your expected quote throughput. A single free-tier or low-rate plan is usually a bottleneck. ### Multi-Region and Edge Deployment If you run spanDEX as an edge service, you can use dynamic provider configuration by region to keep quote latency lower and improve reliability. If you have any regional latency data for providers, please contribute. ```ts import { createConfig, fabric, relay, velora } from "@spandex/core"; export function configForRegion(region: string) { if (region.startsWith("eu")) { return createConfig({ providers: [fabric({ appId: "..." }), relay({})], }); } return createConfig({ providers: [fabric({ appId: "..." }), velora({})], }); } ``` ## Proxy Mode Use proxy mode when your meta-aggregation layer runs as a service and clients fetch quotes from that service, instead of calling provider APIs directly from the client. ### When to Use It * You want provider API keys and provider-selection logic on the server. * You want a single policy point for rate limits, allowlists, and request validation. * You run multi-region services and want region-aware provider configuration. ### Client Configuration ```ts import { proxy } from "@spandex/core"; import { SpandexProvider } from "@spandex/react"; {children} ; ``` With `proxy` configured, you must declare at least one delegated action. Action names match both the function names and the path suffixes on your service. ### Delegation Model Proxy mode now uses two streamed server primitives: * `prepareQuotes` for raw quotes * `prepareSimulatedQuotes` for quotes that have already been simulated on the server Client APIs like `getQuotes` and `getQuote` stay local. They compose over streamed simulated quotes, which keeps custom selection logic available on the client while still moving provider access and simulation infrastructure to the server. ### How It Works 1. Client calls quote APIs/hooks (`getQuotes`, `getQuote`, `getRawQuotes`, `useQuotes`, etc). 2. `AggregatorProxy` serializes swap params into query params and calls your endpoint. 3. `prepareQuotes` uses a streaming response (`newStream(...)`) so quotes arrive progressively. 4. `prepareSimulatedQuotes` uses a streaming response (`newStream(...)`) so simulated quotes arrive progressively. 5. `getQuotes` collects the streamed simulated quotes locally, and `getQuote` selects locally. 6. For fast-return strategies like `fastest`, the client cancels the stream after selection. That abrupt close propagates to the remote fetch and can stop unnecessary work. Related APIs: * [`newStream`](/core/functions/newStream) * [`decodeStream`](/core/functions/decodeStream) ### Hono Service Example This pattern is implemented in `examples/hono/src/index.ts`. ```ts import { Hono } from "hono"; import { stream } from "hono/streaming"; import { zValidator } from "@hono/zod-validator"; import { newStream, prepareQuotes, prepareSimulatedQuotes, quoteStreamErrorHandler, simulatedQuoteStreamErrorHandler, type Quote, type SimulatedQuote, type SwapParams, } from "@spandex/core"; const app = new Hono(); app.get("/prepareQuotes", zValidator("query", querySchema), async (c) => { const swap = c.req.valid("query") satisfies SwapParams; return stream(c, async (streamWriter) => { const prepared = await prepareQuotes({ swap, config, mapFn: (quote: Quote) => Promise.resolve(quote), }); await streamWriter.pipe(newStream(prepared, quoteStreamErrorHandler)); }); }); app.get("/prepareSimulatedQuotes", zValidator("query", querySchema), async (c) => { const swap = c.req.valid("query") satisfies SwapParams; return stream(c, async (streamWriter) => { const prepared = await prepareSimulatedQuotes({ swap, config, }); await streamWriter.pipe( newStream(prepared, simulatedQuoteStreamErrorHandler), ); }); }); ``` ### React Streaming and Async Iterators `useQuotes` defaults to streamed results (`streamResults: true`) and uses TanStack `streamedQuery`. Internally, quote/simulation promises are exposed as an `AsyncIterable` so each result can be yielded as soon as it is ready. This is what enables progressive quote rendering instead of waiting for all providers to finish. ## Move along... nothing to see here!