# 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 `wagmi`.
:::code-group
```bash [npm]
npm i wagmi @spandex/react
```
```bash [bun]
bun i wagmi @spandex/react
```
```bash [pnpm]
pnpm i wagmi @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";
```
` 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). ## newQuoteStream Create a `ReadableStream` that emits serialized quotes as each promise resolves. This is useful for server-to-client streaming of quotes as they are fetched. ### Example ```ts import { newQuoteStream, prepareQuotes } from "@spandex/core"; import { config, swap } from "./config.js"; export async function GET() { const promises = await prepareQuotes({ config, swap, mapFn: async (quote) => quote, }); return new Response(newQuoteStream(promises), { headers: { "Content-Type": "application/octet-stream", }, }); } ``` ### Params #### promises `Array>` Quote promises to resolve and stream. ### Returns `ReadableStream ` Stream of serialized quote frames. ## 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` | ✗ | Provider order | Fallback chains | | Custom | Depends | Your logic | Special requirements | **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"; ## 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`) 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% }, }); ``` ### 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) ### 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 { 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 ProvidersIf 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({})], }); } ``` import { Callout } from "vocs/components"; ## 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, the client delegates quote fetching to your service endpoint. ### How It Works 1. Client calls quote APIs/hooks (`getQuotes`, `useQuotes`, etc). 2. `AggregatorProxy` serializes swap params into a query string and performs a GET to your endpoint. 3. Service fetches provider quotes and streams them immediately as each one resolves via `newQuoteStream(...)`. 4. Client decodes the stream with `decodeQuoteStream(...)` into quote promises. 5. Those promises resolve incrementally, so UI/state can update as results arrive. Related APIs: * [`newQuoteStream`](/core/functions/newQuoteStream) * [`decodeQuoteStream`](/core/functions/decodeQuoteStream) ### 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 { newQuoteStream, prepareQuotes, type Quote, type SwapParams } from "@spandex/core"; const app = new Hono(); app.get("/quotes/stream", 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(newQuoteStream(prepared)); }); }); ``` ### React Streaming and AsyncIterators `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. ### Simulation Placement Today Today, with the built-in proxy path, quote fetching is server-side but simulation still runs on the client (using the configured `PublicClient`).You can move simulation to the service layer with a custom endpoint/contract if you want full server-side execution, but the default proxy flow is quote streaming first, client simulation second. ## Move along... nothing to see here!