# Overview

The Open Source DEX Aggregator that runs on your hardware.

## What is Fynd?

Fynd is an open-source DEX aggregator that runs locally on your server. We built Fynd to be **reliable** and **trustless.**

Fynd gives you quotes in 20ms, supports 1.000 RPS on commodity hardware (see [performance](/reference/benchmark-results)), does not overquote, and is configurable to the pools, tokens, and objectives you care about (low reverts, best price, low latency, etc.).

Fynd builds on [Tycho](https://www.propellerheads.xyz/tycho) the open source DEX indexer (see the [protocols it supports](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols)).

## Own Your Routing <a href="#own-your-dex-routing" id="own-your-dex-routing"></a>

Route APIs are simple, but the tradeoffs are painful: rate limits, network overhead, no transparency, unreliable uptime, and unexplainable slippage. And you can't fix any of it.

Fynd puts you in control:

1. **Real-time market state** via Tycho Stream, covering all [Tycho-supported protocols](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols)
2. **As fast as 10ms per quote:** You choose the balance between routing quality and latency.
3. **Custom algorithms:** Plug in your own algorithm or customize the pre-built one. Fynd runs multiple algorithms in parallel and picks the best result.
4. **Execution on your terms:** Encode and execute swaps on-chain with full control over fees, slippage, and token transfer method.
5. **Vertical scaling:** Scale up to meet your speed requirements.

### Key Design Principles

* **Single source of truth**: All market data lives in one `MarketState` structure. A single feed writes to it; all workers read from it. No duplication.
* **Algorithm-agnostic**: Built around a pluggable `Algorithm` trait. Different algorithms use different graph representations and strategies. Multiple algorithms compete in parallel; the best result wins.
* **Performance-first**: CPU-bound route finding runs on dedicated OS threads (not the async runtime). Each worker pool has its own task queue for independent backpressure and scaling.
* **Observability built-in**: Prometheus metrics, structured logging via `tracing`, and health endpoints are first-class citizens.

### Order Types

Fynd currently supports **sell orders** only (exact input amount). You specify the amount of the input token, and Fynd finds the best output. Buy orders (exact output) are not yet supported.

### Supported Chains

* Ethereum Mainnet
* Base
* Unichain

*Coming soon*: Arbitrum, Polygon, and BSC.

### Supported Protocols

Fynd works with any protocol Tycho supports. See the [list of supported protocols](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols) and [supported RFQs](https://docs.propellerheads.xyz/tycho/for-solvers/request-for-quote-protocols#quickstart).

### How It Works

<figure><picture><source srcset="/files/jMcMvlDQk7efvQa0YfWc" media="(prefers-color-scheme: dark)"><img src="/files/c4zqIWjQOKEGKhA21q6J" alt="How It Works"></picture><figcaption></figcaption></figure>

1. **TychoFeed** connects to **Tycho Streams** ([on-chain protocols](https://docs.propellerheads.xyz/tycho/for-solvers/simulation#streaming-protocol-states) and [RFQs](https://docs.propellerheads.xyz/tycho/for-solvers/request-for-quote-protocols#stream-real-time-price-updates)) and processes market updates (added/removed components and state changes) every block.
2. **MarketState** stores all component states, tokens, and gas prices in a single shared structure.
3. When a **quote request** arrives via HTTP, the **WorkerPoolRouter** fans it out to all worker pools in parallel.
4. Each **Worker Pool** runs a specific algorithm. Workers compete to pick up the task, find routes through their local graph, simulate swaps against shared market state, and return ranked results.
5. The **WorkerPoolRouter** collects results from all pools, picks the best solution by `amount_out_net_gas`, optionally encodes it for execution against the `TychoRouter`, and returns it.

## Try it out

Head to the [quickstart](/get-started/quickstart) to get Fynd running.


# Quickstart

Swap via Fynd in minutes.

Integrate Fynd into your application in two steps.

{% hint style="info" %}
To interact with live quotes directly in your terminal, go to [swap CLI](/guides/swap-cli) instead.
{% endhint %}

## Prerequisites

* **Tycho API key** (set as `TYCHO_API_KEY`, [get one here](https://t.me/fynd_portal_bot))
* **Rust 1.92+** ([install via rustup](https://rustup.rs/)) or **Docker** ([install Docker](https://docs.docker.com/get-started/get-docker/))

## Step 0 — Start Fynd

{% tabs %}
{% tab title="cargo install" %}

```bash
cargo install fynd
export TYCHO_API_KEY=your-api-key
export RUST_LOG=fynd=info
fynd serve
```

{% endtab %}

{% tab title="Docker" %}
Images are available for linux/amd64 and linux/arm64.

```bash
docker run \
  -e TYCHO_API_KEY=your-api-key \
  -e RUST_LOG=fynd=info \
  -p 3000:3000 -p 9898:9898 \
  ghcr.io/propeller-heads/fynd serve
```

{% endtab %}

{% tab title="Build from source" %}

```bash
git clone https://github.com/propeller-heads/fynd.git
cd fynd
cargo install --locked --path .
export TYCHO_API_KEY=your-api-key
export RUST_LOG=fynd=info
fynd serve
```

{% endtab %}
{% endtabs %}

### Select a chain

Each Fynd instance serves a single chain. `fynd serve` defaults to **Ethereum**; pass `--chain` to target another one:

```bash
fynd serve --chain base
```

For Docker, append the flag after `serve`:

```bash
docker run \
  -e TYCHO_API_KEY=your-api-key \
  -e RUST_LOG=fynd=info \
  -p 3000:3000 -p 9898:9898 \
  ghcr.io/propeller-heads/fynd serve --chain base
```

These chains ship with built-in Tycho and RPC endpoints (names are case-insensitive): `ethereum`, `base`, `unichain`, `bsc`, `arbitrum`, `polygon`. For any other chain, also pass `--tycho-url` and `--rpc-url` explicitly.

Your client must target the same chain. If you use the TypeScript client (Step 1), set `chainId` and the viem chain to match the server:

```typescript
import { base } from 'viem/chains';

const publicClient = createPublicClient({ chain: base, transport: http(rpcUrl) });
const client = new FyndClient({ baseUrl: FYND_URL, chainId: base.id, /* ... */ });
```

## Step 1 — Execute a swap

{% hint style="info" %}
Fynd currently only supports **sell orders** (exact input). Set `"side": "sell"` in your order. Buy orders (exact output) are not yet supported.
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}
Full example: [`clients/typescript/examples/tutorial/main.ts`](https://github.com/propeller-heads/fynd/blob/main/clients/typescript/examples/tutorial/main.ts)

Install the [`@kayibal/fynd-client`](https://www.npmjs.com/package/@kayibal/fynd-client) package:

```bash
npm install @kayibal/fynd-client
```

```typescript
const client = new FyndClient({
  baseUrl: FYND_URL,
  sender: account.address,
  provider: viemProvider(publicClient, account.address),
  fetchRevertReason: true,
});

// 1. Quote
const quote = await client.quote({
  order: { tokenIn: WETH, tokenOut: USDC, amount: SELL_AMOUNT, side: 'sell', sender: account.address },
  options: { encodingOptions: encodingOptions(0.005) },
});
console.log(`amount_out: ${quote.amountOut}`);

// 2. Approve if needed (checks on-chain allowance, skips if sufficient)
const approvalPayload = await client.approval({ token: WETH, amount: SELL_AMOUNT, checkAllowance: true });
if (approvalPayload !== null) {
  const sig = await account.sign({ hash: approvalSigningHash(approvalPayload) });
  await client.executeApproval({ tx: approvalPayload.tx, signature: sig });
}

// 3. Sign and execute swap
const payload = await client.swapPayload(quote);
const sig = await account.sign({ hash: swapSigningHash(payload) });
const settled = await (await client.executeSwap(assembleSignedSwap(payload, sig))).settle();
console.log(`settled: ${settled.settledAmount}, gas: ${settled.gasCost}`);
```

{% endtab %}

{% tab title="Rust" %}
Full example: [`clients/rust/examples/swap_erc20.rs`](https://github.com/propeller-heads/fynd/blob/main/clients/rust/examples/swap_erc20.rs)

Add the [`fynd-client`](https://crates.io/crates/fynd-client) crate to your `Cargo.toml`:

```toml
cargo add fynd-client
```

```rust
let client = FyndClientBuilder::new(FYND_URL)
    .with_rpc_url(RPC_URL)
    .with_sender(sender)
    .build()
    .await?;

// 1. Quote
let quote = client
    .quote(QuoteParams::new(
        Order::new(
            Bytes::copy_from_slice(sell_token.as_slice()),
            Bytes::copy_from_slice(buy_token.as_slice()),
            BigUint::from(SELL_AMOUNT),
            OrderSide::Sell,
            Bytes::copy_from_slice(sender.as_slice()),
            None,
        ),
        QuoteOptions::default()
            .with_timeout_ms(5_000)
            .with_encoding_options(EncodingOptions::new(SLIPPAGE)),
    ))
    .await?;
println!("amount_out: {}", quote.amount_out());

// 2. Approve if needed (checks on-chain allowance, skips if sufficient)
if let Some(approval_payload) = client
    .approval(
        &ApprovalParams::new(
            Bytes::copy_from_slice(sell_token.as_slice()),
            BigUint::from(SELL_AMOUNT),
            true,
        ),
        &SigningHints::default(),
    )
    .await?
{
    let sig = signer.sign_hash(&approval_payload.signing_hash()).await?;
    client
        .execute_approval(SignedApproval::assemble(approval_payload, sig))
        .await?
        .await?;
}

// 3. Sign and execute swap
let payload = client.swap_payload(quote, &SigningHints::default()).await?;
let sig = signer.sign_hash(&payload.signing_hash()).await?;
let receipt = client
    .execute_swap(SignedSwap::assemble(payload, sig), &ExecutionOptions::default())
    .await?
    .await?;
println!("gas: {}", receipt.gas_cost());
```

{% endtab %}

{% tab title="curl" %}

```bash
# Wait until healthy
curl http://localhost:3000/v1/health
# → {"healthy":true,...}

# Request a quote — 1000 USDC → WETH
curl -X POST http://localhost:3000/v1/quote \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "token_in":  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "token_out": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
        "amount":    "1000000000",
        "side":      "sell",
        "sender":    "0x0000000000000000000000000000000000000001"
      }
    ],
    "options": {
      "timeout_ms": 5000,
      "min_responses": 1
    }
  }'
```

This quotes 1000 USDC (6 decimals → 1 000 000 000 atomic units) for WETH.
{% endtab %}
{% endtabs %}

## Native ETH swaps

To swap native ETH, use `0x0000...0000` as the `token_in` or `token_out` address in your order.

## Next steps

* [Encoding options](/guides/encoding-options) - encode quotes into ready-to-submit transactions
* [Fynd Fees](/guides/router-fees) - understand Fynd fees on executed swaps
* [Charge Fees on your Swaps](/guides/client-fees) - charge integrator fees on swaps
* [Server configuration](/guides/server-configuration)
* [Custom algorithm](/guides/custom-algorithm)
* [Benchmarking](/guides/benchmarking)
* [Swap CLI](/guides/swap-cli)


# Hosted Fynd API (Beta)

Get routing quotes in minutes from Propeller Heads' hosted Fynd API — no local setup required.

{% hint style="warning" %}
**Beta.** The hosted Fynd API is in beta. Limits, supported chains, and the surface area may change as we scale the service. There is no SLA during beta — status and incidents are posted in [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl). For production workloads with strict uptime or throughput requirements, see [Scaling beyond the hosted tiers](#scaling-beyond-the-hosted-tiers).
{% endhint %}

Fynd is an [open-source](https://github.com/propeller-heads/fynd) DEX aggregator: you send it a token pair and amount, it finds the best on-chain route across the liquidity venues it tracks, and returns the expected output plus a ready-to-submit transaction. You can run Fynd on your own hardware (see [Self-host quickstart](/get-started/quickstart)) — **or you can use our hosted API and skip the setup entirely.** This page covers the hosted path: get an API key, send a request, get a route.

The hosted API runs the **latest released Fynd** with **routing settings tuned per chain by the Fynd team**, so you always track the newest routing improvements without managing releases or worker pools yourself.

{% hint style="info" %}
**Prefer a UI?** A hosted web frontend is coming soon. Today the API is the integration surface.
{% endhint %}

## What you get

* **One endpoint, all chains.** `https://fynd-api.propellerheads.xyz` routes `/v1/{chain}/…` to a per-chain Fynd backend. No per-chain infrastructure on your side.
* **Latest Fynd version.** The hosted backends track the newest Fynd release; you don't pin or upgrade anything. (During beta, the surface area may change — see the [FAQ](#faq) for version and deprecation notes.)
* **Optimized routing.** Worker-pool and algorithm configuration are tuned per chain by the Fynd team. Hosted backends run the full default protocol set — see the [list of supported protocols](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols).
* **Same quote API as self-hosted.** The per-chain request/response bodies are identical to running `fynd serve` yourself — see [API reference](/reference/api). The hosted gateway adds a `/{chain}` path segment and API-key auth; a self-hosted instance serves one chain at `/v1/…` with no auth.

## Get an API key

1. Open [@fynd\_portal\_bot](https://t.me/fynd_portal_bot) on Telegram.
2. Run `/start` and follow the prompts. You'll receive a **Fynd API key** (also valid for the [Tycho](https://docs.propellerheads.xyz/tycho) liquidity indexer that feeds Fynd).
3. Save the key immediately. The bot won't show it again, and you get **3 self-service revocations** (rotations of your key) before you need to contact support in [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl).

```bash
export FYND_API_KEY=your-api-key
```

{% hint style="warning" %}
**Keep the key server-side.** The `Authorization` header is a raw secret. Never ship it in browser/client code. Proxy Fynd requests through your backend; do not call the hosted API directly from a frontend. (A hosted web frontend is coming soon — until then, all API access is server-to-server.)
{% endhint %}

The key is sent as the **raw `Authorization` header value** on every request — **no `Bearer` prefix**:

```bash
curl -H "Authorization: $FYND_API_KEY" https://fynd-api.propellerheads.xyz/v1/ethereum/health
```

## Quickstart

### 1. Health check

```bash
curl -i -H "Authorization: $FYND_API_KEY" \
  https://fynd-api.propellerheads.xyz/v1/ethereum/health
```

```json
{
  "healthy": true,
  "last_update_ms": 4000,
  "num_solver_pools": 2,
  "derived_data_ready": true,
  "gas_price_age_ms": 16263
}
```

The `-i` flag also prints the response headers, including `X-User-Plan: fynd-basic` (your current plan) and `Retry-After` on 429s.

**Health fields:**

| Field                | Meaning                                                                                                                                                                                        |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `healthy`            | Overall solver readiness.                                                                                                                                                                      |
| `last_update_ms`     | Milliseconds since the last market-state update from Tycho. `0` means **no update received yet** (the stream is stalled — quotes will return `no_route_found`); non-zero and small is healthy. |
| `num_solver_pools`   | Number of parallel algorithm worker pools running. Not liquidity pools — internal solver workers.                                                                                              |
| `derived_data_ready` | Whether derived graph data (token pairs, routes) is built and ready to serve.                                                                                                                  |
| `gas_price_age_ms`   | Age of the cached gas price estimate, in milliseconds.                                                                                                                                         |

### 2. Instance info

```bash
curl -H "Authorization: $FYND_API_KEY" \
  https://fynd-api.propellerheads.xyz/v1/ethereum/info
```

```json
{
  "chain_id": 1,
  "router_address": "0xda892c989d07a18b5dd3f392d949f00df15c5736",
  "permit2_address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
  "version": "0.97.0"
}
```

* `chain_id` — the EVM chain ID this backend serves (e.g. `1` for Ethereum). Corresponds to the `chain` path segment in the request URL.
* `router_address` — the on-chain contract that executes swaps. Submit your encoded swap transaction to this address (it's already set as `transaction.to` in an encoded quote — you don't set it yourself). It is **chain-specific**: each chain returns its own router address.
* `permit2_address` — the [Permit2](https://uniswap.org/blog/permit2) contract address. Used for permit-based token approvals (see [Approvals](#approvals) below).
* `version` — the Fynd binary version serving this chain (the example value is illustrative). Hosted backends are updated on each production release, so the running version may lag the newest published Fynd release.

### 3. Request a quote

Sell 1 WETH for USDC on Ethereum. Amounts are in the token's **smallest unit** (wei for WETH, micro-units for USDC):

```bash
curl -X POST https://fynd-api.propellerheads.xyz/v1/ethereum/quote \
  -H "Authorization: $FYND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "token_in":  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
        "token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount":    "1000000000000000000",
        "side":      "sell",
        "sender":    "0x0000000000000000000000000000000000000001"
      }
    ],
    "options": {
      "timeout_ms": 5000,
      "min_responses": 1
    }
  }'
```

{% hint style="info" %}
**Sell orders only.** Fynd currently supports `side: "sell"` (exact input). Buy orders (exact output) are not yet supported.
{% endhint %}

**`sender`** — the address that will submit the swap transaction. The quote (and any encoded calldata) is bound to this sender. The `0x0000…0001` above is a dummy; **replace it with your wallet address** for any quote you intend to execute.

**Quote options** (`options` field — all optional):

| Option             | Default        | Meaning                                                                                                                                                                                                                  |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `timeout_ms`       | server default | Max time the solver spends finding a route. On timeout, the server returns the best route found so far (or a `timeout` status if none).                                                                                  |
| `min_responses`    | server default | Minimum number of solver pools that must respond before the server returns. `1` returns as soon as one pool has a route (fastest). Higher values wait for more pools to compete, improving price at the cost of latency. |
| `encoding_options` | `null`         | If set, the response includes a ready-to-submit `transaction` object. See [step 4](#4-encode-and-approve).                                                                                                               |
| `max_gas`          | `null`         | Cap on gas units the route may use. Routes exceeding it are rejected.                                                                                                                                                    |

The response contains the route, the expected `amount_out`, gas estimate, and the `solve_time_ms` the server spent finding the route:

```json
{
  "orders": [
    {
      "order_id": "4092e006-67b4-4bcc-b7fd-baacd6f7259d",
      "status": "success",
      "route": {
        "swaps": [
          {
            "component_id": "0x7c85004568584fbf3665f41ebe85146ee0483587d65d9ea5a56c79816bb720d0",
            "protocol": "vm:fermiswap",
            "token_in":  "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
            "token_out": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
            "amount_in":  "1000000000000000000",
            "amount_out": "1842438485",
            "gas_estimate": "141239",
            "split": "0"
          }
        ]
      },
      "amount_in":  "1000000000000000000",
      "amount_out": "1842438485",
      "amount_out_net_gas": "1842418197",
      "gas_estimate": "141239",
      "price_impact_bps": 0,
      "block": { "number": 25560140, "hash": "0x7144c8a53f70cf4875864b085055423493671536f494826c7bc62cdd60171013", "timestamp": 1784384351 },
      "gas_price": "78012237",
      "transaction": null
    }
  ],
  "total_gas_estimate": "141239",
  "solve_time_ms": 26
}
```

**Response fields:**

| Field                        | Meaning                                                                                                                                                                          |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `solve_time_ms`              | Server-side time spent finding the route (excludes network/proxy overhead). Top-level, per response.                                                                             |
| `total_gas_estimate`         | Sum of `gas_estimate` across all orders in the response. Top-level, per response.                                                                                                |
| `amount_in`                  | The input amount for this order, in `token_in`'s smallest unit (echoes the requested `amount`).                                                                                  |
| `amount_out`                 | Expected output, in the `token_out`'s smallest unit.                                                                                                                             |
| `amount_out_net_gas`         | `amount_out` minus the gas cost of executing the route, denominated in `token_out`. Use this to compare routes.                                                                  |
| `gas_estimate`               | Gas units the swap will consume.                                                                                                                                                 |
| `gas_price`                  | Gas price (wei) used for the `amount_out_net_gas` calculation.                                                                                                                   |
| `price_impact_bps`           | Price impact of the swap in basis points (100 bps = 1%).                                                                                                                         |
| `route.swaps[].component_id` | Internal ID for the liquidity venue used (pool/curve/vault). Not directly an on-chain address.                                                                                   |
| `route.swaps[].protocol`     | Protocol name. The `vm:` prefix denotes a virtual-machine venue. See [supported protocols](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols).               |
| `route.swaps[].split`        | Fraction of the input amount routed through this swap. `"0"` means "the remainder of the input" (the last leg of a split group), not 0%. A single-swap route shows `split: "0"`. |
| `block`                      | The block the quote is valid against. Submit the encoded transaction promptly — see [Quote validity](#quote-validity).                                                           |
| `transaction`                | `null` unless `encoding_options` is set. See step 4.                                                                                                                             |
| `order_id`                   | Correlation ID for this quote. Not queryable after the fact.                                                                                                                     |

One order per request is the supported path today. The `orders` array shape exists for future batch quoting; for now, send exactly one order.

### 4. Encode and approve

To get a ready-to-submit transaction, pass `encoding_options` with your slippage tolerance (a fraction, as a **string**, e.g. `"0.005"` = 0.5%):

```bash
curl -X POST https://fynd-api.propellerheads.xyz/v1/ethereum/quote \
  -H "Authorization: $FYND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "token_in":  "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
        "token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount":    "1000000000000000000",
        "side":      "sell",
        "sender":    "0xYourWalletAddress"
      }
    ],
    "options": {
      "timeout_ms": 5000,
      "min_responses": 1,
      "encoding_options": { "slippage": "0.005" }
    }
  }'
```

{% hint style="warning" %}
**Replace `sender` with your wallet address.** The encoded calldata is bound to the `sender` you provide — a transaction encoded for `0xYourWalletAddress` will revert if submitted by any other address.
{% endhint %}

The response now includes a populated `transaction` and a `fee_breakdown`:

```json
{
  "orders": [
    {
      "status": "success",
      "amount_in":  "1000000000000000000",
      "amount_out": "1844002345",
      "amount_out_net_gas": "1843958233",
      "gas_estimate": "219652",
      "price_impact_bps": 3,
      "block": { "number": 25560394, "hash": "0x17aeb9d6c8d38c46e0e9d6871fcd4bfe7cf6e6cb4fb79eca2fbfe39a0f1d9837", "timestamp": 1784387423 },
      "gas_price": "108908625",
      "route": { "swaps": [ /* ... */ ] },
      "transaction": {
        "to":   "0xda892c989d07a18b5dd3f392d949f00df15c5736",
        "value": "0",
        "data": "0xce25e49e..."
      },
      "fee_breakdown": {
        "router_fee": "18440",
        "client_fee": "0",
        "max_slippage": "9219919",
        "min_amount_received": "1834763986"
      }
    }
  ],
  "total_gas_estimate": "219652",
  "solve_time_ms": 25
}
```

* `transaction.to` is the chain's `router_address` (already set — submit as-is).
* `transaction.data` is the calldata for the swap.
* `transaction.gas` may or may not be populated — if absent, estimate gas separately with `eth_estimateGas` before submitting.
* `fee_breakdown.router_fee` — the fee Fynd charges on the swap, in `token_out` units. The default Fynd fee is **0.1 bps (0.001%)** of swap output; quotes are free. See [Fynd Fees](/guides/router-fees) for volume discounts and the full fee arithmetic.
* `fee_breakdown.client_fee` — integrator fee (0 unless you set `client_fee_params` in `encoding_options`; see [Charge Fees on your Swaps](/guides/client-fees)).
* `fee_breakdown.max_slippage` — the slippage allowance in `token_out` units, applied to the post-fee amount. `min_amount_received` = `amount_out − router_fee − client_fee − max_slippage`. Verify the settled output is ≥ `min_amount_received` after the transaction confirms.

#### Approvals

Before submitting the swap transaction, the `token_in` must be spendable by the router. The path depends on the `transfer_type` (default `"transfer_from"`):

1. **`transfer_from` (default) — standard ERC-20 approval.** Call `approve(router_address, amount)` on the `token_in` contract, granting the Fynd router an allowance ≥ `amount_in`. The router pulls the exact `amount_in` at execution.
2. **`transfer_from_permit2` — gasless Permit2 signature.** Sign a `PermitSingle` off-chain and pass it via `encoding_options.permit` + `permit2_signature`. No on-chain `approve()` to the router is needed, **but the token must first be approved to the Permit2 contract** (`permit2_address`), not the router. See [Encoding Options](/guides/encoding-options) for the full Permit2 flow.

**Native-token sells skip approval entirely.** If `token_in` is the zero address (`0x0000000000000000000000000000000000000000`), the router wraps native ETH/BNB/POL for you — no approval needed.

Quick approval with `cast` (Foundry) — for the default `transfer_from` path:

```bash
# Approve the ROUTER to spend WETH for the swap (default transfer_from path)
cast send 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
  "approve(address,uint256)" \
  0xda892c989d07a18b5dd3f392d949f00df15c5736 \
  1000000000000000000 \
  --rpc-url $RPC_URL --private-key $PRIVATE_KEY
```

With approval in place, sign and broadcast `transaction` (the `{to, value, data}` object) from your `sender` wallet. If `transaction.gas` is absent, estimate with `eth_estimateGas` first.

#### Quote validity

The quote is valid against the `block` in the response. Encoded calldata includes a deadline, but market state moves every block — **re-quote within \~1 block (12s on Ethereum, faster on L2s) before submitting.** For execution-critical flows, quote → sign → submit in a single sequence; don't cache quotes for later.

### 5. Sign and execute with a Fynd client

For the full approve → sign → submit → settle flow, the Fynd clients (`@kayibal/fynd-client` for TypeScript, `fynd-client` for Rust) support the hosted API directly — pass your API key and chain to the client builder and the rest is handled.

{% hint style="info" %}
The typed clients expose **camelCase** equivalents of the REST fields (`amountOut`, `solveTimeMs`, `amountOutNetGas`, etc.). The REST reference uses snake\_case (`amount_out`, `solve_time_ms`).
{% endhint %}

{% tabs %}
{% tab title="TypeScript" %}

```bash
npm install @kayibal/fynd-client viem
```

```typescript
import {
  FyndClient,
  approvalSigningHash,
  swapSigningHash,
  assembleSignedSwap,
  viemProvider,
} from '@kayibal/fynd-client';
import { createPublicClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

const WETH  = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' as const;
const USDC  = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as const;
const SELL_AMOUNT = 1000000000000000000n;
const RPC_URL = 'https://eth.llamarpc.com'; // use a dedicated RPC in production

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });

const client = new FyndClient({
  baseUrl: 'https://fynd-api.propellerheads.xyz',
  apiKey: process.env.FYND_API_KEY!, // sent raw as the Authorization header
  chain: 'ethereum',                 // routes to /v1/ethereum/*
  sender: account.address,
  provider: viemProvider(publicClient, account.address),
  fetchRevertReason: true,
});

// 1. Quote with encoding
const quote = await client.quote({
  order: { tokenIn: WETH, tokenOut: USDC, amount: SELL_AMOUNT, side: 'sell', sender: account.address },
  options: { encodingOptions: { slippage: 0.005 } },
});

// 2. Approve if needed (checks on-chain allowance, skips if sufficient)
const approvalPayload = await client.approval({ token: WETH, amount: SELL_AMOUNT, checkAllowance: true });
if (approvalPayload !== null) {
  const sig = await account.sign({ hash: approvalSigningHash(approvalPayload) });
  await client.executeApproval({ tx: approvalPayload.tx, signature: sig });
}

// 3. Sign and execute swap
const payload = await client.swapPayload(quote);
const sig = await account.sign({ hash: swapSigningHash(payload) });
const settled = await (await client.executeSwap(assembleSignedSwap(payload, sig))).settle();
console.log(`settled: ${settled.settledAmount}, gas: ${settled.gasCost}`);
```

Full example: [`clients/typescript/examples/tutorial/main.ts`](https://github.com/propeller-heads/fynd/blob/main/clients/typescript/examples/tutorial/main.ts).
{% endtab %}

{% tab title="Rust" %}

```toml
# Cargo.toml
[dependencies]
# Pin to any recent release; check crates.io for the latest: https://crates.io/crates/fynd-client
fynd-client = "0.97"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
alloy = { version = "0.3", features = ["full"] }
num-bigint = "0.4"
```

```rust
use fynd_client::{
    FyndClientBuilder, QuoteParams, Order, OrderSide, QuoteOptions, EncodingOptions,
    ApprovalParams, SigningHints, SignedApproval, SignedSwap, ExecutionOptions,
};
use alloy::{
    primitives::{Address, Bytes},
    signers::local::PrivateKeySigner,
    signers::Signer,
};
use num_bigint::BigUint;
use std::str::FromStr;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let token_in:  Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse()?;
    let token_out: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse()?;
    let sell_amount = BigUint::from(1_000_000_000_000_000_000u64);
    let rpc_url = std::env::var("RPC_URL")?;
    let sender: Address = "0xYourWalletAddress".parse()?;
    let signer = PrivateKeySigner::from_str(&std::env::var("PRIVATE_KEY")?)?;

    let client = FyndClientBuilder::new("https://fynd-api.propellerheads.xyz")
        .with_api_key(std::env::var("FYND_API_KEY")?) // sent raw, no Bearer prefix
        .with_chain("ethereum")                       // routes to /v1/ethereum/*
        .with_rpc_url(&rpc_url)
        .with_sender(sender)
        .build()
        .await?;

    // 1. Quote with encoding
    let quote = client
        .quote(QuoteParams::new(
            Order::new(
                Bytes::copy_from_slice(token_in.as_slice()),
                Bytes::copy_from_slice(token_out.as_slice()),
                sell_amount.clone(),
                OrderSide::Sell,
                sender,
                None, // receiver: None means same as sender
            ),
            QuoteOptions::default()
                .with_timeout_ms(5_000)
                .with_encoding_options(EncodingOptions::new(0.005)),
        ))
        .await?;

    // 2. Approve if needed (checks on-chain allowance, skips if sufficient)
    if let Some(approval_payload) = client
        .approval(
            &ApprovalParams::new(
                Bytes::copy_from_slice(token_in.as_slice()),
                sell_amount,
                true, // check allowance first
            ),
            &SigningHints::default(),
        )
        .await?
    {
        let sig = signer.sign_hash(&approval_payload.signing_hash()).await?;
        client.execute_approval(SignedApproval::assemble(approval_payload, sig)).await?.await?;
    }

    // 3. Sign and execute swap
    let payload = client.swap_payload(quote, &SigningHints::default()).await?;
    let sig = signer.sign_hash(&payload.signing_hash()).await?;
    let receipt = client
        .execute_swap(SignedSwap::assemble(payload, sig), &ExecutionOptions::default())
        .await?
        .await?;
    println!("gas: {}", receipt.gas_cost());
    Ok(())
}
```

Full example: [`clients/rust/examples/swap_erc20.rs`](https://github.com/propeller-heads/fynd/blob/main/clients/rust/examples/swap_erc20.rs).
{% endtab %}
{% endtabs %}

## Supported chains

Each chain is served from its own Fynd backend behind the shared gateway. Send the chain name in the URL path (`/v1/{chain}/…`). With the typed clients, set the `chain` option (TypeScript) or `.with_chain("…")` (Rust).

| Chain           | `chain` path segment | Chain ID | Native token |
| --------------- | -------------------- | -------: | ------------ |
| Ethereum        | `ethereum`           |        1 | ETH          |
| Base            | `base`               |     8453 | ETH          |
| Arbitrum        | `arbitrum`           |    42161 | ETH          |
| BNB Smart Chain | `bsc`                |       56 | BNB          |
| Polygon         | `polygon`            |      137 | POL          |
| Unichain        | `unichain`           |      130 | ETH          |

A request to `/v1/{chain}/…` for a chain that isn't configured returns `404` with `{"error": "unknown_chain", ...}`.

### Native token swaps

Use the zero address (`0x0000000000000000000000000000000000000000`) as `token_in` or `token_out` to swap the chain's native gas token (ETH on Ethereum/Base/Arbitrum/Unichain, BNB on BSC, POL on Polygon). Native-token `token_in` skips the approval step — the router wraps native gas for you.

## API key limits by tier

Your API key belongs to a **plan** that sets your rate limit. Limits are enforced per key by a token-bucket limiter at the gateway.

| Plan                     | Requests / second |   Burst | How to get                                                                                            |
| ------------------------ | ----------------: | ------: | ----------------------------------------------------------------------------------------------------- |
| **fynd-basic** (default) |                10 | 2× (20) | Self-service via [@fynd\_portal\_bot](https://t.me/fynd_portal_bot)                                   |
| **basic** (tycho)        |                10 | 2× (20) | Request via [@tanay\_j](https://t.me/tanay_j) or [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl) |
| **scale**                |                25 | 2× (50) | Request via [@tanay\_j](https://t.me/tanay_j) or [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl) |

{% hint style="info" %}
**Burst** is the bucket capacity = `rps × burst_multiplier`. A `fynd-basic` key can briefly sustain 20 requests in one second before being throttled back to the steady-state 10 rps. **All requests count against the bucket** — `/health`, `/info`, and `/quote` alike. The bucket is **per key, shared across all chains**.
{% endhint %}

When you exceed the limit you get `429 Too Many Requests` with a `Retry-After` header (seconds). The hosted API is **REST-only** today (no WebSocket); for higher throughput, run your own stack (see below) or talk to us about a dedicated instance.

**Pricing:** The hosted API is **free to use during beta** — no platform fee on top of Fynd's standard 0.1 bps router fee on executed swaps (see [Fynd Fees](/guides/router-fees)). Quotes are always free. `scale` is allocated case-by-case — reach out to discuss volume and needs.

### Checking your plan

The gateway returns your plan in the `X-User-Plan` response header on every authenticated request (use `curl -i` to see it). If you're unsure which plan your key is on, message [@fynd\_portal\_bot](https://t.me/fynd_portal_bot) or ask in [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl).

## Errors

| HTTP status | Meaning                                                               | Body format                                                           |
| ----------: | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
|         400 | Malformed request body, bad token address, or invalid `slippage` type | JSON: `{"error": "...", "code": "BAD_REQUEST"}`                       |
|         401 | Missing or invalid API key, or plan not recognized                    | Plain text: `Unauthorized` or `Missing authorization token`           |
|         403 | Your plan doesn't allow this service                                  | Plain text: `Forbidden`                                               |
|         404 | Unknown chain path segment                                            | JSON: `{"error": "unknown_chain", ...}`                               |
|         429 | Rate limit exceeded                                                   | Plain text: `Too Many Requests`, plus `Retry-After: <seconds>` header |
|         5xx | Backend or gateway failure                                            | varies — check health and retry with backoff                          |

{% hint style="warning" %}
**Error body formats differ by layer.** Gateway errors (401, 403, 429) return plain text; backend errors (400, 404, 5xx) return JSON. If your client assumes JSON on every non-2xx, it will throw a parse error on the auth and rate-limit paths you most need to handle. Parse defensively.
{% endhint %}

A `200` with `orders[0].status: "no_route_found"` is **not** an HTTP error — it means the solver ran but couldn't find a profitable route for the pair at the requested size. Check `/v1/{chain}/health` (`last_update_ms: 0` means the Tycho stream isn't delivering live state yet), try a different token pair or size, or confirm the tokens have [Tycho-indexed liquidity](https://docs.propellerheads.xyz/tycho) on that chain.

## Scaling beyond the hosted tiers

The hosted API is a shared, beta-rate-limited service designed for getting started and for moderate-volume integrations. If you need:

* **Higher or unlimited rate limits** — run Fynd on your own hardware. The [self-host quickstart](/get-started/quickstart) has you serving in minutes, and there's no rate limiter in front of your own instance.
* **Custom algorithms, custom protocol sets, or RFQ integration** — self-host and pass `--protocols` / your algorithm config. See [Server Configuration](/guides/server-configuration) and [Custom Algorithm](/guides/custom-algorithm).
* **A dedicated hosted instance with guaranteed capacity and SLA** — reach out to our business team. Message [@tanay\_j](https://t.me/tanay_j) on Telegram, or email us at [Propeller Heads](https://www.propellerheads.xyz).

What the hosted API buys you (vs. self-hosting): no infrastructure to run, no Tycho indexer endpoint to source, no release tracking, and per-chain routing tuned by the Fynd team. Self-hosting gives you \~1,000 RPS on commodity hardware (see [Performance](/reference/benchmark-results)) and full control — pick the tradeoff that fits your scale.

## FAQ

**Is the hosted API the same software as self-hosted Fynd?** Yes. Identical binary, same quote API. The hosted gateway adds a `/{chain}` path segment, API-key auth, and rate limiting — none of which exist in the self-hosted binary. The per-chain request/response bodies are identical.

**How do I know which Fynd version is running?** The unauthenticated endpoint `https://fynd-api.propellerheads.xyz/api-docs/openapi.json` exposes the version under `info.version`. We update the hosted backends shortly after each release. During beta the surface area may change; we post breaking changes in [our Telegram group](https://t.me/+B4CNQwv7dgIyYTJl) ahead of rollout.

**Can I use one key for all chains?** Yes. A single API key works against every `/v1/{chain}/…` path. The rate-limit bucket is shared across chains.

**Why did my quote return `no_route_found`?** The solver found no profitable route for the pair at the requested size on that chain. Common causes: thin [Tycho](https://docs.propellerheads.xyz/tycho)-indexed liquidity for the pair, an amount too large or too small, or the chain's backend is still warming up. Check `/v1/{chain}/health` — `last_update_ms: 0` indicates the Tycho stream isn't delivering live state yet.

**Can I bring my own RPC and Tycho endpoint with the hosted API?** No — the hosted API uses Propeller Heads' Tycho endpoints. To use your own, self-host.

## Next steps

* [API reference](/reference/api) — full OpenAPI spec
* [Encoding Options](/guides/encoding-options) — turn a quote into a submittable transaction (Permit2, transfer types, price guard)
* [Fynd Fees](/guides/router-fees) — fees Fynd charges on executed swaps
* [Charge Fees on your Swaps](/guides/client-fees) — add an integrator fee
* [Self-host Fynd](/get-started/quickstart) — when you're ready to outgrow the hosted tiers


# Server Configuration

Reference for all Fynd server flags, worker pool tuning, blocklist configuration, logging, and monitoring.

## Run options

All on-chain protocols available on your configured Tycho endpoint are fetched by default, so `--protocols` is optional. The `--tycho-url` also defaults to the Fynd endpoint for the selected chain.

```bash
fynd serve
```

To run on a different chain:

```bash
fynd serve --chain base
```

`--rpc-url` defaults to the public endpoint `https://eth.llamarpc.com`. For production, use a dedicated endpoint:

```bash
fynd serve \
  --rpc-url https://your-rpc-provider.com/v1/your_key
```

Specify protocols explicitly:

```bash
fynd serve \
  --protocols uniswap_v2,uniswap_v3,ekubo_v3,fluid_v1
```

See the full [list of available protocols](https://docs.propellerheads.xyz/tycho/for-solvers/supported-protocols).

### Including RFQ Protocols

Include RFQ (Request-for-Quote) protocols alongside on-chain protocols. Use the `all_onchain` keyword to combine auto-fetched on-chain protocols with specific RFQ protocols:

```bash
fynd serve \
  --protocols all_onchain,rfq:bebop
```

Or specify both on-chain and RFQ protocols explicitly:

```bash
fynd serve \
  --protocols uniswap_v2,uniswap_v3,rfq:bebop
```

**Limitations:**

* RFQ protocols cannot run alone. At least one on-chain protocol is required.
* When encoding is enabled (`encoding_options` in the quote request), RFQ quotes require an additional round-trip to the RFQ provider to fetch a signed quote. This can add significant tail latency to solve times. If you are using RFQ protocols, consider quoting first without encoding to evaluate the price, and only request encoding once you are confident the quote is worth executing.

**Environment variables:**

* RFQ protocols require API keys passed via environment variables. Check the [RFQ protocol docs](https://docs.propellerheads.xyz/tycho/for-solvers/request-for-quote-protocols) for the specific variables each protocol needs.

### pAMM Price Level Stream

Serve a proprietary AMM from Titan's pAMM price level stream instead of simulating it in the EVM. Titan publishes a quote ladder per pair every block, so quotes come from interpolating those levels — much cheaper than a VM simulation.

Name a venue with the `pricelevelstream:` prefix. The served venues are `fermiswap`, `kipseli`, `metric`, `bebop`, and `taurusfi`:

```bash
fynd serve \
  --protocols all_onchain,exclude:vm:fermiswap,pricelevelstream:fermiswap
```

The `exclude:` prefix drops a protocol from the list. It matters here: `vm:fermiswap` and `pricelevelstream:fermiswap` price the same maker inventory, so streaming both double-counts that liquidity. Drop the Tycho-streamed one whenever you serve the same venue from the price level stream. An `exclude:` entry that matches no streamed protocol stops the solver rather than warning, so a typo cannot silently ship the double-counted market.

**Limitations:**

* Ethereum mainnet only — the venue addresses are mainnet deployments.
* Quotes below the smallest ladder level are rejected rather than extrapolated, and quotes above the largest come back as a partial fill at the limit.
* A quote executes only in the block it was quoted for. The venue rejects a fill priced off a stale reading, so a quote that misses its block reverts rather than filling at the old price.

### Self-hosted Tycho

By default `--tycho-url` points at the [Fynd hosted endpoint](https://docs.propellerheads.xyz/tycho/for-solvers/hosted-endpoints#tycho-fynd) for the selected chain. Fynd talks to Tycho purely over its RPC/WebSocket API, so whether that Tycho is PropellerHeads-hosted or one you run yourself is transparent to Fynd — you only change where it points.

Run your own Tycho when a chain has no hosted Substreams endpoint. The Tycho Indexer can stream from a self-hosted Firehose + Substreams stack; see [Self-Hosted EVM Chain](https://docs.propellerheads.xyz/tycho/for-solvers/self-hosted-evm-chain) for how to stand it up. Once it is serving, point Fynd at it:

```bash
fynd serve \
  --chain base \
  --tycho-url your-self-hosted-tycho.example.com \
  --rpc-url https://your-node
```

Notes:

* **TLS** — hosted endpoints use TLS; a local or plain-HTTP Tycho does not. Add `--disable-tls` when your endpoint is not served over TLS.
* **API key** — the self-hosted indexer's RPC key is its `AUTH_API_KEY` (default `local-dev-key`). Pass it with `--tycho-api-key` / `TYCHO_API_KEY` if your deployment sets one.
* **Built-in chains** — Fynd's built-in chains are `ethereum`, `base`, `unichain`, `arbitrum`, `polygon`, `bsc`, `starknet`, `zksync`. Self-hosting Tycho for one of these works out of the box.

#### Custom chains

Fynd can also run against a chain that isn't one of Tycho's built-ins, as long as your self-hosted Tycho indexer declares it. Point Fynd at the same `chains.yaml` the indexer uses with `--chains-config` / `TYCHO_CHAINS_CONFIG`, and pass `--tycho-url` and `--rpc-url` explicitly — custom chains have no built-in defaults, so omitting either is an error:

```bash
fynd serve \
  --chain tempo \
  --chains-config ./chains.yaml \
  --tycho-url your-self-hosted-tycho.example.com \
  --rpc-url https://your-node \
  --disable-tls
```

See Tycho's [Self-Hosted EVM Chain](https://docs.propellerheads.xyz/tycho/for-solvers/self-hosted-evm-chain) guide for the "Declaring a custom chain" (`chains.yaml`) and "Consuming the custom chain" sections.

**Quote-only until the router is deployed** — a custom chain has no Tycho router/executor contracts until ops deploys them. Until then, Fynd runs quote-only for that chain: `GET /v1/info` reports `router_address: null`, and a quote request with `encoding_options` set returns `501 Not Implemented`. Once the router/executor contracts are deployed, encoding works as usual.

## Flag reference

Run `fynd serve --help` for the full list.

### Required

| Flag              | Env Var         | Description   |
| ----------------- | --------------- | ------------- |
| `--tycho-api-key` | `TYCHO_API_KEY` | Tycho API key |

### Optional

| Flag                                          | Env Var               | Default                                                                                                  | Description                                                                                                                                                                                                                                                                                                                  |
| --------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--rpc-url`                                   | `RPC_URL`             | `https://eth.llamarpc.com`                                                                               | Node RPC endpoint for the target chain. Use a dedicated endpoint in production.                                                                                                                                                                                                                                              |
| `--tycho-url`                                 | `TYCHO_URL`           | *(chain-specific)*                                                                                       | Tycho URL. Defaults to the [Fynd hosted endpoint](https://docs.propellerheads.xyz/tycho/for-solvers/hosted-endpoints#tycho-fynd) for the selected chain.                                                                                                                                                                     |
| `--chain`                                     | —                     | `Ethereum`                                                                                               | Target chain                                                                                                                                                                                                                                                                                                                 |
| `--chains-config`                             | `TYCHO_CHAINS_CONFIG` | *(none)*                                                                                                 | Path to the custom-chains `chains.yaml`. Required for a chain Tycho does not know as a built-in.                                                                                                                                                                                                                             |
| `-p, --protocols`                             | —                     | *(all on-chain)*                                                                                         | Protocols to index (comma-separated). If omitted, all on-chain protocols available on your configured Tycho endpoint are fetched. Use `all_onchain` to combine auto-fetched protocols with explicit entries (e.g. `all_onchain,rfq:bebop`), and the `exclude:` prefix to drop one (e.g. `all_onchain,exclude:vm:fermiswap`). |
| `--http-host`                                 | `HTTP_HOST`           | `0.0.0.0`                                                                                                | HTTP bind address                                                                                                                                                                                                                                                                                                            |
| `--http-port`                                 | `HTTP_PORT`           | `3000`                                                                                                   | API port                                                                                                                                                                                                                                                                                                                     |
| `--min-tvl`                                   | —                     | `10.0`                                                                                                   | Minimum pool TVL in native token (ETH)                                                                                                                                                                                                                                                                                       |
| `--tvl-buffer-ratio`                          | —                     | `1.1`                                                                                                    | Hysteresis buffer for TVL filtering. Components are added when TVL >= `min_tvl` and removed when TVL drops below `min_tvl / tvl_buffer_ratio`.                                                                                                                                                                               |
| `--traded-n-days-ago`                         | —                     | `3`                                                                                                      | Only include tokens traded within this many days.                                                                                                                                                                                                                                                                            |
| `--worker-router-timeout-ms`                  | —                     | `100`                                                                                                    | Default solve timeout (ms)                                                                                                                                                                                                                                                                                                   |
| `--worker-router-min-responses`               | —                     | `0`                                                                                                      | Early return threshold (0 = wait for all pools)                                                                                                                                                                                                                                                                              |
| `-w, --worker-pools-config`                   | `WORKER_POOLS_CONFIG` | `worker_pools.toml`                                                                                      | Worker pools config file path                                                                                                                                                                                                                                                                                                |
| `--blocklist-config`                          | `BLOCKLIST_CONFIG`    | [tycho-simulation default](https://github.com/propeller-heads/tycho-simulation/blob/main/blocklist.toml) | Path to blocklist TOML config file. Components listed here are excluded from the Tycho stream.                                                                                                                                                                                                                               |
| `--disable-tls`                               | —                     | `false`                                                                                                  | Disable TLS for Tycho connection                                                                                                                                                                                                                                                                                             |
| `--min-token-quality`                         | —                     | `100`                                                                                                    | Minimum [token quality](https://docs.propellerheads.xyz/tycho/overview/concepts#token) filter                                                                                                                                                                                                                                |
| `--gas-refresh-interval-secs`                 | —                     | `30`                                                                                                     | Gas price refresh interval                                                                                                                                                                                                                                                                                                   |
| `--reconnect-delay-secs`                      | —                     | `5`                                                                                                      | Reconnect delay on connection failure                                                                                                                                                                                                                                                                                        |
| `--gas-price-stale-threshold-secs`            | —                     | *(disabled)*                                                                                             | Health returns 503 when gas price exceeds this age. Disabled by default.                                                                                                                                                                                                                                                     |
| `--partial-blocks`                            | —                     | `false`                                                                                                  | Enable partial block (flashblock) updates from the Tycho stream. Pool state updates are delivered mid-block rather than only at finalization, reducing latency. Only applies to on-chain protocols.                                                                                                                          |
| `--enable-price-guard`                        | —                     | `false`                                                                                                  | Enable [price guard](/guides/price-guard) validation against external price sources.                                                                                                                                                                                                                                         |
| `--price-guard-lower-tolerance-bps`           | —                     | `300`                                                                                                    | Max allowed deviation (bps) when the quote's output is below the provider's expected amount.                                                                                                                                                                                                                                 |
| `--price-guard-upper-tolerance-bps`           | —                     | `10000`                                                                                                  | Max allowed deviation (bps) when the quote's output is above the provider's expected amount.                                                                                                                                                                                                                                 |
| `--price-guard-fail-on-provider-error`        | —                     | `false`                                                                                                  | Reject quotes when all price providers fail with infrastructure errors.                                                                                                                                                                                                                                                      |
| `--price-guard-fail-on-token-price-not-found` | —                     | `false`                                                                                                  | Reject quotes when no provider lists the token.                                                                                                                                                                                                                                                                              |
| `--metrics-port`                              | `METRICS_PORT`        | `9898`                                                                                                   | Port for the Prometheus metrics HTTP server. Requires the `metrics` feature (enabled by default).                                                                                                                                                                                                                            |

## Worker pools (`worker_pools.toml`)

Worker pools control solver thread count and routing strategies. The default config ships with one pool:

```toml
# worker_pools.toml
[pools.bellman_ford_2_hops]
algorithm = "bellman_ford"
num_workers = 3
task_queue_capacity = 1000
max_hops = 2
timeout_ms = 500
```

All pools solve every incoming order in parallel. Fynd picks the best result across pools within the timeout.

### Worker pool fields

| Field                 | Default            | Description                                                                                                                                                          |
| --------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `algorithm`           | *(required)*       | Algorithm used for the pool (`"most_liquid"`, `"bellman_ford"`, `"path_frank_wolfe"`, or `"water_fill"`)                                                             |
| `num_workers`         | CPU count          | Number of OS threads dedicated to this pool                                                                                                                          |
| `task_queue_capacity` | `1000`             | Maximum number of orders that can be queued simultaneously                                                                                                           |
| `min_hops`            | `1`                | Minimum number of hops required for routing                                                                                                                          |
| `max_hops`            | `3`                | Maximum number of hops permitted for routing                                                                                                                         |
| `timeout_ms`          | `100`              | Maximum time in milliseconds allowed per order processing in this pool                                                                                               |
| `max_routes`          | *(no limit)*       | Maximum number of candidate routes to evaluate per order                                                                                                             |
| `connector_tokens`    | *(no restriction)* | Allowlist of `"0x..."`-prefixed token addresses permitted as intermediate hops. Source and destination are always allowed regardless. Absent = all tokens reachable. |

### Connector tokens

By default Fynd routes through any token reachable in the pool graph. On live markets this can expose routes to illiquid or long-tail intermediates, which increases reversion risk: price impact at the intermediate hop can push slippage over the tolerance threshold, causing the transaction to revert.

`connector_tokens` restricts intermediate hops to a trusted set. It is most useful for deployments that are particularly sensitive to reverts:

```toml
[pools.bellman_ford_safe]
algorithm  = "bellman_ford"
max_hops   = 3
timeout_ms = 500
connector_tokens = [
    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",  # WETH
    "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",  # USDC
    "0xdac17f958d2ee523a2206206994597c13d831ec7",  # USDT
    "0x6b175474e89094c44da98b954eedeac495271d0f",  # DAI
    "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",  # WBTC
    "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0",  # wstETH
]
```

> **Anchor tokens are not configured.** The `water_fill` algorithm's discovery uses a soft anchor preference when no `connector_tokens` allowlist is set — it prefers to route through the most connected tokens plus the native-ETH sentinel. This set is derived per solve from the live graph, so it stays correct on every chain automatically and needs no configuration.

Use `fynd derive-connector-tokens` to generate a ranked list for your chain from live Tycho data:

```bash
fynd derive-connector-tokens --chain Ethereum --top-n 10 --output toml
```

The command scores every token by pool count and outputs a ready-to-paste TOML snippet. Run `fynd derive-connector-tokens --help` for all options.

> **Tradeoff:** A narrower allowlist reduces reversion risk but may also reduce route quality — routes through unlisted tokens are never explored. For most chains, 5–10 highly liquid tokens cover the vast majority of pairs.

To use a custom config file:

```bash
fynd serve -w my_worker_pools.toml
```

## Blocklist config

By default, Fynd loads `blocklist.toml` from tycho-simulation. The default excludes components with known simulation issues (e.g., [rebasing tokens on UniswapV3 pools](https://docs.uniswap.org/concepts/protocol/integration-issues)). Override with `--blocklist-config`:

```bash
fynd serve --blocklist-config my_blocklist.toml
```

The config file uses a `[blocklist]` section listing component IDs to exclude:

```toml
[blocklist]
components = [
    "0x86d257cdb7bc9c0df10e84c8709697f92770b335",
]
```

## Logging and monitoring

### Logs

Control log verbosity with `RUST_LOG`:

```bash
# Minimal output
RUST_LOG=warn fynd serve ...

# Default (recommended)
RUST_LOG=fynd=info fynd serve ...

# Debug solver internals
RUST_LOG=info,fynd_core=debug fynd serve ...

# Trace-level (very verbose, not recommended)
RUST_LOG=info,fynd_core=trace fynd serve ...
```

### Prometheus metrics

Fynd exposes Prometheus metrics on a dedicated HTTP server (enabled by default via the `metrics` feature). Scrape the `/metrics` endpoint with Prometheus or any compatible tool:

```
http://localhost:9898/metrics
```

The port defaults to `9898` and can be changed with `--metrics-port` or the `METRICS_PORT` environment variable:

```bash
fynd serve --metrics-port 9090
```

Available metrics include solve duration, response counts, failure types, and pool performance.

## Tuning tips

### Worker pools

* **More workers** = more orders can be solved concurrently. Each worker is a dedicated OS thread, so avoid exceeding your CPU core count across all pools.
* **Lower `max_hops`** = faster solves but may miss better multi-hop routes.
* **Higher `max_hops`** = explores deeper routes but takes longer. Pair with a higher `timeout_ms`.
* **Multiple pools** with different `max_hops` and `timeout_ms` let you trade off speed vs. route quality — e.g. a fast 2-hop pool alongside a slower 3-hop pool.
* **Lower `max_routes`** = more predictable latency on large graphs, at the cost of potentially missing a better route.

### Request routing

* **Lower `--worker-router-min-responses`** = faster response with multiple pools — set to `1` to return as soon as the first pool finishes, at the cost of potentially missing a better result from a slower pool.


# Encoding Options

When you request a quote, you can include `encoding_options` to have Fynd encode the swap into a ready-to-submit transaction. Without encoding options, you get a quote only (price, route, gas estimate) but no transaction.

For full details on how the TychoRouter contract works, see the [Tycho execution docs](https://docs.propellerheads.xyz/tycho/for-solvers/execution).

## Fields

| Field               | Type               | Required | Default           | Description                                                                                                             |
| ------------------- | ------------------ | -------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `slippage`          | `string`           | yes      | —                 | Slippage tolerance as a decimal string (e.g. `"0.005"` = 0.5%). Applied to the quoted output to compute `minAmountOut`. |
| `transfer_type`     | `string`           | no       | `"transfer_from"` | How the router receives your input tokens. See [transfer types](#transfer-types).                                       |
| `permit`            | `PermitSingle`     | no       | —                 | Permit2 authorization. Required when `transfer_type` is `"transfer_from_permit2"`.                                      |
| `permit2_signature` | `string`           | no       | —                 | Hex-encoded 65-byte signature over the permit. Required when `permit` is set.                                           |
| `client_fee_params` | `ClientFeeParams`  | no       | —                 | Optional integrator fee. See the [client fees guide](/guides/client-fees).                                              |
| `price_guard`       | `PriceGuardConfig` | no       | —                 | Per-request overrides for price-guard validation. See the [price guard guide](/guides/price-guard).                     |

## Transfer types

The `transfer_type` field controls how the TychoRouter contract receives your input tokens. For a deeper explanation see the [Tycho execution docs](https://docs.propellerheads.xyz/tycho/for-solvers/execution).

### `transfer_from` (default)

Standard ERC-20 approval flow. Before submitting the transaction, the sender must have called `approve()` on the input token granting the TychoRouter contract a sufficient allowance.

### `transfer_from_permit2`

Uses Uniswap's [Permit2](https://docs.propellerheads.xyz/tycho/for-solvers/execution) contract for gasless approvals. The sender signs a `PermitSingle` off-chain and passes it along with the signature in the quote request. No on-chain `approve()` needed (but the token must be approved to the Permit2 contract).

When using this transfer type, both `permit` and `permit2_signature` are required.

### `use_vaults_funds`

Draws tokens from the sender's vault balance in the TychoRouter contract (ERC-6909). No approval or permit needed — tokens must have been deposited into the vault beforehand. See the [vault mechanism](https://docs.propellerheads.xyz/tycho/for-solvers/execution) in the Tycho docs.

## Slippage

The `slippage` value is a decimal fraction:

| Value     | Meaning |
| --------- | ------- |
| `"0.001"` | 0.1%    |
| `"0.005"` | 0.5%    |
| `"0.01"`  | 1%      |

Fynd computes `minAmountOut = quotedAmountOut * (1 - slippage)` and encodes it into the transaction. If on-chain execution produces less than `minAmountOut`, the transaction reverts.

Typical values are `0.005` (0.5%) for stablecoin pairs and `0.01` (1%) for volatile pairs.

The router rejects a `minAmountOut` more than 20% below the quoted output, so fees plus slippage must stay within that band. Encoding fails with an error rather than returning calldata that would revert.

## The response transaction

When encoding options are present and the quote succeeds, the response includes a `transaction` object:

| Field   | Type     | Description                                                                                                                                 |
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `to`    | `string` | The TychoRouter contract address. See [contract addresses](https://docs.propellerheads.xyz/tycho/for-solvers/execution/contract-addresses). |
| `value` | `string` | Native token value (wei). Non-zero only when the input token is the native token.                                                           |
| `data`  | `string` | Hex-encoded calldata. Submit this as the `data` field of your Ethereum transaction.                                                         |

Use `to`, `value`, and `data` directly in your transaction. Set `from` to the sender address from your order, choose a gas limit (the quote's `gas_estimate` is a good starting point), and submit.


# Swap CLI

Use fynd-swap-cli to dry-run and execute swaps against a running Fynd server.

`fynd-swap-cli` is a CLI binary for quoting, simulating, and executing swaps. It's useful for quick testing from the terminal without writing any code.

## Setup

{% tabs %}
{% tab title="Docker Compose" %}
**Prerequisites:**

* **Docker**: [install Docker](https://docs.docker.com/engine/install/)
* **Tycho API key**: [get one here](https://t.me/fynd_portal_bot)

Get the [docker-compose.swap.yml](https://github.com/propeller-heads/fynd/blob/main/docker-compose.swap.yml) file:

```bash
curl -o docker-compose.swap.yml https://raw.githubusercontent.com/propeller-heads/fynd/main/docker-compose.swap.yml
```

Start the server and drop into a shell with `fynd-swap-cli` pre-installed:

```bash
export TYCHO_API_KEY=your_tycho_api_key

docker compose -f docker-compose.swap.yml run --rm fynd-shell
```

This also starts `fynd-serve` automatically. Run swaps with:

```bash
# Inside the fynd-shell container:
fynd-swap-cli
```

{% hint style="info" %}
For on-chain execution, pass `PRIVATE_KEY` at startup: `docker compose -f docker-compose.swap.yml run --rm -e PRIVATE_KEY=your_key fynd-shell`
{% endhint %}

When done, stop and remove the server container:

```bash
docker compose -f docker-compose.swap.yml down
```

{% endtab %}

{% tab title="Build from source" %}
**Prerequisites:** A running Fynd server — start `fynd serve` first. See the [Quickstart](/get-started/quickstart) if you haven't.

```bash
cargo install --path tools/fynd-swap-cli
```

{% endtab %}
{% endtabs %}

***

## Dry-run a swap (ERC-20)

By default, `fynd-swap-cli` runs a **dry-run**: it uses a well-funded sender address and injects ERC-20 storage overrides so the simulation succeeds without any real funds or wallet approvals.

```bash
fynd-swap-cli
```

This sells 1 WETH for USDC using the defaults. Pass explicit tokens and amounts to customise:

```bash
fynd-swap-cli \
  --sell-token  0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
  --buy-token   0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
  --sell-amount 2000000000000000000
```

The output prints the quote (amount\_in, amount\_out, gas estimate, route) followed by the simulation result.

{% hint style="info" %}
`--sell-amount` is in raw atomic units. 1 000 000 000 = 1000 USDC (6 decimals). 1 000 000 000 000 000 000 = 1 WETH (18 decimals).
{% endhint %}

## Dry-run a swap (Permit2)

Add `--transfer-type transfer-from-permit2`. The dry-run uses nonce 0 and maximum deadlines, so no chain reads are needed.

```bash
fynd-swap-cli --transfer-type transfer-from-permit2
```

## Execute on-chain (ERC-20)

Add `--execute` and set `PRIVATE_KEY`. The CLI checks the router allowance automatically and submits an approval transaction first if one is needed.

{% hint style="warning" %}
This sends real transactions. Ensure your wallet has the sell token before running with `--execute`.
{% endhint %}

```bash
export RPC_URL=https://your-rpc-provider.com/v1/your_key
export PRIVATE_KEY=your_private_key_hex   # no 0x prefix

fynd-swap-cli --execute
```

## Execute on-chain (Permit2)

Add `--execute --transfer-type transfer-from-permit2`. The CLI checks whether the ERC-20 allowance to the Permit2 contract is sufficient for the swap. If not, it approves the maximum amount so subsequent swaps do not require re-approval. It then reads the current nonce, builds the EIP-712 permit, signs it, and submits the swap.

```bash
export RPC_URL=https://your-rpc-provider.com/v1/your_key
export PRIVATE_KEY=your_private_key_hex   # no 0x prefix

fynd-swap-cli --transfer-type transfer-from-permit2 --execute
```

## Swap using vault funds

If tokens are already deposited in the Tycho Router vault, use `--transfer-type use-vaults-funds`. No ERC-20 approval or Permit2 signature is needed.

```bash
fynd-swap-cli --transfer-type use-vaults-funds
```

***

## CLI Reference

| Flag              | Env var    | Default                                      | Description                                                     |
| ----------------- | ---------- | -------------------------------------------- | --------------------------------------------------------------- |
| `--sell-token`    | —          | WETH (mainnet)                               | Token address to sell                                           |
| `--buy-token`     | —          | USDC (mainnet)                               | Token address to buy                                            |
| `--sell-amount`   | —          | `1000000000000000000` (1 WETH)               | Amount to sell in raw atomic units                              |
| `--slippage-bps`  | —          | `50` (0.5%)                                  | Slippage tolerance in basis points                              |
| `--fynd-url`      | `FYND_URL` | `http://localhost:3000`                      | Fynd server URL                                                 |
| `--transfer-type` | —          | `transfer-from`                              | `transfer-from`, `transfer-from-permit2`, or `use-vaults-funds` |
| `--execute`       | —          | false (dry-run)                              | Submit the swap on-chain. Requires `PRIVATE_KEY`.               |
| `--permit2`       | —          | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | Permit2 contract address                                        |
| `--rpc-url`       | `RPC_URL`  | `https://reth-ethereum.ithaca.xyz/rpc`       | Ethereum RPC endpoint (must support `eth_call` state overrides) |

## Security Notes

1. **Never expose your private key.** Use the `PRIVATE_KEY` environment variable, never a CLI argument. Run `unset HISTFILE` before setting it to prevent it from being saved to your shell history.
2. **Dry-run first.** The default mode (no `--execute`) simulates the full swap with storage overrides — no funds needed. Confirm the output looks correct before adding `--execute`.
3. **Slippage protection.** The default 0.5% slippage may not be sufficient for large trades or volatile markets. Adjust `--slippage-bps` accordingly.
4. **Mainnet warning.** `--execute` may send multiple transactions (approval + swap). Start with small amounts. All routes execute through the [Tycho Router](https://docs.propellerheads.xyz/tycho/for-solvers/execution/contract-addresses) contract.
5. **Verify routes.** The CLI prints the full route before executing. Multi-hop routes through low-liquidity pools can result in worse execution.
6. **Prices are indicative.** Quotes reflect the best route at query time but are not guaranteed on-chain. Pool states change every block, and the longer you wait to execute, the more the price may drift.

## Troubleshooting

**"Solver is not healthy"**: Wait for the solver to finish loading market data. Check the `fynd serve` terminal for progress, or poll `curl http://localhost:3000/v1/health`.

**"Sell/buy token not found"**: Ensure the token address is correct and [the token exists on Tycho's indexer](https://docs.propellerheads.xyz/tycho/for-solvers/indexer/tycho-rpc#post-v1-tokens).

**"No route found"**: Fynd couldn't find a path between your tokens. Check that both tokens have enough on-chain liquidity.


# Price Guard

Fynd quotes can carry a concrete `min_amount_out` that commits the user to a trade. A mispriced quote — caused by stale pool data, a bug, or manipulated liquidity — can lock the user into an unfavorable execution. The price guard catches these before they reach the caller.

It sits between solving and encoding in the order pipeline, querying multiple independent price oracles (Hyperliquid and Binance by default) concurrently and rejecting any solution whose `amount_out` falls outside the tolerance interval. When validation fails, the quote's status is set to `price_check_failed`; other orders in the same batch are unaffected.

## Configuration

The server controls only whether the guard is on or off. All other parameters — tolerance thresholds and fallback behavior — are set per-request by the client. Omitted fields fall back to struct defaults.

### Server-side

The guard is disabled by default. Enable it with `--enable-price-guard`:

```bash
fynd --enable-price-guard
```

When enabled, price providers (Hyperliquid, Binance) are started in the background so their caches stay warm. Validation only runs for requests where the client sets `enabled: true` in `encoding_options.price_guard`. When disabled, no providers are started and requests that set `enabled: true` return an error.

### Per-request

Clients configure tolerance and fallback behavior through `encoding_options.price_guard` (see [encoding options](/guides/encoding-options)). Omitted fields use the defaults shown below.

| Field                           | Type      | Default | Description                                                                                                       |
| ------------------------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `enabled`                       | `boolean` | `false` | Set to `true` to run price guard validation for this request. Requires the server to have `--enable-price-guard`. |
| `lower_tolerance_bps`           | `integer` | `300`   | Max allowed deviation in basis points when the quote's `amount_out` is below the provider's expected amount out.  |
| `upper_tolerance_bps`           | `integer` | `10000` | Max allowed deviation in basis points when the quote's `amount_out` is above the provider's expected amount out.  |
| `fail_on_provider_error`        | `boolean` | `false` | See [fallback behavior](#fallback-behavior).                                                                      |
| `fail_on_token_price_not_found` | `boolean` | `false` | See [fallback behavior](#fallback-behavior).                                                                      |

```bash
fynd --enable-price-guard
```

**Development** — leave the guard disabled on the server. No providers are started and no resources are used.

```bash
fynd  # no --enable-price-guard
```

## Tolerance

The quote's `amount_out` is compared to each provider's expected amount out and the check short-circuits as soon as one provider validates within tolerance — the remaining providers are not consulted.

For both directions, deviation is computed the same way:

```
deviation_bps = abs(expected - actual) * 10000 / expected
```

* `amount_out < expected` → reject if deviation exceeds `lower_tolerance_bps`
* `amount_out >= expected` → reject if deviation exceeds `upper_tolerance_bps`

The lower bound is stricter by default (`300` bps = 3%) to catch under-delivery — the user getting less than expected. The upper bound is looser (`10000` bps = 100%, allowing `amount_out` up to twice the expected) to catch suspicious over-delivery that may indicate a pricing bug; lower it for stricter checks.

## Fallback behavior

The fallback flags apply only when no provider returned a price — every response was either an infrastructure error or `price_not_found`. If any provider returned a price, the quote is judged purely on tolerance regardless of these flags: in-tolerance passes, out-of-tolerance rejects.

* **`fail_on_provider_error`** — applies when all providers failed with an infrastructure error (network issue, API down, rate-limited). `false` (default) lets the quote pass; `true` rejects it.
* **`fail_on_token_price_not_found`** — applies when every provider was reached but none list the token. `false` (default) lets the quote pass; `true` rejects it.

When responses mix `price_not_found` with infrastructure errors, the token might be listed on one of the unreachable providers, so the guard applies `fail_on_provider_error` rather than `fail_on_token_price_not_found`.

## Symbol collisions and long-tail tokens

Price providers (Binance, Hyperliquid) identify tokens by their trading symbol — e.g. "ETH", "LINK", "PEPE". On-chain, symbols are not unique: any token can declare itself "PEPE", and multiple unrelated tokens on the same chain may share a symbol. The guard resolves tokens by matching the on-chain symbol from `MarketState` to a provider's symbol, so a long-tail token whose symbol collides with a well-known token will be priced as if it were that token.

In practice this means the guard works reliably for major tokens listed on CEXs, but may produce false rejections (or false passes) for obscure tokens that happen to share a symbol with a listed asset. Clients trading long-tail tokens should consider leaving `enabled: false` for those requests.

## Custom providers

The `PriceProvider` trait, `ExternalPrice`, and `PriceProviderError` are public. Implement the trait to add your own price provider and register it via `FyndBuilder::register_price_provider()`:

```rust
let solver = FyndBuilder::new(chain, tycho_url, rpc_url, protocols, min_tvl)
    .register_price_provider(Box::new(MyCustomProvider::new()))
    .price_guard_enabled(true)
    .build()
    .await?;
```

Providers follow a worker+cache pattern: `start()` spawns a background task that populates an in-memory cache, and `get_expected_out()` reads from that cache without blocking or making network calls.

If no providers are registered before `build()`, the built-in providers (Hyperliquid, Binance) are added automatically. Calling `register_price_provider()` skips the defaults — register only what you need.

To keep the defaults **and** add a custom provider, call `add_default_price_providers()` first:

```rust
let solver = FyndBuilder::new(chain, tycho_url, rpc_url, protocols, min_tvl)
    .add_default_price_providers()
    .register_price_provider(Box::new(MyCustomProvider::new()))
    .price_guard_enabled(true)
    .build()
    .await?;
```

## Example Quote with Price Guard protection

Enable the price guard server-side, then tighten the lower bound and fail-closed on unknown tokens for a specific request:

```json
{
  "orders": [
    {
      "token_in": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      "token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "amount": "1000000000000000000",
      "side": "sell",
      "sender": "0x0000000000000000000000000000000000000001"
    }
  ],
  "options": {
    "encoding_options": {
      "price_guard": {
        "enabled": true,
        "lower_tolerance_bps": 100,
        "upper_tolerance_bps": 5000,
        "fail_on_token_price_not_found": true
      }
    }
  }
}
```

### Disabling per-request

To disable the price guard on a server that has it enabled, send `enabled: false` in `encoding_options.price_guard`:

```json
"encoding_options": {
  "price_guard": { "enabled": false }
}
```


# Fynd Fees

Fynd charges a fee when you execute a swap. Quotes are free.

The default Fynd fee is 0.1 bps (0.001%) of swap output. Contact us for volume discounts.

If you charge your own swap fees, Fynd also takes 20% of those fees. See [Charge Fees on your Swaps](/guides/client-fees).

## Fee breakdown

Quotes with encoding include a `fee_breakdown` with the exact amounts.

`amount_out` is the raw pre-fee swap output: what the route produces before router or client fees. It is **not** what the user receives. The user receives at least `fee_breakdown.min_amount_received` on-chain.

Fynd mirrors the on-chain `FeeCalculator` with identical integer arithmetic, then uses the result for `minAmountOut` in the encoded transaction.

Given `amount_out`, `router_fee_bps`, and `slippage`:

```
1. router_fee        = amount_out * router_fee_bps / 10,000
2. amount_after_fees = amount_out - router_fee
3. max_slippage      = amount_after_fees * slippage
4. min_amount_received = amount_after_fees - max_slippage
```

All response fields use output token units:

| Field                 | Description                                                     |
| --------------------- | --------------------------------------------------------------- |
| `router_fee`          | Fynd fee                                                        |
| `client_fee`          | `0` unless you [charge fees on your swaps](/guides/client-fees) |
| `max_slippage`        | Slippage allowance on the post-fee amount                       |
| `min_amount_received` | On-chain minimum the user receives (`minAmountOut` in the tx)   |

Invariant without client fees: `amount_out = router_fee + max_slippage + min_amount_received`

### Example

Example: 1,000,000 USDC output, 0.1 bps Fynd fee, 1% slippage:

```
router_fee           = 1,000,000 * 0.1 / 10,000         = 10
amount_after_fees    = 1,000,000 - 10                   = 999,990
max_slippage         = 999,990 * 0.01                   = 9,999
min_amount_received  = 999,990 - 9,999                  = 989,991
```

## Charge Fees on your Swaps

Fynd fees are separate from integrator fees. If you want to monetize your swap flow, see [Charge Fees on your Swaps](/guides/client-fees).

When you add a client fee, `router_fee` also includes Fynd's 20% share of that client fee.


# Charge Fees on your Swaps

Use client fees to monetize your swap flow. Client fees are optional integrator fees set with `ClientFeeParams`.

The integrator keeps 80% of the client fee. Fynd keeps 20%.

Client fees are separate from [Fynd fees](/guides/router-fees), which still apply when no client fee is set.

## Fee breakdown with client fees

Quotes with encoding include a `fee_breakdown` with the exact amounts.

`amount_out` is the raw pre-fee swap output: what the route produces before router or client fees. It is **not** what the user receives. The user receives at least `fee_breakdown.min_amount_received` on-chain.

Fynd mirrors the on-chain `FeeCalculator` with identical integer arithmetic, then uses the result for `minAmountOut` in the encoded transaction.

Given `amount_out`, `router_fee_bps` (see [Fynd Fees](/guides/router-fees)), `client_fee_bps`, and `slippage`:

```
1. client_fee        = amount_out * client_fee_bps / 10,000
2. router_share      = amount_out * client_fee_bps * 2,000 / 100,000,000
3. client_portion    = client_fee - router_share
4. router_fee_output = amount_out * router_fee_bps / 10,000
5. router_fee        = router_share + router_fee_output
6. amount_after_fees = amount_out - client_portion - router_fee
7. max_slippage      = amount_after_fees * slippage
8. min_amount_received = amount_after_fees - max_slippage
```

All response fields use output token units:

| Field                 | Description                                                   |
| --------------------- | ------------------------------------------------------------- |
| `router_fee`          | Fynd fee + 20% of client fee                                  |
| `client_fee`          | Integrator's 80% share of the client fee                      |
| `max_slippage`        | Slippage allowance on the post-fee amount                     |
| `min_amount_received` | On-chain minimum the user receives (`minAmountOut` in the tx) |

Invariant: `amount_out = router_fee + client_fee + max_slippage + min_amount_received`

### Example

Example: 1,000,000 USDC output, 0.1 bps Fynd fee, 50 bps client fee, 1% slippage:

```
client_fee (total)   = 1,000,000 * 50 / 10,000         = 5,000
router_share         = 1,000,000 * 50 * 2,000 / 1e8    = 1,000
client_portion       = 5,000 - 1,000                    = 4,000
router_fee_output    = 1,000,000 * 0.1 / 10,000         = 10
router_fee           = 10 + 1,000                        = 1,010
amount_after_fees    = 1,000,000 - 4,000 - 1,010        = 994,990
max_slippage         = 994,990 * 0.01                    = 9,949
min_amount_received  = 994,990 - 9,949                   = 985,041
```

## Setting up client fees

1. Set a fee in basis points (e.g. `50` = 0.5%), a receiver address, and a `maxClientContribution`.
2. Have the fee receiver sign an EIP-712 `ClientFee` message authorizing these parameters.
3. Attach the signed params to `EncodingOptions.clientFeeParams`.
4. The router verifies the signature on-chain and deducts the fee. Fees go to the receiver's vault balance.

Without `ClientFeeParams`, no client fee is charged. [Fynd fees](/guides/router-fees) still apply.

### maxClientContribution

`maxClientContribution` caps how much the client can subsidize from their vault balance if slippage pushes the output below `minAmountOut`. If the shortfall exceeds the cap, the transaction reverts.

Set it to `0` to collect fees without covering slippage losses. This is the common case.

See [Tycho encoding docs](https://docs.propellerheads.xyz/tycho/for-solvers/execution/encoding#encode) for vault details.

## EIP-712 signing

The fee receiver signs a typed data hash binding the fee params to the swap they were quoted for:

| Field                   | Type      | Description                                           |
| ----------------------- | --------- | ----------------------------------------------------- |
| `clientFeeBps`          | `uint32`  | Fee in fee units (100,000,000 = 100%; 1 bps = 10,000) |
| `clientFeeReceiver`     | `address` | Address receiving the fee                             |
| `maxClientContribution` | `uint256` | Maximum subsidy from client vault                     |
| `deadline`              | `uint256` | Signature expiry (Unix timestamp)                     |
| `amountIn`              | `uint256` | Exact input amount from the order                     |
| `tokenIn`               | `address` | Input token                                           |
| `tokenOut`              | `address` | Output token                                          |
| `expectedAmountOut`     | `uint256` | Quoted output (`amount_out` of the unsigned quote)    |
| `minAmountOut`          | `uint256` | `fee_breakdown.min_amount_received`                   |
| `receiver`              | `address` | Address receiving the swap output                     |
| `swaps`                 | `bytes`   | Encoded swaps — hashed as `fee_breakdown.swaps_hash`  |

The API takes the client fee in basis points and scales it into the router's fee units, so the client library helpers sign the scaled value rather than the raw bps.

**EIP-712 domain:**

| Field               | Value                        |
| ------------------- | ---------------------------- |
| `name`              | `TychoRouter`                |
| `version`           | `1`                          |
| `chainId`           | Target chain ID              |
| `verifyingContract` | TychoRouter contract address |

## Code examples

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// Build fee params (without signature).
const feeParams: ClientFeeParams = {
    bps: 50,              // 0.5% fee
    receiver: feeReceiver,
    maxContribution: 0n,  // no vault subsidy
    deadline: 1893456000, // Unix timestamp
};

// Compute the EIP-712 hash and sign with the fee receiver's wallet. The swap context comes
// from a prior unsigned quote.
const hash = clientFeeSigningHash(feeParams, 1, routerAddress, {
    amountIn: quote.amountIn,
    tokenIn: sellToken,
    tokenOut: buyToken,
    expectedAmountOut: quote.amountOut,
    minAmountOut: quote.feeBreakdown.minAmountReceived,
    receiver: sender,
    swapsHash: quote.feeBreakdown.swapsHash,
});
const signature = await account.signMessage({message: {raw: hash}});

// Attach signature and wire into encoding options.
const opts = withClientFee(encodingOptions(0.005), {...feeParams, signature});
```

{% endtab %}

{% tab title="Rust" %}

```rust
    // Step 1: request a quote using unsigned client fee params.
    // The server encodes the full calldata and returns `swaps_hash`
    // in the fee breakdown and `signature_offset` in the transaction
    // so the client can patch the real signature in.
    let fee = ClientFeeParams::new(
        FEE_BPS,
        Bytes::copy_from_slice(fee_receiver.as_slice()),
        BigUint::ZERO,
        u64::MAX,
    );
    let order = Order::new(
        Bytes::copy_from_slice(sell_token.as_slice()),
        Bytes::copy_from_slice(buy_token.as_slice()),
        BigUint::from(SELL_AMOUNT),
        OrderSide::Sell,
        Bytes::copy_from_slice(sender.as_slice()),
        None,
    );
    let quote = client
        .quote(QuoteParams::new(
            order,
            QuoteOptions::default()
                .with_timeout_ms(5_000)
                .with_encoding_options(EncodingOptions::new(SLIPPAGE).with_client_fee(fee.clone())),
        ))
        .await?;

    let fee_breakdown = quote
        .fee_breakdown()
        .ok_or("no fee breakdown in quote")?;
    let swaps_hash = fee_breakdown
        .swaps_hash()
        .ok_or("no swaps_hash — server must support client fee signing")?;

    // Step 2: sign the full 11-field EIP-712 ClientFee hash.
    // receiver defaults to sender when the order has no explicit receiver.
    let hash = fee.eip712_signing_hash(
        chain_id,
        &router_address,
        quote.amount_in(),
        &Bytes::copy_from_slice(sell_token.as_slice()),
        &Bytes::copy_from_slice(buy_token.as_slice()),
        quote.amount_out(),
        fee_breakdown.min_amount_received(),
        &Bytes::copy_from_slice(sender.as_slice()),
        swaps_hash,
    )?;
    let sig = fee_signer
        .sign_hash(&B256::from(hash))
        .await?;

    // Step 3: patch the real signature into the calldata.
    let quote = quote.with_client_fee_signature(&sig.as_bytes()[..])?;
```

See the full working example: [`clients/rust/examples/swap_client_fee.rs`](https://github.com/propeller-heads/fynd/tree/main/clients/rust/examples/swap_client_fee.rs)
{% endtab %}
{% endtabs %}


# Benchmarking

Measure solver performance and compare output quality between branches.

Fynd ships with a benchmark tool (`fynd-benchmark`) for load-testing a solver and comparing output quality between two solver instances. Both features live in `tools/benchmark/` and run against live solver instances.

{% hint style="info" %}
**Prerequisite:** You need a running solver before using any benchmark command. See [Quickstart](/get-started/quickstart) for setup instructions.
{% endhint %}

## Load Testing

Measures latency (round-trip, solve time, overhead) and throughput for a single solver instance.

```bash
cargo run -p fynd-benchmark --release -- load [OPTIONS]
```

{% hint style="warning" %}
Always build with `--release`. Debug builds produce misleading latency numbers.
{% endhint %}

### Options

| Flag              | Default                 | Description                              |
| ----------------- | ----------------------- | ---------------------------------------- |
| `--solver-url`    | `http://localhost:3000` | Solver URL to benchmark against          |
| `-n`              | `1`                     | Number of requests to send               |
| `-m`              | `sequential`            | Parallelization mode                     |
| `--requests-file` | *(none)*                | Path to JSON file with request templates |
| `--output-file`   | *(none)*                | Output file for JSON results             |

### Parallelization Modes

Control how requests are dispatched with the `-m` flag:

* **`sequential`** — Send one request at a time, wait for each response before sending the next. Good for measuring single-request latency.
* **`fixed:N`** — Maintain exactly N concurrent in-flight requests (e.g., `fixed:5`). Good for simulating sustained load.
* **`rate:Nms`** — Fire a new request every N milliseconds regardless of pending responses (e.g., `rate:100`). Good for testing behavior under a fixed request rate.

### Examples

```bash
# Measure single-request latency (10 sequential requests)
cargo run -p fynd-benchmark --release -- load -n 10

# Simulate 10 concurrent users sending 100 total requests
cargo run -p fynd-benchmark --release -- load -m fixed:10 -n 100

# Fire a request every 50ms using custom request templates
cargo run -p fynd-benchmark --release -- load \
  -m rate:50 -n 100 \
  --requests-file tools/benchmark/requests_set.json

# Export results to JSON for further analysis
cargo run -p fynd-benchmark --release -- load \
  -m fixed:10 -n 1000 \
  --output-file results.json
```

### Output

The tool prints real-time progress, summary statistics (min, max, mean, median, p95, p99, stddev), and ASCII histograms of timing distributions. Pass `--output-file` to export the full results as JSON.

***

## Comparing Two Solvers

Sends identical quote requests to two running solver instances and compares output quality: amount out (in bps), gas estimates, and route selection.

```bash
cargo run -p fynd-benchmark --release -- compare [OPTIONS]
```

### Setup

You need two Fynd instances running simultaneously — typically from different git branches. Since both share the same binary target directory and metrics port, use **git worktrees** to avoid conflicts.

#### 1. Create a worktree for the baseline

```bash
# From the main repo
git worktree add ../fynd-baseline main
```

#### 2. Start solver A (baseline) in the worktree

```bash
cd ../fynd-baseline
RUST_LOG=info cargo run --release -- serve \
  --protocols uniswap_v2,uniswap_v3 \
  --http-port 3000 \
  --tycho-url <TYCHO_URL> \
  --tycho-api-key <API_KEY>
```

#### 3. Start solver B (your branch) in the original repo

```bash
cd /path/to/fynd
RUST_LOG=info cargo run --release -- serve \
  --protocols uniswap_v2,uniswap_v3 \
  --http-port 3001 \
  --tycho-url <TYCHO_URL> \
  --tycho-api-key <API_KEY>
```

#### 4. Wait for both solvers to be healthy

```bash
curl http://localhost:3000/v1/health
curl http://localhost:3001/v1/health
```

Both should return `{"healthy": true, ...}` before running the comparison.

#### 5. Run the comparison

```bash
cargo run -p fynd-benchmark --release -- compare \
  --url-a http://localhost:3000 \
  --url-b http://localhost:3001 \
  --label-a main \
  --label-b my-branch \
  -n 100
```

### Options

| Flag              | Default                   | Description                            |
| ----------------- | ------------------------- | -------------------------------------- |
| `--url-a`         | `http://localhost:3000`   | Solver A (baseline) URL                |
| `--url-b`         | `http://localhost:3001`   | Solver B (candidate) URL               |
| `--label-a`       | `main`                    | Label for solver A in output           |
| `--label-b`       | `branch`                  | Label for solver B in output           |
| `-n`              | `500`                     | Number of requests to send             |
| `--requests-file` | *(none)*                  | Path to JSON file with custom requests |
| `--output`        | `comparison_results.json` | Path for full results JSON             |
| `--timeout-ms`    | `15000`                   | Per-request timeout in milliseconds    |
| `--seed`          | `42`                      | Random seed for reproducibility        |

### Net-of-Gas Comparison

The compare tool uses the server-computed `amount_out_net_gas` field for net-of-gas output comparison. This value represents the output amount minus gas cost denominated in the output token, calculated by the solver. It works for all token pairs.

### Output

Prints a summary table to stdout showing win/loss counts and bps differences (both gross and net-of-gas). Writes detailed per-request results to the output JSON file. Positive bps diffs mean solver B returned more output than solver A.

***

## CPU Scaling

Measures how solver throughput (req/s) scales with worker thread count. The tool builds a solver in-process for each worker count, runs a load test, shuts down, and repeats.

```bash
cargo run -p fynd-benchmark --release -- scale [OPTIONS]
```

{% hint style="warning" %}
Requires a `worker_pools.toml` with exactly **one** pool defined.
{% endhint %}

### Options

| Flag                    | Default             | Description                                       |
| ----------------------- | ------------------- | ------------------------------------------------- |
| `--base-config`         | `worker_pools.toml` | Single-pool TOML config                           |
| `--worker-counts`       | *(required)*        | Comma-separated worker counts (e.g. `1,2,4,8,16`) |
| `--protocols`           | *(required)*        | Comma-separated protocols for solver              |
| `--tycho-url`           | `localhost:4242`    | Tycho WebSocket URL                               |
| `--tycho-api-key`       | *(none)*            | Tycho API key                                     |
| `--disable-tls`         | `false`             | Disable TLS for Tycho connection                  |
| `--rpc-url`             | *(none)*            | Node RPC URL                                      |
| `--chain`               | `ethereum`          | Chain name                                        |
| `--http-port`           | `3000`              | Solver HTTP port                                  |
| `-n`                    | `100`               | Requests per iteration                            |
| `-m`                    | `fixed:8`           | Parallelization mode                              |
| `--requests-file`       | *(none)*            | Custom request templates                          |
| `--warmup-secs`         | `30`                | Seconds to wait after health before benchmarking  |
| `--health-timeout-secs` | `300`               | Max seconds to wait for solver health             |
| `--output-file`         | *(none)*            | JSON output file                                  |

### Example

```bash
cargo run -p fynd-benchmark --release -- scale \
  --base-config single_pool.toml \
  --worker-counts 1,2,4,8,16 \
  --protocols uniswap_v2,uniswap_v3 \
  --tycho-url wss://tycho.example.com \
  --tycho-api-key $TYCHO_API_KEY \
  -n 200 \
  -m fixed:8 \
  --output-file scale_results.json
```

### Output

The tool prints a summary table showing throughput, latency, and per-worker efficiency at each worker count:

```
=== CPU Scaling Results ===

Pool: most_liquid_2_hops_fast (algorithm: most_liquid)
Requests per run: 200, Mode: fixed:8

 Workers | Throughput (req/s) | Median RT (ms) | P99 RT (ms) | RPS/Worker
---------+--------------------+----------------+-------------+-----------
       1 |               8.50 |             95 |          142 |      8.50
       2 |              16.20 |             50 |           98 |      8.10
       4 |              30.10 |             28 |           65 |      7.53
       8 |              48.90 |             18 |           52 |      6.11
      16 |              62.30 |             15 |           48 |      3.89
```

Pass `--output-file` to export the full results as JSON for further analysis.

***

## Request Data

By default, the load test uses a single WETH→USDC swap and the compare tool samples from a built-in set of 50 real aggregator trades. Both commands accept `--requests-file` to supply custom requests.

### Downloading More Trades

For broader coverage, download a larger set of real aggregator trades (10k):

```bash
cargo run -p fynd-benchmark --release -- download-trades
```

Then use it with either command via `--requests-file aggregator_trades_10k.json`.

### Custom Request Format

The file should be a JSON array of quote request bodies:

```json
[
  {
    "orders": [{
      "token_in": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      "token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "amount": "1000000000000000000000",
      "side": "sell",
      "sender": "0x0000000000000000000000000000000000000001"
    }]
  }
]
```

See `tools/benchmark/requests_set.json` in the repository for a complete example.


# Custom Algorithm

Fynd exposes an `Algorithm` trait that lets you plug in custom routing logic without modifying `fynd-core`. This guide walks through implementing the trait and wiring it into a worker pool.

## The `Algorithm` trait

The trait has four methods:

* `name()` — a string identifier used in config and logs
* `find_best_route()` — given a routing graph and an order, return the best route. Call `Route::validate()` on each candidate and skip invalid ones (disconnected swaps, repeated tokens, malformed splits): the solver worker rejects an invalid route, which drops the whole solution for that worker pool, so prefer the next-best valid route instead
* `computation_requirements()` — declares which derived data the algorithm needs (spot prices, depths, etc.)
* `timeout()` — per-order solve deadline

Your algorithm receives a read-only reference to the routing graph and shared market data. The worker infrastructure handles graph initialisation, event handling, and edge-weight updates.

## Implement the trait

From [`fynd-core/examples/custom_algorithm.rs`](https://github.com/propeller-heads/fynd/tree/main/fynd-core/examples/custom_algorithm.rs):

```rust
/// A naive algorithm that finds a direct component (liquidity pool) between two tokens.
///
/// This iterates through all edges in the routing graph, finds one that
/// connects `token_in` to `token_out`, simulates the swap, and returns
/// the first successful result. It only supports single-hop (direct) routes.
struct DirectComponentAlgorithm {
    timeout: Duration,
}

impl DirectComponentAlgorithm {
    fn new(_config: fynd_core::AlgorithmConfig) -> Self {
        Self { timeout: Duration::from_millis(100) }
    }
}

impl Algorithm for DirectComponentAlgorithm {
    // Reuse the built-in petgraph manager — it handles graph initialization and
    // market event updates automatically. We just need a simple graph with no
    // edge weights (unit `()` type).
    type GraphType = StableDiGraph<()>;
    type GraphManager = PetgraphStableDiGraphManager<()>;

    fn name(&self) -> &str {
        "direct_pool"
    }

    async fn find_best_route(
        &self,
        graph: &Self::GraphType,
        market: MarketData,
        label: Option<StateLabel>,
        _derived: Option<SharedDerivedDataRef>,
        order: &Order,
    ) -> Result<RouteResult, AlgorithmError> {
        let market = match label.as_ref() {
            Some(l) => market
                .read_labeled(l)
                .await
                .map_err(|e| AlgorithmError::Other(e.to_string()))?,
            None => market.read().await,
        };

        let gas_price = market
            .gas_price()
            .ok_or(AlgorithmError::Other("gas price not available".to_string()))?
            .effective_gas_price()
            .clone();

        // Walk every edge looking for one that goes token_in → token_out.
        for edge_idx in graph.edge_indices() {
            let Some((src_idx, dst_idx)) = graph.edge_endpoints(edge_idx) else {
                continue;
            };
            let (src_addr, dst_addr) = (&graph[src_idx], &graph[dst_idx]);

            if src_addr != order.token_in() || dst_addr != order.token_out() {
                continue;
            }

            let component_id = &graph
                .edge_weight(edge_idx)
                .expect("edge exists")
                .component_id;

            // Look up component metadata and simulation state.
            let Some(component) = market.get_component(component_id) else {
                continue;
            };
            let Some(state) = market.get_simulation_state(component_id) else {
                continue;
            };
            let Some(token_in) = market.get_token(order.token_in()) else {
                continue;
            };
            let Some(token_out) = market.get_token(order.token_out()) else {
                continue;
            };

            // Simulate the swap.
            let result = match state.get_amount_out(order.amount().clone(), token_in, token_out) {
                Ok(r) => r,
                Err(_) => continue,
            };

            let swap = Swap::new(
                component_id.clone(),
                component.protocol_system.clone(),
                token_in.address.clone(),
                token_out.address.clone(),
                order.amount().clone(),
                result.amount.clone(),
                result.gas,
                component.clone(),
                state.clone_box(),
            );

            let route = Route::new(vec![swap], FxHashMap::default())?;

            // Validate every candidate route before returning it. The solver worker rejects
            // invalid routes (disconnected swaps, repeated tokens, malformed splits) and that
            // failure drops the whole solution for this worker pool. Skipping invalid candidates
            // here lets a later component be chosen instead. Any custom algorithm should validate
            // the routes it might return, and — when it ranks multiple candidates —
            // fall through to the next-best valid one rather than returning the invalid
            // route.
            if let Err(e) = route.validate() {
                eprintln!("skipping invalid route: {e}");
                continue;
            }

            let net_amount_out = BigInt::from(result.amount);

            return Ok(RouteResult::new(route, net_amount_out, gas_price));
        }

        Err(AlgorithmError::Other(format!(
            "no direct component from {:?} to {:?}",
            order.token_in(),
            order.token_out()
        )))
    }

    fn computation_requirements(&self) -> ComputationRequirements {
        ComputationRequirements::default()
    }

    fn timeout(&self) -> Duration {
        self.timeout
    }
}
```

The example uses `PetgraphStableDiGraphManager<()>` so the worker infrastructure handles graph maintenance automatically. The algorithm walks graph edges to find a pool connecting the two tokens, simulates the swap, and constructs a `Swap` → `Route` → `RouteResult`.

## Wire it up

Pass your algorithm factory to `FyndBuilder::with_algorithm()` instead of the string-based `.algorithm()` method:

```rust
    let solver = FyndBuilder::new(
        Chain::Ethereum,
        tycho_url,
        rpc_url,
        vec!["uniswap_v2".to_string(), "uniswap_v3".to_string()],
        10.0,
    )
    .tycho_api_key(tycho_api_key)
    .with_algorithm("direct_pool", DirectComponentAlgorithm::new)
    .build()?;
```

The factory closure receives an `AlgorithmConfig` (hop limits, timeout) and returns your algorithm instance. `FyndBuilder` handles all the infrastructure: Tycho feed, gas price fetcher, computation manager, and worker pool setup.

## Run the example

### Prerequisites

```bash
export TYCHO_API_KEY="your-api-key"
export RPC_URL="https://your-rpc-provider.com"
```

### Run

```bash
cargo run --package fynd-core --example custom_algorithm
```

The example connects to Tycho, loads market data, and solves a 1000 USDC → WBTC order using `DirectPoolAlgorithm`.

For the complete runnable example, see [`fynd-core/examples/custom_algorithm.rs`](https://github.com/propeller-heads/fynd/tree/main/fynd-core/examples/custom_algorithm.rs).


# Overview

Fynd ships four built-in routing algorithms. Most Liquid and Bellman-Ford each return a single route. Path Frank-Wolfe and [Water-fill](/algorithms/water-fill) split one order across several parallel routes to cut price impact on large trades. This section explains the routing problem and how each algorithm works.

## The routing problem

A DEX aggregator receives a request like "swap 1 ETH for USDC" and must find the best path through a network of on-chain liquidity pools. This is a graph problem: tokens are nodes, pools are edges, and the goal is to find the path that maximizes output.

Three properties make this harder than classical shortest-path routing:

1. **Edge weights are functions, not constants.** The output of a pool depends on the input amount (price impact). A pool that gives a great rate for 0.1 ETH may give a terrible rate for 100 ETH. You cannot precompute weights once and reuse them.
2. **Weights are multiplicative, not additive.** Exchange rates multiply along a path. Shortest-path algorithms like Dijkstra assume additive costs.
3. **The best route depends on trade size.** A pool with deep liquidity wins for large trades; a shallow pool with a better spot price wins for small ones. There is no single "best route" independent of the amount.

These properties rule out off-the-shelf graph algorithms that rely on precomputed, additive, size-independent edge weights. Fynd's algorithms handle this by simulating the actual swap math at the points where it matters.

## How Fynd uses algorithms

Each algorithm runs inside a **worker pool**: a group of dedicated OS threads that process quote requests. Multiple worker pools can run in parallel with different algorithms and configurations (e.g., a fast 2-hop Most Liquid pool alongside a deeper 5-hop Bellman-Ford pool). The **WorkerPoolRouter** fans out each request to all pools and returns the best result.

This competition design means algorithms don't need to be perfect in isolation. A fast heuristic algorithm can win on common pairs while a thorough algorithm catches the routes the heuristic misses.

The simulation-heavy algorithms (Path Frank-Wolfe and Water-fill) simulate many candidate swaps per request, and that cost scales with per-pool simulation time — higher for VM-simulated protocols than for native ones. Run them in a worker pool alongside a Bellman-Ford pool: the WorkerPoolRouter returns whichever pool answers best within the timeout, so the fast baseline still covers a request even when the heavier pool is slow on VM-heavy routes.

See [Architecture](/reference/architecture) for the full system design and [Custom Algorithm](/guides/custom-algorithm) for how to plug in your own.

## Built-in algorithms

|                        | [Most Liquid](/algorithms/most-liquid)                              | [Bellman-Ford](/algorithms/bellman-ford)           | [Path Frank-Wolfe](/algorithms/path-frank-wolfe)                      | [Water-fill](/algorithms/water-fill)                                                                                                                                                               |
| ---------------------- | ------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Approach**           | Enumerate paths, score by heuristic, simulate top-N                 | Simulate every reachable edge, keep best amounts   | Bellman-Ford path discovery plus Frank-Wolfe split optimization       | Tries the best single path and three ways to split the order, then returns whichever gives the most output net of gas                                                                              |
| **Strengths**          | Fast; good at common, high-liquidity pairs                          | Finds non-obvious routes; no heuristic blind spots | Reduces price impact by splitting flow across parallel paths          | Never returns less than the best single path; handles every order, so it can run as the only pool; captures the gains on large trades                                                              |
| **Weaknesses**         | Path count explodes at high hop counts; heuristic can misjudge      | Single path only; suboptimal for large trades      | More simulation work per request; overkill for small trades           | Simulates every candidate on every order, so it is slower than the single-route finders (offline 10k p50 \~15 ms) and slower still on VM-simulated protocols; run it alongside a Bellman-Ford pool |
| **Default config**     | *(not in default `worker_pools.toml`)*                              | 2 hops, 3 workers (see `worker_pools.toml`)        | *(not in default `worker_pools.toml`)*                                | *(not in default `worker_pools.toml`)*                                                                                                                                                             |
| **Derived data needs** | Spot prices + pool depths (scoring), token gas prices (gas ranking) | Token gas prices (optional, for gas-aware mode)    | Token gas prices + spot prices (price impact, probe amount, gas cost) | Spot prices + pool depths (candidate ranking), token gas prices (optional, gas-aware net)                                                                                                          |


# Most Liquid

The Most Liquid algorithm finds swap routes by enumerating candidate paths, scoring them with a cheap heuristic, then simulating only the most promising ones. It trades completeness for speed: it won't evaluate every possible route, but it finds good routes fast.

## Overview

The algorithm runs in four stages:

1. **Enumerate** all simple paths up to `max_hops` using BFS
2. **Score and sort** paths by a heuristic (spot price and liquidity depth)
3. **Simulate** the top-N paths using actual pool math
4. **Rank** by net output after gas cost deduction

The key insight is that stages 1-2 are cheap (graph traversal and arithmetic), while stage 3 is expensive (full AMM simulation per hop). The heuristic in stage 2 acts as a filter, ensuring simulation budget is spent on paths most likely to win.

## Stage 1: Path enumeration

Starting from the source token, BFS explores all outgoing edges up to `max_hops` depth. At each step it follows every edge (including parallel edges between the same token pair from different pools), building complete paths from source to destination.

The result is a list of all simple paths (no repeated tokens) from source to destination within the hop limit.

## Stage 2: Heuristic scoring

Each path is scored without simulation using two derived data values per edge:

* **Spot price**: the marginal exchange rate at zero trade size (includes pool fees)
* **Depth**: the pool's available liquidity in USD terms

The score for a path is:

```
score = (product of spot prices along the route) × min(depth along the route)
```

The spot price product estimates the exchange rate. The minimum depth acts as a bottleneck indicator: a path is only as liquid as its shallowest pool. Paths through deep, well-priced pools score highest.

This scoring is approximate. It ignores price impact (the spot price assumes infinitesimal trade size) and doesn't account for how liquidity changes after each hop. But it's fast and good enough to rank tens of thousands of candidates so the expensive simulation stage focuses on the right ones.

Paths are sorted by score descending. If `max_routes` is configured, only the top-N proceed to simulation.

## Stage 3: Simulation

Each surviving path is simulated end-to-end. For every hop, the algorithm calls `get_amount_out()` on the actual pool state with the running amount from the previous hop. This accounts for:

* Price impact at the exact trade size
* The pool's fee structure
* Tick crossings (Uniswap V3) or other non-linear mechanics
* Reserve state as of the latest block

If a simulation fails (e.g., insufficient liquidity in a pool), the path is discarded. Otherwise, the final output amount is recorded.

## Stage 4: Gas-adjusted ranking

Each simulated path's output is adjusted for gas cost:

```
net_output = gross_output - (total_gas * gas_price * token_price_ratio)
```

Where `total_gas` is the sum of gas estimates for each swap in the route, `gas_price` is the current block's gas price, and `token_price_ratio` converts the gas cost (in the native token) to the output token.

The path with the highest `net_output` wins.

> **Note:** `route.total_gas()` is a fast, approximate estimate used for ranking paths *within* this algorithm. When multiple worker pools compete, the `WorkerPoolRouter` applies a more accurate gas estimate (`estimate_gas_usage` from tycho-execution, which accounts for token transfers and router overhead) before the final cross-pool ranking.

## When it works well

* **Common pairs** (WETH/USDC, WETH/WBTC): a few high-liquidity pools dominate, and the heuristic reliably ranks them correctly.
* **Low hop counts** (2-3): the path space is small enough to enumerate exhaustively, so the heuristic filter drops very little.
* **High-frequency quoting**: the algorithm is fast enough to serve latency-sensitive integrators.

## When it struggles

* **High hop counts** (4+): the number of candidate paths grows exponentially. Even with `max_routes` capping simulation, the heuristic may not surface the best path.
* **Exotic pairs**: tokens with thin liquidity often have non-obvious routes where the spot price heuristic misjudges the actual output. The Bellman-Ford algorithm, which simulates every edge without a heuristic filter, handles these better.

## Suggested configuration

```toml
[pools.most_liquid_3_hops]
algorithm = "most_liquid"
num_workers = 3
task_queue_capacity = 1000
max_hops = 3
timeout_ms = 500
max_routes = 50
```

* `max_hops = 3` keeps enumeration tractable. At 4+ hops the candidate path count explodes and the heuristic filter starts dropping good routes.
* `max_routes = 50` caps the simulation stage. Without it every enumerated path is simulated, which is what makes high hop counts expensive.
* `timeout_ms = 500` leaves headroom for the simulation stage on VM-simulated protocols. Lower it if you run this pool purely as a latency-sensitive baseline.

{% hint style="warning" %}
**Set connector tokens.** With `connector_tokens` unset, routes can pass through illiquid long-tail intermediates, which raises reversion risk. Restrict intermediate hops to a small trusted set — generate one for your chain with `fynd derive-connector-tokens --chain Ethereum --top-n 10 --output toml` and paste it into the pool. See [Connector tokens](/guides/server-configuration#connector-tokens).
{% endhint %}

## Source reference

| File                                     | Purpose                                        |
| ---------------------------------------- | ---------------------------------------------- |
| `fynd-core/src/algorithm/most_liquid.rs` | Algorithm implementation                       |
| `fynd-core/src/algorithm/mod.rs`         | `Algorithm` trait definition                   |
| `fynd-core/src/graph/petgraph.rs`        | Graph implementation (petgraph::StableDiGraph) |
| `fynd-core/src/worker_pool/registry.rs`  | Maps `"most_liquid"` to `MostLiquidAlgorithm`  |
| `worker_pools.toml`                      | Worker pool configuration                      |


# Bellman-Ford

This document explains how Fynd's Bellman-Ford routing algorithm finds the best swap route through a network of decentralized exchange pools.

## The algorithm

The algorithm maintains one number per token: the best amount of that token reachable from the source via any path found so far. It improves these numbers by repeatedly simulating swaps through pools, keeping only improvements. This process is called **relaxation**.

### What the algorithm tracks

Two arrays, both indexed by token:

* **`amount[token]`**: the best amount of this token reachable from the source. Initialized to `order_amount` for the source token, 0 for everything else.
* **`predecessor[token]`**: which token and pool led to this best amount. Used to reconstruct the path at the end.

For a trade starting from WETH, partway through execution the array might look like:

```
amount[WETH]  = 1000000000000000000   (1 ETH, the input)
amount[USDC]  = 2015000000            (best USDC reachable: ~2015)
amount[DAI]   = 2008000000000000000   (best DAI reachable: ~2008)
amount[WBTC]  = 6120000              (best WBTC reachable: ~0.0612)
amount[LINK]  = 0                     (no path found yet)
```

Each token competes only with itself. The algorithm doesn't compare a path to USDC against a path to DAI. It compares paths that end at the same token, keeping only the best one.

A consequence: a single run finds the best route from the source to *every* reachable token, not just the destination. If multiple orders share the same source token and amount in the same block, you could serve them all from a single run. The current code doesn't exploit this (each order triggers a fresh run), but the structure supports it.

### Relaxation

For each edge (pool connecting two tokens), relaxation calls `get_amount_out()`, which runs the actual pool math for the exact input amount at the current pool state:

```
new_amount = pool.get_amount_out(amount[u], token_u, token_v)
if new_amount > amount[v]:
    amount[v] = new_amount
    predecessor[v] = (u, pool)
```

This simulation accounts for the pool's reserves, its fee structure, and price impact at the exact trade size. A Uniswap V2 pool runs the constant product formula. A Uniswap V3 pool steps through price ticks. The simulation returns the precise output for that specific input.

This is the core insight: instead of precomputing approximate edge weights and searching over them, we simulate the actual swap at every step. The search and the evaluation happen together.

When the algorithm relaxes an edge from USDC to DAI, it asks: "starting from the best USDC amount I've found (2015), swapping through this USDC/DAI pool, do I get more DAI than the 2008 I already have?" If yes, update. If no, move on.

### The active set

Relaxation is expensive (each call runs real AMM math), so we avoid wasting simulation calls on tokens that haven't improved. The algorithm maintains an **active set**: the set of tokens whose amount improved in the previous round. Only outgoing edges from active tokens are relaxed.

```
amount[source] = order_amount
active = {source}

for round = 1 to max_hops:
    next_active = {}
    for each node u in active:
        for each outgoing edge (u, v) through pool P:
            out = P.get_amount_out(amount[u], token_u, token_v)
            if out > amount[v]:
                amount[v] = out
                predecessor[v] = (u, P)
                next_active.add(v)
    active = next_active
```

Round 1 starts with just the source. Its active set is the source's direct neighbors (tokens reachable in one hop). Round 2's active set is the neighbors that actually improved. The active set expands outward like a wavefront, but only through nodes where something changed.

If a token's amount didn't improve, its outgoing edges can't produce new results either: same input, same pool states, same outputs. Only tokens that received a higher amount can propagate improvements.

The full graph might have 2,400 tokens. At round 2, perhaps 50 are active. The algorithm processes 50 nodes' outgoing edges instead of 2,400. With 5 rounds, the savings compound: thousands of simulation calls are avoided.

### Gas-aware relaxation

The basic relaxation above compares gross output amounts. But a 5-hop route with slightly more gross output can lose to a 3-hop route after gas costs. To handle this, relaxation optionally compares **net** amounts: gross output minus cumulative gas cost, converted to the output token.

At each node, the algorithm tracks `cumul_gas[v]`: the total gas units along the best path to v. When comparing a candidate path against the current best, it computes:

```
net_candidate = gross_output - cumul_gas_candidate * gas_price * token_price[v]
net_existing  = amount[v]    - cumul_gas[v]         * gas_price * token_price[v]
if net_candidate > net_existing: update
```

`token_price[v]` converts gas cost (in wei) to the token at node v. The algorithm resolves this from derived data when available, or falls back to a cumulative spot price product along the path (multiplying the spot prices of each pool traversed). If neither gas price nor token prices are available, it falls back to gross comparison automatically.

This is configurable via the `gas_aware` setting in the algorithm config (defaults to true).

## Forbid revisits

The algorithm above has a problem. Consider this graph:

```
ETH -[pool A]-> USDC -[pool B]-> ETH -[pool C]-> DAI
```

If pool B gives a great rate for USDC-to-ETH, the algorithm might find that going ETH -> USDC -> ETH -> DAI gives more DAI than going ETH -> DAI directly. The route visits ETH twice, passing through a USDC/ETH roundtrip in the middle.

This route looks profitable on paper, but it will fail in practice. Here's why.

### Token revisit implies arbitrage

If the path goes ETH -> USDC -> ETH, the USDC -> ETH leg is getting more ETH back than you started with (otherwise the roundtrip would lose money and the algorithm wouldn't choose it). That means there's a price discrepancy between pool A and pool B for the ETH/USDC pair. This is an **arbitrage opportunity**.

Arbitrageurs monitor these discrepancies with specialized infrastructure. They will execute the ETH -> USDC -> ETH cycle themselves, pocketing the profit. Their transaction will adjust the pool reserves, eliminating the discrepancy. By the time our transaction executes, the pools will have moved, and the route will no longer produce the expected output.

Building a route that depends on an arbitrage opportunity is building on sand.

### The fix: check before relaxing

Before each relaxation, we walk the predecessor chain backward from the current node to the source and check two things:

1. **Token check**: does the destination token already appear in the path? If yes, skip this edge.
2. **Pool check**: has this pool already been used in the path? If yes, skip. For two-token pools this is redundant with the token check, but multi-token pools (e.g., Balancer weighted pools or Curve tri-pools) connect more than two tokens. Without the pool check, the algorithm could route through the same pool twice on different token pairs, which would produce incorrect results because the pool state changes after the first swap.

```
for each edge (u, v) through pool P:
    if v's token is already in the path to u: skip
    if P is already used in the path to u: skip
    // ... proceed with simulation and relaxation
```

Each check walks the predecessor chain, which has at most `max_hops` entries (typically 5). The cost is negligible compared to the simulation call that follows.

With these constraints, every path the algorithm considers visits each token at most once and uses each pool at most once. The route is a simple chain through distinct pools, which is exactly what we want for execution.

## Subgraph extraction

Before relaxation even starts, we prune the graph.

On Ethereum, the full token graph has \~2,400 tokens and \~10,000 directed edges. For a trade from ETH to USDC with a 3-hop budget, most of these are irrelevant. Tokens that are 4 hops away from ETH can't appear in any 3-hop route.

**BFS from the source**: starting from `token_in`, we run breadth-first search following outgoing edges, stopping at depth `max_hops`. Only edges encountered during this BFS are kept. Everything else is discarded.

The result is a subgraph of a few hundred edges, down from 10,000. All subsequent work (building the adjacency list, running relaxation, walking predecessor chains) operates on this smaller graph.

This complements the active set. BFS removes structurally unreachable nodes before relaxation. The active set skips nodes that are structurally reachable but haven't received tokens yet. Together, they keep the number of simulation calls affordable.

## The complete algorithm

Here is the full sequence, end to end. Each step maps to a specific section of `fynd-core/src/algorithm/bellman_ford.rs`.

### Step 1: Setup

Validate that the order is a sell order (exact input amount, find best output). Look up the source and destination tokens in the graph. Acquire a read lock on the shared market data (pool states update every block), snapshot what we need, and release the lock. All subsequent steps are lock-free.

### Step 2: Subgraph extraction

BFS from the source token up to `max_hops` depth. Collect all reachable edges. Build a token map (node index to token metadata) and extract a market data subset containing only the relevant pools.

### Step 3: Initialize

Set `amount[source] = order_amount`. Build an adjacency list from the subgraph edges. Seed the active set with the source node.

### Step 4: Relaxation

For each round up to `max_hops`:

* Check the timeout. If exceeded, stop with whatever we have.
* If the active set is empty, stop early (no more improvements possible).
* For each active node u, for each outgoing edge (u, v) through pool P:
  * Check if v's token already appears in the path (predecessor walk). Skip if yes.
  * Check if pool P already appears in the path. Skip if yes.
  * Call `P.get_amount_out(amount[u], token_u, token_v)`.
  * If the result exceeds `amount[v]`, update and add v to the next active set.

### Step 5: Check destination

If `amount[destination]` is still zero, no path was found. Return an error.

### Step 6: Reconstruct path

Walk the predecessor array backward from the destination to the source. At each node, `predecessor[node]` tells us which node and pool led here. Collect these into a list of (from, to, pool) edges and reverse to get forward order.

### Step 7: Re-simulate

Run the reconstructed path forward, calling `get_amount_out()` at each hop with the actual running amount. This produces the authoritative output and the `Swap` structs needed for on-chain execution.

### Step 8: Gas adjustment

Compute the total gas cost of the route (sum of each swap's gas estimate `route.total_gas()`), multiply by the current gas price, convert to the output token using price ratios, and subtract from the gross output. The result is `net_amount_out`: the output after accounting for execution cost.

Return the route and net amount.

> **Note:** `route.total_gas()` is a fast, approximate estimate used for ranking paths *within* this algorithm. When multiple worker pools compete, the `WorkerPoolRouter` applies a more accurate gas estimate (`estimate_gas_usage` from tycho-execution, which accounts for token transfers and router overhead) before the final cross-pool ranking.

## A worked example

Let's trace the algorithm on a small graph.

**Trade**: sell 1,000 units of token A for token D. **Max hops**: 3.

**Graph** (each pool has a simplified exchange rate for illustration):

```
         [pool1: 1 in -> 2 out]         [pool3: 1 in -> 3 out]
    A -----------------> B -----------------> D
    |                    |
    |   [pool2: 1 -> 5]  |   [pool4: 1 -> 0.5]
    +---------> C --------+
                |
                | [pool5: 1 -> 4]
                +-----------------------------> D
```

Edges (directed, with output per unit of input):

* pool1: A->B, 2 out per 1 in
* pool2: A->C, 5 out per 1 in
* pool3: B->D, 3 out per 1 in
* pool4: C->B, 0.5 out per 1 in
* pool5: C->D, 4 out per 1 in

### Step 2: Subgraph

BFS from A with depth 3 reaches all nodes and all edges. The full graph is the subgraph.

### Step 3: Initialize

```
amount = { A: 1000, B: 0, C: 0, D: 0 }
predecessor = { A: none, B: none, C: none, D: none }
active = { A }
```

### Step 4, Round 1

Process A's outgoing edges:

* **A -> B via pool1**: `get_amount_out(1000) = 2000`. `2000 > 0`, so update. `amount[B] = 2000`, `predecessor[B] = (A, pool1)`, add B to next\_active.
* **A -> C via pool2**: `get_amount_out(1000) = 5000`. `5000 > 0`, so update. `amount[C] = 5000`, `predecessor[C] = (A, pool2)`, add C to next\_active.

```
amount = { A: 1000, B: 2000, C: 5000, D: 0 }
active = { B, C }
```

### Step 4, Round 2

Process B's outgoing edges:

* **B -> D via pool3**: `get_amount_out(2000) = 6000`. `6000 > 0`, so update. `amount[D] = 6000`, `predecessor[D] = (B, pool3)`, add D to next\_active.

Process C's outgoing edges:

* **C -> B via pool4**: Before simulating, check: is B's token already in the path to C? Path to C is: A -> C. B is not in it. Is pool4 already used? No. Proceed. `get_amount_out(5000) = 2500`. `2500 > 2000` (current B), so update. `amount[B] = 2500`, `predecessor[B] = (C, pool4)`, add B to next\_active.
* **C -> D via pool5**: `get_amount_out(5000) = 20000`. `20000 > 6000`, so update. `amount[D] = 20000`, `predecessor[D] = (C, pool5)`, add D to next\_active.

```
amount = { A: 1000, B: 2500, C: 5000, D: 20000 }
active = { B, D }
```

### Step 4, Round 3

Process B's outgoing edges (B was re-activated because its amount improved):

* **B -> D via pool3**: `get_amount_out(2500) = 7500`. `7500 < 20000`. No update.

Process D's outgoing edges: D has no outgoing edges in this graph.

```
amount = { A: 1000, B: 2500, C: 5000, D: 20000 }
active = {}  (nothing improved)
```

### Step 6: Reconstruct

Start at D. `predecessor[D] = (C, pool5)`. Move to C. `predecessor[C] = (A, pool2)`. Move to A. That's the source. Done.

Path (reversed): **A -\[pool2]-> C -\[pool5]-> D**

### Step 7: Re-simulate

* A -> C via pool2: `get_amount_out(1000) = 5000`
* C -> D via pool5: `get_amount_out(5000) = 20000`

Final output: **20,000 units of D**.

Note that the algorithm also explored A -> B -> D (6,000) and A -> C -> B -> D (7,500) but found A -> C -> D (20,000) to be the best.

## Design tradeoffs

### Gas-aware vs. gross relaxation

With `gas_aware` enabled (the default), the algorithm compares net amounts during relaxation, steering path selection toward routes with better value after gas deduction. This requires token prices and a gas price from derived data; when either is missing, it falls back to gross comparison transparently.

The gas cost conversion uses two strategies: a direct lookup in the derived token-gas-price table (primary), or a cumulative spot price product along the path (fallback for tokens not in the price table). The fallback multiplies spot prices hop by hop, so it degrades in accuracy for long paths, but it extends coverage to tokens that lack a direct WETH price.

The improvement is most visible on routes where a cheap 2-hop path beats an expensive 4-hop path with marginally higher gross output.

### Forward-only BFS vs. bidirectional

Subgraph extraction uses forward BFS from the source, not bidirectional BFS (forward from source, backward from destination). Bidirectional BFS produces a tighter subgraph (edges must lie on a viable source-to-destination path), but forward-only is simpler and the active set mechanism already avoids processing nodes that don't lead anywhere useful.

## FAQ

### Why not Dijkstra or A\*?

Three properties of DEX pools break the assumptions these algorithms rely on:

1. **Edge weights are functions, not constants.** The output of a pool depends on how much you push through it (price impact). You cannot precompute weights once and reuse them.
2. **Weights are multiplicative, not additive.** Exchange rates multiply along a path; shortest-path algorithms add. (The log-transform trick exists but doesn't handle price impact.)
3. **The best route depends on trade size.** A pool with deep liquidity wins for big trades; a shallow pool wins for small ones. No single "best route" exists independently of the amount.

Our algorithm handles all three by simulating the actual swap at each step instead of operating on precomputed weights.

## Suggested configuration

```toml
[pools.bellman_ford_3_hops]
algorithm = "bellman_ford"
num_workers = 3
task_queue_capacity = 1000
max_hops = 3
timeout_ms = 500
```

* `max_hops = 3` is the sweet spot. The subgraph and active set keep the simulation count affordable at 3 hops; beyond that the round count grows the active set faster than the pruning shrinks it. The shipped default in `worker_pools.toml` is 2 hops — raise it to 3 once you have connector tokens set, since the allowlist bounds the extra breadth.
* `timeout_ms = 500` gives relaxation room to finish on VM-simulated protocols. The algorithm returns its best-so-far result on timeout, so a tighter budget degrades quality rather than failing.
* `gas_aware` (default `true`) is only reachable through the builder API, not `worker_pools.toml`.

{% hint style="warning" %}
**Set connector tokens.** With `connector_tokens` unset, routes can pass through illiquid long-tail intermediates, which raises reversion risk. Restrict intermediate hops to a small trusted set — generate one for your chain with `fynd derive-connector-tokens --chain Ethereum --top-n 10 --output toml` and paste it into the pool. See [Connector tokens](/guides/server-configuration#connector-tokens).
{% endhint %}

## Acknowledgements

The Bellman-Ford routing approach in Fynd was inspired by the work of [János Tapolcai](http://lendulet.tmit.bme.hu/tapolcai) ([@jtapolcai](https://twitter.com/jtapolcai)), Full Professor at the Department of Telecommunications and Artificial Intelligence, Budapest University of Technology and Economics (BME). His [tycho-searcher](https://github.com/jtapolcai/tycho-searcher) project demonstrates a modified Bellman-Ford algorithm for DEX arbitrage detection built on Tycho.

## Source reference

| File                                      | Purpose                                         |
| ----------------------------------------- | ----------------------------------------------- |
| `fynd-core/src/algorithm/bellman_ford.rs` | Algorithm implementation                        |
| `fynd-core/src/algorithm/mod.rs`          | `Algorithm` trait definition                    |
| `fynd-core/src/graph/petgraph.rs`         | Graph implementation (petgraph::StableDiGraph)  |
| `fynd-core/src/worker_pool/registry.rs`   | Maps `"bellman_ford"` to `BellmanFordAlgorithm` |
| `worker_pools.toml`                       | Worker pool configuration                       |


# Path Frank-Wolfe

The Path Frank-Wolfe algorithm extends Bellman-Ford with **split routing**: instead of sending the entire input through a single path, it discovers multiple candidate paths and optimally distributes the input across them. For large trades where price impact is a binding constraint, splitting across parallel paths can produce meaningfully better output than any single route.

## Overview

The algorithm runs in three stages:

1. **Initial route**: Run Bellman-Ford at full input to find the best single path
2. **Frank-Wolfe loop**: Iteratively discover additional paths and shift flow toward them
3. **Final comparison**: Return the split route if it beats the single-path result net of gas

The key insight is that price impact is the enemy. A constant-product pool that returns a good rate on 100 USDC returns a progressively worse rate as you push more through it. By splitting the same total input across multiple pools, each pool sees a smaller amount and operates at a better rate.

Two properties hold throughout the optimization:

* **Honest shared-pool accounting**: whenever a set of paths is evaluated, they are simulated sequentially against a shared post-swap state map. A path that reuses a pool another path already touched sees the depleted reserves, so a split can never double-count the same liquidity. Reported route amounts therefore correspond to what the route delivers when executed.
* **Never lose the single path**: the single-path route is a floor. Every candidate split must beat it net of gas, and any failure inside the split search (missing derived data, invalid split route) falls back to the single-path result instead of failing the solve.

## When does splitting help?

The algorithm computes a **price impact estimate** before attempting any split. If price impact is negligible relative to gas costs, splitting can't pay for itself — extra swaps cost gas, and the marginal gain from reducing impact is too small. The algorithm skips the Frank-Wolfe loop and returns the single-path result directly.

Splitting is most valuable when:

* **Large trades relative to pool depth**: A trade of 10% of a pool's reserves has meaningful price impact; 0.01% does not.
* **Multiple parallel pools exist**: Splitting only helps when there are alternative paths to absorb the extra flow.
* **Multi-hop paths share entry pools**: Paths that start differently but converge on the same intermediate token can be split at the entry, reducing impact on all shared segments.

## Stage 1: Initial route

Bellman-Ford (BF) runs at the full order amount to find the best single-path route. This serves two purposes: it gives a quality baseline for the final comparison, and it provides an initial allocation — 100% of flow on one path — from which the Frank-Wolfe loop starts.

## Stage 2: Frank-Wolfe loop

### Probe amount

Before each iteration, the algorithm computes a **probe amount**: the minimum trade size where an additional path would pay for its gas cost. This is `gas_cost / price_impact`. If price impact has fallen enough (because prior splits already reduced it), the probe exceeds the configured `max_probe` cap and the loop stops.

### Finding a candidate path

To find the next candidate path, the algorithm constructs a **post-swap market state** that reflects the current allocation:

1. Simulate all current path allocations through their respective pools
2. Store the post-swap pool states as overrides
3. Zero out gas costs for pools already committed in the current solution (they are already executed once on-chain; their gas is already priced in)

Bellman-Ford then runs on this degraded state at `probe_amount`. It finds the best path *given that prior allocations have already moved the committed pools*. If a previously-committed pool has absorbed so much flow that its rate degraded, BF naturally routes around it.

### Duplicate detection

If BF returns the same path that already exists in the allocation (same sequence of `(pool, token_in, token_out)` triples), exploration is exhausted and the loop stops. Paths that share only a prefix but diverge at a later hop are not duplicates — shared prefixes are handled by the route builder, not by splitting.

### Line search

Once a candidate path is found, the algorithm uses **golden-section search** to find the optimal `step_size ∈ [0, 1]`: the fraction of total flow to shift from the existing allocation to the new candidate. At each probe point, the algorithm re-simulates all paths **sequentially against a shared post-swap state map** — later paths see the reserves earlier paths consumed — and scores the result by **net output**: gross output minus the trial's total gas converted into output-token terms. The step size that maximises net output is selected.

This line search is the Frank-Wolfe "descent direction" computation. It runs `line_search_evals` function evaluations (default: 12), which is enough for \~4-5 decimal digits of precision.

### Gas-aware activation

A path's gas cost is constant once it carries any flow, so the line search alone cannot reject a candidate whose extra output never covers its extra gas — it just finds the least-bad step. The activation check handles this: the net output at the chosen step is compared against the net output at step 0, where the candidate carries no flow and no gas. The candidate is only activated when the split genuinely nets more than the current allocation. This mirrors the water-fill activation rule from the split-routing research: a path must pay for its own gas before it gets flow.

### Applying the step

The chosen step size is applied:

* All existing path fractions are scaled by `(1 - step_size)`
* The candidate is added at `step_size`
* Paths whose fraction falls below `min_split` are dropped and the remaining fractions are renormalized
* All paths are re-simulated at their new allocations, sequentially against a shared post-swap state map, to refresh amounts and per-hop outputs. Paths that share a pool (including a pool reused by a later hop of the same path) are priced on depleted reserves, so the recorded per-hop amounts match executable reality

The loop then repeats with the updated allocation as the new starting point.

## Stage 3: Final comparison

After the loop completes (due to `max_paths`, timeout, price impact exit, or duplicate detection), the algorithm builds the full split route, validates it, and compares it against the initial single-path result by **net amount out** (gross output minus gas cost). The better result is returned.

If only one path survived (because all splits were too small), the initial single-path result is returned directly without building a split route.

The entire split search runs behind a fallback boundary: if any step of it errors — a pool missing derived data, a malformed split route, a failed validation — the algorithm logs the failure and returns the single-path result. A solvable order is never failed by the split optimization.

## Shared pools and route assembly

Paths can share intermediate pools. For example:

```
Path 1: WETH → [P1] → USDC → [P2] → DAI
Path 2: WETH → [P1] → USDC → [P3] → DAI
```

Both paths use pool P1. On-chain, P1 is called once with the combined WETH input; P2 and P3 each receive a fraction of the resulting USDC. The route builder:

1. **Merges shared hops**: identifies hops with the same `(pool, token_in, token_out)` across all paths and combines them into a single swap
2. **Assigns split fractions**: sorts within each branch by flow fraction (largest first); the last swap in each branch receives `split = 0.0` (the TychoRouter "use remaining balance" convention)
3. **Topological ordering** (Kahn's algorithm): swaps are emitted only after all upstream swaps producing their input token are done. This is necessary when paths of different lengths converge on the same intermediate token — the shared downstream pool must wait for all inflows to complete before its swap is emitted

Gas for shared pools is counted once, not once per path.

## Design tradeoffs

### Versus Bellman-Ford

Bellman-Ford finds the best **single path**. PathFrankWolfe wraps BF and adds a split optimization layer on top. For trades with negligible price impact, PFW produces the same result as BF. For large trades with meaningful impact, PFW can produce better net output by spreading flow across multiple pools.

The cost is additional simulation work: each Frank-Wolfe iteration runs one BF solve plus `line_search_evals` evaluations of the total-output function. With `max_paths = 4` and `line_search_evals = 12`, the worst case is roughly 3 BF solves and \~36 path simulations on top of the initial BF run.

### Single-path fallback

The algorithm always compares the split result against the single-path baseline before returning. If the split route doesn't beat the single path net of gas (this can happen when the gas overhead of extra swaps outweighs the reduced impact), the single-path result wins. Errors during the split search degrade to the single-path result the same way, so split optimization can only add output, never cost coverage.

### Sequential evaluation versus independent evaluation

An earlier version of the evaluator simulated every path from the original pool states. For pool-disjoint splits the two are identical, but for splits that reuse a pool, independent simulation counts the same liquidity twice and overstates output — the route then under-delivers when executed. Sequential evaluation prices each path on the state the previous paths left behind, which matches how the merged route executes on-chain (a shared hop becomes one combined swap). The optimizer's objective, the recorded per-hop amounts, and the reported net all use the sequential semantics.

### Timeout safety

The Frank-Wolfe loop checks elapsed time at the start of each iteration. If the timeout is exceeded, the loop stops and the algorithm proceeds with however many paths it has found. The result is always valid — just potentially less optimal than a full-budget run.

## Suggested configuration

```toml
[pools.path_frank_wolfe_3_hops]
algorithm = "path_frank_wolfe"
num_workers = 3
task_queue_capacity = 1000
max_hops = 3
timeout_ms = 1000
```

* `timeout_ms = 1000` is roughly double a single-path pool's budget, because each Frank-Wolfe iteration runs a full Bellman-Ford solve plus a line search on top of the initial solve. On timeout the loop stops and the algorithm returns the paths it has, so a tight budget silently reduces the number of splits it can find.
* `max_hops = 3` bounds the inner Bellman-Ford solve, which runs once per iteration — hop cost is multiplied here, not paid once.
* Run this pool **alongside** a Bellman-Ford or Most Liquid pool. PFW does more simulation work per request, and the WorkerPoolRouter returns whichever pool answers best within the timeout, so the cheap pool still covers requests where PFW is slow on VM-heavy routes.

{% hint style="warning" %}
**Set connector tokens.** With `connector_tokens` unset, routes can pass through illiquid long-tail intermediates, which raises reversion risk — and a split route multiplies that exposure across every path it activates. Restrict intermediate hops to a small trusted set — generate one for your chain with `fynd derive-connector-tokens --chain Ethereum --top-n 10 --output toml` and paste it into the pool. See [Connector tokens](/guides/server-configuration#connector-tokens).
{% endhint %}

The PFW-specific tuning parameters are not currently exposed in `worker_pools.toml`; they use defaults:

| Parameter           | Default | Description                                                    |
| ------------------- | ------- | -------------------------------------------------------------- |
| `max_paths`         | 4       | Maximum number of distinct paths to split across               |
| `max_probe`         | 25%     | Probe amount cap as a fraction of total input                  |
| `min_split`         | 5%      | Minimum flow fraction for any path; smaller shares are dropped |
| `line_search_evals` | 12      | Golden-section evaluations per step size search                |

## Source reference

| File                                          | Purpose                                                             |
| --------------------------------------------- | ------------------------------------------------------------------- |
| `fynd-core/src/algorithm/path_frank_wolfe.rs` | Algorithm implementation and Frank-Wolfe loop                       |
| `fynd-core/src/algorithm/split_primitives.rs` | Shared primitives: path simulation, route assembly, line search     |
| `fynd-core/src/algorithm/bellman_ford.rs`     | Inner BF solver used for path discovery                             |
| `fynd-core/src/algorithm/mod.rs`              | `Algorithm` trait definition                                        |
| `fynd-core/src/worker_pool/registry.rs`       | Maps `"path_frank_wolfe"` to `PathFrankWolfeAlgorithm`              |
| `worker_pools.toml`                           | Worker pool configuration (add a `path_frank_wolfe` pool to enable) |


# Water-fill

`water_fill` splits one order across several parallel routes to reduce price impact on large trades. It tries a few ways to split the order, simulates each, and returns whichever gives the most output after gas — and never less than the best single route.

It runs on the `DepthAndPrice` weighted graph (like Most Liquid). It has no hard derived-data requirement: it uses token gas prices when they are available but tolerates stale ones and never waits on them. With gas prices, ranking is gas-aware — it subtracts each path's activation cost and the route's gas from the output. Without them, it ranks on gross output.

## What it returns

For each order, water-fill builds up to four candidate routes and returns the one with the highest output net of gas. If none beats the single path, it returns the single path.

1. **Best single path** — the best route that carries the whole order on its own. This is the floor: water-fill never returns less than this.
2. **20-chunk disjoint split** — splits the order across paths that share no pool ("pool-disjoint"), allocating in 20 chunks. This is the safety net: it is cheap and always finishes, even under a tight timeout, so water-fill can still beat the single path when a split would win (see [Never-lose floor](#never-lose-floor)).
3. **256-chunk disjoint split** — the same pool-disjoint paths, allocated in 256 chunks for a finer split. It runs in two phases (see [Two-phase activation](#two-phase-activation)) and then an [exchange-refinement](#exchange-refinement) pass.
4. **Fill-and-spill** — a split that lets paths share a pool and branch at an intermediate token (a "tree route"). The pool-disjoint splits above cannot express these.

## Incremental water-fill

Every split fills the order in small chunks. For each chunk, water-fill checks how much each path would return for that chunk, gives the chunk to the best one, and moves on.

Constant-product and tick AMMs are path-independent in cumulative input: one swap of `x` returns the same as two back-to-back swaps that sum to `x`. So instead of re-simulating each path at its full running total for every chunk (O(chunks²)), water-fill simulates only the next chunk against the pool state committed so far (O(chunks)). That saved work is what makes the 256-chunk split affordable.

## Two-phase activation

Splitting into smaller chunks can backfire on large trades. A path only pays off once it carries enough volume to cover its gas cost (its "activation gate"). If the first chunk given to a path is too small, the path fails that gate and never turns on — even when it would be profitable at a larger share.

Water-fill avoids this in two phases: first decide which paths to use at coarse (20-chunk) granularity, where each path's share is large enough for the gate to be meaningful; then allocate across the chosen paths at fine (256-chunk) granularity with the gate off, since their gas is already covered.

## Exchange refinement

A chunk-by-chunk split can never take back a chunk it already gave out, so the refined split is only accurate to one chunk (1/256 of the order) and can sit slightly off the ideal split. The exchange pass corrects this. Starting from the refined split, it moves a small amount of input from an over-allocated path to an under-allocated one, keeping the move only if the two paths' combined output goes up. When no move helps, it halves the amount and tries again, down to a floor below one chunk. The paths share no pool, so each trial re-simulates only the two paths it touches. The pass stops at the solve timeout or a cap on trial simulations. Because it keeps only moves that improve output, the result is never worse than the split it started from.

## Candidate discovery

Water-fill finds candidate routes two ways and combines them, so no useful route is dropped:

1. **Exhaustive enumeration** — lists paths between the sell and buy tokens (breadth-first) and ranks them by a spot-price × depth heuristic, reusing [Most Liquid](/algorithms/most-liquid) on the same `DepthAndPrice` graph (see the Dependency note below).
2. **Bounded amount-aware search** — a frontier search (the discovery section of `water_fill.rs`) that expands from the sell token with the full order amount, preferring edges toward the buy token, the `connector_tokens` allowlist, or a set of anchor tokens.

Anchors are a **soft ranking hint**, distinct from the `connector_tokens` **hard allowlist**: when no `connector_tokens` allowlist is set, discovery prefers to expand through anchors but still reaches any other token. The anchor set is derived per solve from the graph itself — the most connected tokens (highest pool-edge degree, the same signal `derive-connector-tokens` ranks by) plus the native-ETH sentinel `0x0000000000000000000000000000000000000000`. Deriving from live connectivity keeps anchoring correct on every chain with no hardcoded per-chain list; the sentinel is anchored explicitly so WETH → ETH → token routes survive on full Fynd setups where Tycho models native ETH as the zero address.

The bounded set is placed ahead of the heuristic-ranked set, so connector and anchor routes survive the spot × depth cutoff. If the bounded search fails, it is not fatal: the exhaustive set already guarantees a route.

> **Dependency:** water-fill's discovery and single-path ranking are built on Most Liquid's `find_paths`, `try_score_path`, and `simulate_path`. Changing those Most Liquid internals changes water-fill's candidate set and ranking — keep both in mind when editing either algorithm.

## Route assembly

Every returned candidate is assembled through the shared split primitives (`build_split_route`), which emit swaps in topological order, apply the tycho-execution remainder-split convention, merge shared hops (paths that share a prefix pool produce one combined swap with split downstream legs), and attach the route's token map. If a candidate's legs cannot be topologically ordered — for example two disjoint legs that form a token cycle — assembly returns nothing and that candidate is dropped, so water-fill falls back to another candidate or the single path. Assembly is the same code the router encodes on-chain, so every route water-fill returns can be encoded.

## Never-lose floor

Water-fill never returns less net output than the best single path. This holds even under a tight timeout because the 20-chunk disjoint split (candidate 2) is cheap and always finishes on the same solve clock — a timeout cannot cut off the split while leaving the single path, so if a split wins, water-fill returns it.

Because water-fill always returns at least the best single path, it can be the only pool on a chain. A split router that returned nothing when it declined to split would need a single-path pool beside it to answer those orders.

## Suggested configuration

```toml
[pools.water_fill_3_hops]
algorithm = "water_fill"
num_workers = 3
task_queue_capacity = 1000
max_hops = 3
timeout_ms = 5000
max_routes = 1024
```

* `max_hops = 3` keeps discovery bounded. Water-fill simulates every candidate on every order, so each extra hop widens both the enumerated set and the per-candidate simulation cost.
* `max_routes = 1024` caps the heuristic-ranked candidate set. The bounded amount-aware search is placed ahead of it, so connector and anchor routes survive the cutoff.
* `timeout_ms = 5000` is generous because the exchange-refinement pass and the 256-chunk split use whatever budget remains. Cutting it short is safe — the cheap 20-chunk split always finishes on the same clock, so the [never-lose floor](#never-lose-floor) still holds.
* Water-fill answers every order, so it can run as the only pool on a chain. It is still slower than the single-route finders, so pair it with a Bellman-Ford pool if you serve latency-sensitive traffic.

{% hint style="warning" %}
**Set connector tokens.** With `connector_tokens` unset, routes can pass through illiquid long-tail intermediates, which raises reversion risk — and a split route multiplies that exposure across every path it activates. Restrict intermediate hops to a small trusted set (a hard filter) — generate one for your chain with `fynd derive-connector-tokens --chain Ethereum --top-n 10 --output toml` and paste it into the pool. See [Connector tokens](/guides/server-configuration#connector-tokens). The soft anchor preference used when no allowlist is set is derived automatically from the graph and needs no configuration.
{% endhint %}

## Source reference

| File                                          | Purpose                                                                                                                                                                    |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fynd-core/src/algorithm/water_fill.rs`       | `WaterFillAlgorithm`: candidate allocation (20-chunk disjoint split, 256-chunk disjoint split, fill-and-spill) and bounded candidate discovery                             |
| `fynd-core/src/algorithm/most_liquid.rs`      | [Most Liquid](/algorithms/most-liquid) path finder reused for discovery and ranking: `find_paths`, `try_score_path`, `simulate_path`, and the `DepthAndPrice` graph weight |
| `fynd-core/src/algorithm/split_primitives.rs` | Shared-hop merging and executable route assembly                                                                                                                           |
| `fynd-core/src/worker_pool/registry.rs`       | Maps `"water_fill"` to `WaterFillAlgorithm`                                                                                                                                |


# API

Specifications to interact with Fynd server

## GET /v1/health - Health check endpoint.

> Returns the current health status of the service.

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"paths":{"/v1/health":{"get":{"tags":["health"],"summary":"GET /v1/health - Health check endpoint.","description":"Returns the current health status of the service.","operationId":"health","responses":{"200":{"description":"Service healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}},"503":{"description":"Data stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}}}}}},"components":{"schemas":{"HealthStatus":{"type":"object","description":"Health check response.","required":["healthy","last_update_ms","num_solver_pools"],"properties":{"derived_data_ready":{"type":"boolean","description":"Whether derived data has been computed at least once.\n\nThis indicates overall readiness, not per-block freshness. Some algorithms\nrequire fresh derived data for each block — they are ready to receive orders\nbut will wait for recomputation before solving."},"gas_price_age_ms":{"type":["integer","null"],"format":"int64","description":"Time since last gas price update in milliseconds, if available.","minimum":0},"healthy":{"type":"boolean","description":"Whether the service is healthy."},"last_update_ms":{"type":"integer","format":"int64","description":"Time since last market update in milliseconds.","minimum":0},"num_solver_pools":{"type":"integer","description":"Number of solver pools configured at startup.\n\nThis is the configured/registered count, not a live count of healthy worker\nthreads — it does not decrease if individual workers stop or panic.","minimum":0}}}}}}
```

## POST /v1/quote - Request a quote.

> Accepts a \`QuoteRequest\` and returns a \`Quote\` with the best routes found, or an error\
> if the request could not be filled.\
> \
> \# Errors\
> \
> \- 400 Bad Request: Invalid request format\
> \- 422 Unprocessable Entity: No routes found\
> \- 503 Service Unavailable: Queue full or service overloaded\
> \- 503 Service Unavailable: Queue full, service overloaded, or quote timeout

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"paths":{"/v1/quote":{"post":{"tags":["solver"],"summary":"POST /v1/quote - Request a quote.","description":"Accepts a `QuoteRequest` and returns a `Quote` with the best routes found, or an error\nif the request could not be filled.\n\n# Errors\n\n- 400 Bad Request: Invalid request format\n- 422 Unprocessable Entity: No routes found\n- 503 Service Unavailable: Queue full or service overloaded\n- 503 Service Unavailable: Queue full, service overloaded, or quote timeout","operationId":"quote","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"}}},"required":true},"responses":{"200":{"description":"Quote completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"No route found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Queue full, overloaded, stale data, or timeout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"QuoteRequest":{"type":"object","description":"Request to solve one or more swap orders.","required":["orders"],"properties":{"options":{"$ref":"#/components/schemas/QuoteOptions","description":"Optional solving parameters that apply to all orders."},"orders":{"type":"array","items":{"$ref":"#/components/schemas/Order"},"description":"Orders to solve."}}},"QuoteOptions":{"type":"object","description":"Options to customize the solving behavior.","properties":{"encoding_options":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EncodingOptions","description":"Options during encoding. If None, quote will be returned without calldata."}]},"max_gas":{"type":["string","null"],"description":"Maximum gas cost allowed for a solution. Quotes exceeding this are filtered out."},"min_responses":{"type":["integer","null"],"description":"Minimum number of solver responses to wait for before returning.\nIf `None` or `0`, waits for all solvers to respond (or timeout).\n\nUse the `/health` endpoint to check `num_solver_pools` before setting this value.\nValues exceeding the number of active solver pools are clamped internally.","minimum":0},"timeout_ms":{"type":["integer","null"],"format":"int64","description":"Timeout in milliseconds. If `None`, uses server default.","minimum":0}}},"EncodingOptions":{"type":"object","description":"Options to customize the encoding behavior.","required":["slippage"],"properties":{"client_fee_params":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ClientFeeParams","description":"Client fee configuration. When absent, no fee is charged."}]},"permit":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PermitSingle","description":"Permit2 single-token authorization. Required when using `transfer_from_permit2`."}]},"permit2_signature":{"type":["string","null"],"description":"Permit2 signature (65 bytes, hex-encoded). Required when `permit` is set."},"price_guard":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PriceGuardConfig","description":"Per-request price guard configuration. If `None`, struct defaults are used."}]},"slippage":{"type":"number","format":"double"},"transfer_type":{"$ref":"#/components/schemas/UserTransferType","description":"Token transfer method. Defaults to `transfer_from`."}}},"ClientFeeParams":{"type":"object","description":"Client fee configuration for the Tycho Router.\n\nWhen provided, the router charges a client fee on the swap output. The `signature`\nmust be an EIP-712 signature by the `receiver` over the `ClientFee` typed data.","required":["bps","receiver","max_contribution","deadline","signature"],"properties":{"bps":{"type":"integer","format":"int32","description":"Fee in basis points (0–10,000). 100 = 1%.","minimum":0},"deadline":{"type":"integer","format":"int64","description":"Unix timestamp after which the signature is invalid.","minimum":0},"max_contribution":{"type":"string","description":"Maximum subsidy from the client's vault balance."},"receiver":{"type":"string","description":"Address that receives the fee (also the required EIP-712 signer)."},"signature":{"type":"string","description":"65-byte EIP-712 ECDSA signature by `receiver` (hex-encoded)."}}},"PermitSingle":{"type":"object","description":"A single permit for permit2 token transfer authorization.","required":["details","spender","sig_deadline"],"properties":{"details":{"$ref":"#/components/schemas/PermitDetails","description":"The permit details (token, amount, expiration, nonce)."},"sig_deadline":{"type":"string","description":"Deadline timestamp for the permit signature."},"spender":{"type":"string","description":"Address authorized to spend the tokens (typically the router)."}}},"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}},"PriceGuardConfig":{"type":"object","description":"Per-request price guard configuration.\n\nAll fields are optional. When `None`, struct defaults are used.","properties":{"enabled":{"type":["boolean","null"],"description":"Whether price guard validation is enabled."},"fail_on_provider_error":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider can return a price."},"fail_on_token_price_not_found":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider returns price for token pair."},"lower_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out < expected`, in basis points.","minimum":0},"upper_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out >= expected`, in basis points.","minimum":0}}},"UserTransferType":{"type":"string","description":"Token transfer method for moving funds into Tycho execution.","enum":["transfer_from_permit2","transfer_from","use_vaults_funds"]},"Order":{"type":"object","description":"A single swap order to be solved.\n\nAn order specifies an intent to swap one token for another.","required":["token_in","token_out","amount","side","sender"],"properties":{"amount":{"type":"string","description":"Amount to swap, interpreted according to `side` (in token units, as decimal string)."},"receiver":{"type":["string","null"],"description":"Address that will receive the output tokens.\n\nDefaults to `sender` if not specified."},"sender":{"type":"string","description":"Address that will send the input tokens."},"side":{"$ref":"#/components/schemas/OrderSide","description":"Whether this is a sell (exact input) or buy (exact output) order."},"token_in":{"type":"string","description":"Input token address (the token being sold)."},"token_out":{"type":"string","description":"Output token address (the token being bought)."}}},"OrderSide":{"type":"string","description":"Specifies the side of an order: sell (exact input) or buy (exact output).\n\nCurrently only `Sell` is supported. `Buy` will be added in a future version.","enum":["sell"]},"Quote":{"type":"object","description":"Complete solution for a [`QuoteRequest`].\n\nContains a solution for each order in the request, along with aggregate\ngas estimates and timing information.","required":["orders","total_gas_estimate","solve_time_ms"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderQuote"},"description":"Quotes for each order, in the same order as the request."},"solve_time_ms":{"type":"integer","format":"int64","description":"Time taken to compute this solution, in milliseconds.","minimum":0},"total_gas_estimate":{"type":"string","description":"Total estimated gas for executing all swaps (as decimal string)."}}},"OrderQuote":{"type":"object","description":"Quote for a single [`Order`].\n\nContains the route to execute (if found), along with expected amounts,\ngas estimates, and status information.","required":["order_id","status","amount_in","amount_out","gas_estimate","amount_out_net_gas","block"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"amount_out_net_gas":{"type":"string","description":"Amount out minus gas cost in output token terms.\nUsed by WorkerPoolRouter to compare solutions from different solvers."},"block":{"$ref":"#/components/schemas/BlockInfo","description":"Block at which this quote was computed."},"fee_breakdown":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FeeBreakdown","description":"Fee breakdown (populated when encoding options are provided)."}]},"gas_estimate":{"type":"string","description":"Estimated gas cost for executing this route (as decimal string)."},"gas_price":{"type":["string","null"],"description":"Effective gas price (in wei) at the time the route was computed."},"order_id":{"type":"string","description":"ID of the order this solution corresponds to."},"price_impact_bps":{"type":["integer","null"],"format":"int32","description":"Price impact in basis points (1 bip = 0.01%)."},"route":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Route","description":"The route to execute, if a valid route was found."}]},"status":{"$ref":"#/components/schemas/QuoteStatus","description":"Status indicating whether a route was found."},"transaction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Transaction","description":"An encoded EVM transaction ready to be submitted on-chain."}]}}},"BlockInfo":{"type":"object","description":"Block information at which a quote was computed.\n\nQuotes are only valid for the block at which they were computed. Market\nconditions may change in subsequent blocks.","required":["number","hash","timestamp"],"properties":{"hash":{"type":"string","description":"Block hash as a hex string."},"number":{"type":"integer","format":"int64","description":"Block number.","minimum":0},"timestamp":{"type":"integer","format":"int64","description":"Block timestamp in Unix seconds.","minimum":0}}},"FeeBreakdown":{"type":"object","description":"Breakdown of fees applied to the swap output by the on-chain FeeCalculator.\n\nAll amounts are absolute values in output token units.","required":["router_fee","client_fee","max_slippage","min_amount_received"],"properties":{"client_fee":{"type":"string","description":"Client's portion of the fee (after the router takes its share)."},"max_slippage":{"type":"string","description":"Maximum slippage: (amount_out - router_fee - client_fee) * slippage."},"min_amount_received":{"type":"string","description":"Minimum amount the user receives on-chain.\nEqual to amount_out - router_fee - client_fee - max_slippage."},"router_fee":{"type":"string","description":"Router protocol fee (fee on output + router's share of client fee)."},"swaps_hash":{"type":["string","null"],"description":"keccak256 of the ABI-encoded swap bytes, as a 0x-prefixed hex string.\nPresent only when client fee params were included in the request.\nUse this with `amount_in`, `token_in`, `token_out`, `amount_out`, `min_amount_received`,\nand `receiver` to compute the 11-field EIP-712 `ClientFee` signing hash (see client library\nhelpers)."}}},"Route":{"type":"object","description":"A route consisting of one or more sequential swaps.\n\nA route describes the path through components (liquidity pools) to execute a swap.\nFor multi-hop swaps, the output of each swap becomes the input of the next.","required":["swaps"],"properties":{"swaps":{"type":"array","items":{"$ref":"#/components/schemas/Swap"},"description":"Ordered sequence of swaps to execute."}}},"Swap":{"type":"object","description":"A single swap within a route.\n\nRepresents an atomic swap on a specific component (liquidity pool).","required":["component_id","protocol","token_in","token_out","amount_in","amount_out","gas_estimate","split"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"component_id":{"type":"string","description":"Identifier of the component (liquidity pool)."},"gas_estimate":{"type":"string","description":"Estimated gas cost for this swap (as decimal string)."},"protocol":{"type":"string","description":"Protocol system identifier (e.g., \"uniswap_v2\", \"uniswap_v3\", \"vm:balancer\")."},"split":{"type":"number","format":"double","description":"Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)"},"token_in":{"type":"string","description":"Input token address."},"token_out":{"type":"string","description":"Output token address."}}},"QuoteStatus":{"type":"string","description":"Status of an order quote.","enum":["success","no_route_found","insufficient_liquidity","timeout","not_ready","price_check_failed"]},"Transaction":{"type":"object","description":"An encoded EVM transaction ready to be submitted on-chain.","required":["to","value","data"],"properties":{"client_fee_signature_offset":{"type":["integer","null"],"description":"Byte offset of the client fee signature within `data`.\nClients use this to overwrite the placeholder signature with the real one.","minimum":0},"data":{"type":"string","description":"ABI-encoded calldata as hex string."},"to":{"type":"string","description":"Contract address to call."},"value":{"type":"string","description":"Native token value to send with the transaction (as decimal string)."}}},"ErrorResponse":{"type":"object","description":"Error response body.","required":["error","code"],"properties":{"code":{"type":"string"},"details":{},"error":{"type":"string"}}}}}}
```

## GET /v1/info

> GET /v1/info - Return static metadata about this Fynd instance.

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"paths":{"/v1/info":{"get":{"tags":["solver"],"summary":"GET /v1/info - Return static metadata about this Fynd instance.","operationId":"info","responses":{"200":{"description":"Instance info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstanceInfo"}}}}}}}},"components":{"schemas":{"InstanceInfo":{"type":"object","description":"Static metadata about this Fynd instance, returned by `GET /v1/info`.","required":["chain_id","permit2_address"],"properties":{"chain_id":{"type":"integer","format":"int64","description":"EIP-155 chain ID (e.g. 1 for Ethereum mainnet).","minimum":0},"permit2_address":{"type":"string","description":"Address of the canonical Permit2 contract (same on all EVM chains)."},"router_address":{"type":["string","null"],"description":"Address of the Tycho Router contract on this chain; `null` on a quote-only chain."},"version":{"type":"string","description":"Fynd binary version (Cargo package version, e.g. \"0.89.1\").\n\nDefaults to empty when absent so newer clients tolerate older servers that predate it."}}}}}}
```

## The BlockInfo object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"BlockInfo":{"type":"object","description":"Block information at which a quote was computed.\n\nQuotes are only valid for the block at which they were computed. Market\nconditions may change in subsequent blocks.","required":["number","hash","timestamp"],"properties":{"hash":{"type":"string","description":"Block hash as a hex string."},"number":{"type":"integer","format":"int64","description":"Block number.","minimum":0},"timestamp":{"type":"integer","format":"int64","description":"Block timestamp in Unix seconds.","minimum":0}}}}}}
```

## The ClientFeeParams object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"ClientFeeParams":{"type":"object","description":"Client fee configuration for the Tycho Router.\n\nWhen provided, the router charges a client fee on the swap output. The `signature`\nmust be an EIP-712 signature by the `receiver` over the `ClientFee` typed data.","required":["bps","receiver","max_contribution","deadline","signature"],"properties":{"bps":{"type":"integer","format":"int32","description":"Fee in basis points (0–10,000). 100 = 1%.","minimum":0},"deadline":{"type":"integer","format":"int64","description":"Unix timestamp after which the signature is invalid.","minimum":0},"max_contribution":{"type":"string","description":"Maximum subsidy from the client's vault balance."},"receiver":{"type":"string","description":"Address that receives the fee (also the required EIP-712 signer)."},"signature":{"type":"string","description":"65-byte EIP-712 ECDSA signature by `receiver` (hex-encoded)."}}}}}}
```

## The EncodingOptions object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"EncodingOptions":{"type":"object","description":"Options to customize the encoding behavior.","required":["slippage"],"properties":{"client_fee_params":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ClientFeeParams","description":"Client fee configuration. When absent, no fee is charged."}]},"permit":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PermitSingle","description":"Permit2 single-token authorization. Required when using `transfer_from_permit2`."}]},"permit2_signature":{"type":["string","null"],"description":"Permit2 signature (65 bytes, hex-encoded). Required when `permit` is set."},"price_guard":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PriceGuardConfig","description":"Per-request price guard configuration. If `None`, struct defaults are used."}]},"slippage":{"type":"number","format":"double"},"transfer_type":{"$ref":"#/components/schemas/UserTransferType","description":"Token transfer method. Defaults to `transfer_from`."}}},"ClientFeeParams":{"type":"object","description":"Client fee configuration for the Tycho Router.\n\nWhen provided, the router charges a client fee on the swap output. The `signature`\nmust be an EIP-712 signature by the `receiver` over the `ClientFee` typed data.","required":["bps","receiver","max_contribution","deadline","signature"],"properties":{"bps":{"type":"integer","format":"int32","description":"Fee in basis points (0–10,000). 100 = 1%.","minimum":0},"deadline":{"type":"integer","format":"int64","description":"Unix timestamp after which the signature is invalid.","minimum":0},"max_contribution":{"type":"string","description":"Maximum subsidy from the client's vault balance."},"receiver":{"type":"string","description":"Address that receives the fee (also the required EIP-712 signer)."},"signature":{"type":"string","description":"65-byte EIP-712 ECDSA signature by `receiver` (hex-encoded)."}}},"PermitSingle":{"type":"object","description":"A single permit for permit2 token transfer authorization.","required":["details","spender","sig_deadline"],"properties":{"details":{"$ref":"#/components/schemas/PermitDetails","description":"The permit details (token, amount, expiration, nonce)."},"sig_deadline":{"type":"string","description":"Deadline timestamp for the permit signature."},"spender":{"type":"string","description":"Address authorized to spend the tokens (typically the router)."}}},"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}},"PriceGuardConfig":{"type":"object","description":"Per-request price guard configuration.\n\nAll fields are optional. When `None`, struct defaults are used.","properties":{"enabled":{"type":["boolean","null"],"description":"Whether price guard validation is enabled."},"fail_on_provider_error":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider can return a price."},"fail_on_token_price_not_found":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider returns price for token pair."},"lower_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out < expected`, in basis points.","minimum":0},"upper_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out >= expected`, in basis points.","minimum":0}}},"UserTransferType":{"type":"string","description":"Token transfer method for moving funds into Tycho execution.","enum":["transfer_from_permit2","transfer_from","use_vaults_funds"]}}}}
```

## The ErrorResponse object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"ErrorResponse":{"type":"object","description":"Error response body.","required":["error","code"],"properties":{"code":{"type":"string"},"details":{},"error":{"type":"string"}}}}}}
```

## The FeeBreakdown object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"FeeBreakdown":{"type":"object","description":"Breakdown of fees applied to the swap output by the on-chain FeeCalculator.\n\nAll amounts are absolute values in output token units.","required":["router_fee","client_fee","max_slippage","min_amount_received"],"properties":{"client_fee":{"type":"string","description":"Client's portion of the fee (after the router takes its share)."},"max_slippage":{"type":"string","description":"Maximum slippage: (amount_out - router_fee - client_fee) * slippage."},"min_amount_received":{"type":"string","description":"Minimum amount the user receives on-chain.\nEqual to amount_out - router_fee - client_fee - max_slippage."},"router_fee":{"type":"string","description":"Router protocol fee (fee on output + router's share of client fee)."},"swaps_hash":{"type":["string","null"],"description":"keccak256 of the ABI-encoded swap bytes, as a 0x-prefixed hex string.\nPresent only when client fee params were included in the request.\nUse this with `amount_in`, `token_in`, `token_out`, `amount_out`, `min_amount_received`,\nand `receiver` to compute the 11-field EIP-712 `ClientFee` signing hash (see client library\nhelpers)."}}}}}}
```

## The HealthStatus object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"HealthStatus":{"type":"object","description":"Health check response.","required":["healthy","last_update_ms","num_solver_pools"],"properties":{"derived_data_ready":{"type":"boolean","description":"Whether derived data has been computed at least once.\n\nThis indicates overall readiness, not per-block freshness. Some algorithms\nrequire fresh derived data for each block — they are ready to receive orders\nbut will wait for recomputation before solving."},"gas_price_age_ms":{"type":["integer","null"],"format":"int64","description":"Time since last gas price update in milliseconds, if available.","minimum":0},"healthy":{"type":"boolean","description":"Whether the service is healthy."},"last_update_ms":{"type":"integer","format":"int64","description":"Time since last market update in milliseconds.","minimum":0},"num_solver_pools":{"type":"integer","description":"Number of solver pools configured at startup.\n\nThis is the configured/registered count, not a live count of healthy worker\nthreads — it does not decrease if individual workers stop or panic.","minimum":0}}}}}}
```

## The InstanceInfo object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"InstanceInfo":{"type":"object","description":"Static metadata about this Fynd instance, returned by `GET /v1/info`.","required":["chain_id","permit2_address"],"properties":{"chain_id":{"type":"integer","format":"int64","description":"EIP-155 chain ID (e.g. 1 for Ethereum mainnet).","minimum":0},"permit2_address":{"type":"string","description":"Address of the canonical Permit2 contract (same on all EVM chains)."},"router_address":{"type":["string","null"],"description":"Address of the Tycho Router contract on this chain; `null` on a quote-only chain."},"version":{"type":"string","description":"Fynd binary version (Cargo package version, e.g. \"0.89.1\").\n\nDefaults to empty when absent so newer clients tolerate older servers that predate it."}}}}}}
```

## The Order object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"Order":{"type":"object","description":"A single swap order to be solved.\n\nAn order specifies an intent to swap one token for another.","required":["token_in","token_out","amount","side","sender"],"properties":{"amount":{"type":"string","description":"Amount to swap, interpreted according to `side` (in token units, as decimal string)."},"receiver":{"type":["string","null"],"description":"Address that will receive the output tokens.\n\nDefaults to `sender` if not specified."},"sender":{"type":"string","description":"Address that will send the input tokens."},"side":{"$ref":"#/components/schemas/OrderSide","description":"Whether this is a sell (exact input) or buy (exact output) order."},"token_in":{"type":"string","description":"Input token address (the token being sold)."},"token_out":{"type":"string","description":"Output token address (the token being bought)."}}},"OrderSide":{"type":"string","description":"Specifies the side of an order: sell (exact input) or buy (exact output).\n\nCurrently only `Sell` is supported. `Buy` will be added in a future version.","enum":["sell"]}}}}
```

## The OrderSide object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"OrderSide":{"type":"string","description":"Specifies the side of an order: sell (exact input) or buy (exact output).\n\nCurrently only `Sell` is supported. `Buy` will be added in a future version.","enum":["sell"]}}}}
```

## The OrderQuote object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"OrderQuote":{"type":"object","description":"Quote for a single [`Order`].\n\nContains the route to execute (if found), along with expected amounts,\ngas estimates, and status information.","required":["order_id","status","amount_in","amount_out","gas_estimate","amount_out_net_gas","block"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"amount_out_net_gas":{"type":"string","description":"Amount out minus gas cost in output token terms.\nUsed by WorkerPoolRouter to compare solutions from different solvers."},"block":{"$ref":"#/components/schemas/BlockInfo","description":"Block at which this quote was computed."},"fee_breakdown":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FeeBreakdown","description":"Fee breakdown (populated when encoding options are provided)."}]},"gas_estimate":{"type":"string","description":"Estimated gas cost for executing this route (as decimal string)."},"gas_price":{"type":["string","null"],"description":"Effective gas price (in wei) at the time the route was computed."},"order_id":{"type":"string","description":"ID of the order this solution corresponds to."},"price_impact_bps":{"type":["integer","null"],"format":"int32","description":"Price impact in basis points (1 bip = 0.01%)."},"route":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Route","description":"The route to execute, if a valid route was found."}]},"status":{"$ref":"#/components/schemas/QuoteStatus","description":"Status indicating whether a route was found."},"transaction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Transaction","description":"An encoded EVM transaction ready to be submitted on-chain."}]}}},"BlockInfo":{"type":"object","description":"Block information at which a quote was computed.\n\nQuotes are only valid for the block at which they were computed. Market\nconditions may change in subsequent blocks.","required":["number","hash","timestamp"],"properties":{"hash":{"type":"string","description":"Block hash as a hex string."},"number":{"type":"integer","format":"int64","description":"Block number.","minimum":0},"timestamp":{"type":"integer","format":"int64","description":"Block timestamp in Unix seconds.","minimum":0}}},"FeeBreakdown":{"type":"object","description":"Breakdown of fees applied to the swap output by the on-chain FeeCalculator.\n\nAll amounts are absolute values in output token units.","required":["router_fee","client_fee","max_slippage","min_amount_received"],"properties":{"client_fee":{"type":"string","description":"Client's portion of the fee (after the router takes its share)."},"max_slippage":{"type":"string","description":"Maximum slippage: (amount_out - router_fee - client_fee) * slippage."},"min_amount_received":{"type":"string","description":"Minimum amount the user receives on-chain.\nEqual to amount_out - router_fee - client_fee - max_slippage."},"router_fee":{"type":"string","description":"Router protocol fee (fee on output + router's share of client fee)."},"swaps_hash":{"type":["string","null"],"description":"keccak256 of the ABI-encoded swap bytes, as a 0x-prefixed hex string.\nPresent only when client fee params were included in the request.\nUse this with `amount_in`, `token_in`, `token_out`, `amount_out`, `min_amount_received`,\nand `receiver` to compute the 11-field EIP-712 `ClientFee` signing hash (see client library\nhelpers)."}}},"Route":{"type":"object","description":"A route consisting of one or more sequential swaps.\n\nA route describes the path through components (liquidity pools) to execute a swap.\nFor multi-hop swaps, the output of each swap becomes the input of the next.","required":["swaps"],"properties":{"swaps":{"type":"array","items":{"$ref":"#/components/schemas/Swap"},"description":"Ordered sequence of swaps to execute."}}},"Swap":{"type":"object","description":"A single swap within a route.\n\nRepresents an atomic swap on a specific component (liquidity pool).","required":["component_id","protocol","token_in","token_out","amount_in","amount_out","gas_estimate","split"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"component_id":{"type":"string","description":"Identifier of the component (liquidity pool)."},"gas_estimate":{"type":"string","description":"Estimated gas cost for this swap (as decimal string)."},"protocol":{"type":"string","description":"Protocol system identifier (e.g., \"uniswap_v2\", \"uniswap_v3\", \"vm:balancer\")."},"split":{"type":"number","format":"double","description":"Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)"},"token_in":{"type":"string","description":"Input token address."},"token_out":{"type":"string","description":"Output token address."}}},"QuoteStatus":{"type":"string","description":"Status of an order quote.","enum":["success","no_route_found","insufficient_liquidity","timeout","not_ready","price_check_failed"]},"Transaction":{"type":"object","description":"An encoded EVM transaction ready to be submitted on-chain.","required":["to","value","data"],"properties":{"client_fee_signature_offset":{"type":["integer","null"],"description":"Byte offset of the client fee signature within `data`.\nClients use this to overwrite the placeholder signature with the real one.","minimum":0},"data":{"type":"string","description":"ABI-encoded calldata as hex string."},"to":{"type":"string","description":"Contract address to call."},"value":{"type":"string","description":"Native token value to send with the transaction (as decimal string)."}}}}}}
```

## The PermitDetails object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}}}}}
```

## The PermitSingle object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"PermitSingle":{"type":"object","description":"A single permit for permit2 token transfer authorization.","required":["details","spender","sig_deadline"],"properties":{"details":{"$ref":"#/components/schemas/PermitDetails","description":"The permit details (token, amount, expiration, nonce)."},"sig_deadline":{"type":"string","description":"Deadline timestamp for the permit signature."},"spender":{"type":"string","description":"Address authorized to spend the tokens (typically the router)."}}},"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}}}}}
```

## The PriceGuardConfig object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"PriceGuardConfig":{"type":"object","description":"Per-request price guard configuration.\n\nAll fields are optional. When `None`, struct defaults are used.","properties":{"enabled":{"type":["boolean","null"],"description":"Whether price guard validation is enabled."},"fail_on_provider_error":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider can return a price."},"fail_on_token_price_not_found":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider returns price for token pair."},"lower_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out < expected`, in basis points.","minimum":0},"upper_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out >= expected`, in basis points.","minimum":0}}}}}}
```

## The Route object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"Route":{"type":"object","description":"A route consisting of one or more sequential swaps.\n\nA route describes the path through components (liquidity pools) to execute a swap.\nFor multi-hop swaps, the output of each swap becomes the input of the next.","required":["swaps"],"properties":{"swaps":{"type":"array","items":{"$ref":"#/components/schemas/Swap"},"description":"Ordered sequence of swaps to execute."}}},"Swap":{"type":"object","description":"A single swap within a route.\n\nRepresents an atomic swap on a specific component (liquidity pool).","required":["component_id","protocol","token_in","token_out","amount_in","amount_out","gas_estimate","split"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"component_id":{"type":"string","description":"Identifier of the component (liquidity pool)."},"gas_estimate":{"type":"string","description":"Estimated gas cost for this swap (as decimal string)."},"protocol":{"type":"string","description":"Protocol system identifier (e.g., \"uniswap_v2\", \"uniswap_v3\", \"vm:balancer\")."},"split":{"type":"number","format":"double","description":"Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)"},"token_in":{"type":"string","description":"Input token address."},"token_out":{"type":"string","description":"Output token address."}}}}}}
```

## The Quote object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"Quote":{"type":"object","description":"Complete solution for a [`QuoteRequest`].\n\nContains a solution for each order in the request, along with aggregate\ngas estimates and timing information.","required":["orders","total_gas_estimate","solve_time_ms"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderQuote"},"description":"Quotes for each order, in the same order as the request."},"solve_time_ms":{"type":"integer","format":"int64","description":"Time taken to compute this solution, in milliseconds.","minimum":0},"total_gas_estimate":{"type":"string","description":"Total estimated gas for executing all swaps (as decimal string)."}}},"OrderQuote":{"type":"object","description":"Quote for a single [`Order`].\n\nContains the route to execute (if found), along with expected amounts,\ngas estimates, and status information.","required":["order_id","status","amount_in","amount_out","gas_estimate","amount_out_net_gas","block"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"amount_out_net_gas":{"type":"string","description":"Amount out minus gas cost in output token terms.\nUsed by WorkerPoolRouter to compare solutions from different solvers."},"block":{"$ref":"#/components/schemas/BlockInfo","description":"Block at which this quote was computed."},"fee_breakdown":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FeeBreakdown","description":"Fee breakdown (populated when encoding options are provided)."}]},"gas_estimate":{"type":"string","description":"Estimated gas cost for executing this route (as decimal string)."},"gas_price":{"type":["string","null"],"description":"Effective gas price (in wei) at the time the route was computed."},"order_id":{"type":"string","description":"ID of the order this solution corresponds to."},"price_impact_bps":{"type":["integer","null"],"format":"int32","description":"Price impact in basis points (1 bip = 0.01%)."},"route":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Route","description":"The route to execute, if a valid route was found."}]},"status":{"$ref":"#/components/schemas/QuoteStatus","description":"Status indicating whether a route was found."},"transaction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Transaction","description":"An encoded EVM transaction ready to be submitted on-chain."}]}}},"BlockInfo":{"type":"object","description":"Block information at which a quote was computed.\n\nQuotes are only valid for the block at which they were computed. Market\nconditions may change in subsequent blocks.","required":["number","hash","timestamp"],"properties":{"hash":{"type":"string","description":"Block hash as a hex string."},"number":{"type":"integer","format":"int64","description":"Block number.","minimum":0},"timestamp":{"type":"integer","format":"int64","description":"Block timestamp in Unix seconds.","minimum":0}}},"FeeBreakdown":{"type":"object","description":"Breakdown of fees applied to the swap output by the on-chain FeeCalculator.\n\nAll amounts are absolute values in output token units.","required":["router_fee","client_fee","max_slippage","min_amount_received"],"properties":{"client_fee":{"type":"string","description":"Client's portion of the fee (after the router takes its share)."},"max_slippage":{"type":"string","description":"Maximum slippage: (amount_out - router_fee - client_fee) * slippage."},"min_amount_received":{"type":"string","description":"Minimum amount the user receives on-chain.\nEqual to amount_out - router_fee - client_fee - max_slippage."},"router_fee":{"type":"string","description":"Router protocol fee (fee on output + router's share of client fee)."},"swaps_hash":{"type":["string","null"],"description":"keccak256 of the ABI-encoded swap bytes, as a 0x-prefixed hex string.\nPresent only when client fee params were included in the request.\nUse this with `amount_in`, `token_in`, `token_out`, `amount_out`, `min_amount_received`,\nand `receiver` to compute the 11-field EIP-712 `ClientFee` signing hash (see client library\nhelpers)."}}},"Route":{"type":"object","description":"A route consisting of one or more sequential swaps.\n\nA route describes the path through components (liquidity pools) to execute a swap.\nFor multi-hop swaps, the output of each swap becomes the input of the next.","required":["swaps"],"properties":{"swaps":{"type":"array","items":{"$ref":"#/components/schemas/Swap"},"description":"Ordered sequence of swaps to execute."}}},"Swap":{"type":"object","description":"A single swap within a route.\n\nRepresents an atomic swap on a specific component (liquidity pool).","required":["component_id","protocol","token_in","token_out","amount_in","amount_out","gas_estimate","split"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"component_id":{"type":"string","description":"Identifier of the component (liquidity pool)."},"gas_estimate":{"type":"string","description":"Estimated gas cost for this swap (as decimal string)."},"protocol":{"type":"string","description":"Protocol system identifier (e.g., \"uniswap_v2\", \"uniswap_v3\", \"vm:balancer\")."},"split":{"type":"number","format":"double","description":"Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)"},"token_in":{"type":"string","description":"Input token address."},"token_out":{"type":"string","description":"Output token address."}}},"QuoteStatus":{"type":"string","description":"Status of an order quote.","enum":["success","no_route_found","insufficient_liquidity","timeout","not_ready","price_check_failed"]},"Transaction":{"type":"object","description":"An encoded EVM transaction ready to be submitted on-chain.","required":["to","value","data"],"properties":{"client_fee_signature_offset":{"type":["integer","null"],"description":"Byte offset of the client fee signature within `data`.\nClients use this to overwrite the placeholder signature with the real one.","minimum":0},"data":{"type":"string","description":"ABI-encoded calldata as hex string."},"to":{"type":"string","description":"Contract address to call."},"value":{"type":"string","description":"Native token value to send with the transaction (as decimal string)."}}}}}}
```

## The QuoteOptions object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"QuoteOptions":{"type":"object","description":"Options to customize the solving behavior.","properties":{"encoding_options":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EncodingOptions","description":"Options during encoding. If None, quote will be returned without calldata."}]},"max_gas":{"type":["string","null"],"description":"Maximum gas cost allowed for a solution. Quotes exceeding this are filtered out."},"min_responses":{"type":["integer","null"],"description":"Minimum number of solver responses to wait for before returning.\nIf `None` or `0`, waits for all solvers to respond (or timeout).\n\nUse the `/health` endpoint to check `num_solver_pools` before setting this value.\nValues exceeding the number of active solver pools are clamped internally.","minimum":0},"timeout_ms":{"type":["integer","null"],"format":"int64","description":"Timeout in milliseconds. If `None`, uses server default.","minimum":0}}},"EncodingOptions":{"type":"object","description":"Options to customize the encoding behavior.","required":["slippage"],"properties":{"client_fee_params":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ClientFeeParams","description":"Client fee configuration. When absent, no fee is charged."}]},"permit":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PermitSingle","description":"Permit2 single-token authorization. Required when using `transfer_from_permit2`."}]},"permit2_signature":{"type":["string","null"],"description":"Permit2 signature (65 bytes, hex-encoded). Required when `permit` is set."},"price_guard":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PriceGuardConfig","description":"Per-request price guard configuration. If `None`, struct defaults are used."}]},"slippage":{"type":"number","format":"double"},"transfer_type":{"$ref":"#/components/schemas/UserTransferType","description":"Token transfer method. Defaults to `transfer_from`."}}},"ClientFeeParams":{"type":"object","description":"Client fee configuration for the Tycho Router.\n\nWhen provided, the router charges a client fee on the swap output. The `signature`\nmust be an EIP-712 signature by the `receiver` over the `ClientFee` typed data.","required":["bps","receiver","max_contribution","deadline","signature"],"properties":{"bps":{"type":"integer","format":"int32","description":"Fee in basis points (0–10,000). 100 = 1%.","minimum":0},"deadline":{"type":"integer","format":"int64","description":"Unix timestamp after which the signature is invalid.","minimum":0},"max_contribution":{"type":"string","description":"Maximum subsidy from the client's vault balance."},"receiver":{"type":"string","description":"Address that receives the fee (also the required EIP-712 signer)."},"signature":{"type":"string","description":"65-byte EIP-712 ECDSA signature by `receiver` (hex-encoded)."}}},"PermitSingle":{"type":"object","description":"A single permit for permit2 token transfer authorization.","required":["details","spender","sig_deadline"],"properties":{"details":{"$ref":"#/components/schemas/PermitDetails","description":"The permit details (token, amount, expiration, nonce)."},"sig_deadline":{"type":"string","description":"Deadline timestamp for the permit signature."},"spender":{"type":"string","description":"Address authorized to spend the tokens (typically the router)."}}},"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}},"PriceGuardConfig":{"type":"object","description":"Per-request price guard configuration.\n\nAll fields are optional. When `None`, struct defaults are used.","properties":{"enabled":{"type":["boolean","null"],"description":"Whether price guard validation is enabled."},"fail_on_provider_error":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider can return a price."},"fail_on_token_price_not_found":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider returns price for token pair."},"lower_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out < expected`, in basis points.","minimum":0},"upper_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out >= expected`, in basis points.","minimum":0}}},"UserTransferType":{"type":"string","description":"Token transfer method for moving funds into Tycho execution.","enum":["transfer_from_permit2","transfer_from","use_vaults_funds"]}}}}
```

## The QuoteRequest object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"QuoteRequest":{"type":"object","description":"Request to solve one or more swap orders.","required":["orders"],"properties":{"options":{"$ref":"#/components/schemas/QuoteOptions","description":"Optional solving parameters that apply to all orders."},"orders":{"type":"array","items":{"$ref":"#/components/schemas/Order"},"description":"Orders to solve."}}},"QuoteOptions":{"type":"object","description":"Options to customize the solving behavior.","properties":{"encoding_options":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EncodingOptions","description":"Options during encoding. If None, quote will be returned without calldata."}]},"max_gas":{"type":["string","null"],"description":"Maximum gas cost allowed for a solution. Quotes exceeding this are filtered out."},"min_responses":{"type":["integer","null"],"description":"Minimum number of solver responses to wait for before returning.\nIf `None` or `0`, waits for all solvers to respond (or timeout).\n\nUse the `/health` endpoint to check `num_solver_pools` before setting this value.\nValues exceeding the number of active solver pools are clamped internally.","minimum":0},"timeout_ms":{"type":["integer","null"],"format":"int64","description":"Timeout in milliseconds. If `None`, uses server default.","minimum":0}}},"EncodingOptions":{"type":"object","description":"Options to customize the encoding behavior.","required":["slippage"],"properties":{"client_fee_params":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ClientFeeParams","description":"Client fee configuration. When absent, no fee is charged."}]},"permit":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PermitSingle","description":"Permit2 single-token authorization. Required when using `transfer_from_permit2`."}]},"permit2_signature":{"type":["string","null"],"description":"Permit2 signature (65 bytes, hex-encoded). Required when `permit` is set."},"price_guard":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PriceGuardConfig","description":"Per-request price guard configuration. If `None`, struct defaults are used."}]},"slippage":{"type":"number","format":"double"},"transfer_type":{"$ref":"#/components/schemas/UserTransferType","description":"Token transfer method. Defaults to `transfer_from`."}}},"ClientFeeParams":{"type":"object","description":"Client fee configuration for the Tycho Router.\n\nWhen provided, the router charges a client fee on the swap output. The `signature`\nmust be an EIP-712 signature by the `receiver` over the `ClientFee` typed data.","required":["bps","receiver","max_contribution","deadline","signature"],"properties":{"bps":{"type":"integer","format":"int32","description":"Fee in basis points (0–10,000). 100 = 1%.","minimum":0},"deadline":{"type":"integer","format":"int64","description":"Unix timestamp after which the signature is invalid.","minimum":0},"max_contribution":{"type":"string","description":"Maximum subsidy from the client's vault balance."},"receiver":{"type":"string","description":"Address that receives the fee (also the required EIP-712 signer)."},"signature":{"type":"string","description":"65-byte EIP-712 ECDSA signature by `receiver` (hex-encoded)."}}},"PermitSingle":{"type":"object","description":"A single permit for permit2 token transfer authorization.","required":["details","spender","sig_deadline"],"properties":{"details":{"$ref":"#/components/schemas/PermitDetails","description":"The permit details (token, amount, expiration, nonce)."},"sig_deadline":{"type":"string","description":"Deadline timestamp for the permit signature."},"spender":{"type":"string","description":"Address authorized to spend the tokens (typically the router)."}}},"PermitDetails":{"type":"object","description":"Details for a permit2 single-token permit.","required":["token","amount","expiration","nonce"],"properties":{"amount":{"type":"string","description":"Amount of tokens approved."},"expiration":{"type":"string","description":"Expiration timestamp for the permit."},"nonce":{"type":"string","description":"Nonce to prevent replay attacks."},"token":{"type":"string","description":"Token address for which the permit is granted."}}},"PriceGuardConfig":{"type":"object","description":"Per-request price guard configuration.\n\nAll fields are optional. When `None`, struct defaults are used.","properties":{"enabled":{"type":["boolean","null"],"description":"Whether price guard validation is enabled."},"fail_on_provider_error":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider can return a price."},"fail_on_token_price_not_found":{"type":["boolean","null"],"description":"Whether to reject solutions when no provider returns price for token pair."},"lower_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out < expected`, in basis points.","minimum":0},"upper_tolerance_bps":{"type":["integer","null"],"format":"int32","description":"Maximum allowed deviation when `amount_out >= expected`, in basis points.","minimum":0}}},"UserTransferType":{"type":"string","description":"Token transfer method for moving funds into Tycho execution.","enum":["transfer_from_permit2","transfer_from","use_vaults_funds"]},"Order":{"type":"object","description":"A single swap order to be solved.\n\nAn order specifies an intent to swap one token for another.","required":["token_in","token_out","amount","side","sender"],"properties":{"amount":{"type":"string","description":"Amount to swap, interpreted according to `side` (in token units, as decimal string)."},"receiver":{"type":["string","null"],"description":"Address that will receive the output tokens.\n\nDefaults to `sender` if not specified."},"sender":{"type":"string","description":"Address that will send the input tokens."},"side":{"$ref":"#/components/schemas/OrderSide","description":"Whether this is a sell (exact input) or buy (exact output) order."},"token_in":{"type":"string","description":"Input token address (the token being sold)."},"token_out":{"type":"string","description":"Output token address (the token being bought)."}}},"OrderSide":{"type":"string","description":"Specifies the side of an order: sell (exact input) or buy (exact output).\n\nCurrently only `Sell` is supported. `Buy` will be added in a future version.","enum":["sell"]}}}}
```

## The QuoteStatus object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"QuoteStatus":{"type":"string","description":"Status of an order quote.","enum":["success","no_route_found","insufficient_liquidity","timeout","not_ready","price_check_failed"]}}}}
```

## The Swap object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"Swap":{"type":"object","description":"A single swap within a route.\n\nRepresents an atomic swap on a specific component (liquidity pool).","required":["component_id","protocol","token_in","token_out","amount_in","amount_out","gas_estimate","split"],"properties":{"amount_in":{"type":"string","description":"Amount of input token (in token units, as decimal string)."},"amount_out":{"type":"string","description":"Amount of output token (in token units, as decimal string)."},"component_id":{"type":"string","description":"Identifier of the component (liquidity pool)."},"gas_estimate":{"type":"string","description":"Estimated gas cost for this swap (as decimal string)."},"protocol":{"type":"string","description":"Protocol system identifier (e.g., \"uniswap_v2\", \"uniswap_v3\", \"vm:balancer\")."},"split":{"type":"number","format":"double","description":"Decimal of the amount to be swapped in this operation (for example, 0.5 means 50%)"},"token_in":{"type":"string","description":"Input token address."},"token_out":{"type":"string","description":"Output token address."}}}}}}
```

## The Transaction object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"Transaction":{"type":"object","description":"An encoded EVM transaction ready to be submitted on-chain.","required":["to","value","data"],"properties":{"client_fee_signature_offset":{"type":["integer","null"],"description":"Byte offset of the client fee signature within `data`.\nClients use this to overwrite the placeholder signature with the real one.","minimum":0},"data":{"type":"string","description":"ABI-encoded calldata as hex string."},"to":{"type":"string","description":"Contract address to call."},"value":{"type":"string","description":"Native token value to send with the transaction (as decimal string)."}}}}}}
```

## The UserTransferType object

```json
{"openapi":"3.1.0","info":{"title":"fynd-rpc","version":"0.99.11"},"components":{"schemas":{"UserTransferType":{"type":"string","description":"Token transfer method for moving funds into Tycho execution.","enum":["transfer_from_permit2","transfer_from","use_vaults_funds"]}}}}
```


# Architecture

## Overview

Fynd is a solver built on Tycho that finds optimal swap routes across DeFi protocols. It is organized as a multi-crate Rust workspace:

* **`fynd-core`** - Pure solving logic with no HTTP dependencies
* **`fynd-rpc`** - HTTP RPC server library
* **`fynd`** - CLI binary that runs the complete routing service

This modular architecture allows users to:

* Use just the routing algorithms (`fynd-core`) in their own applications
* Build custom HTTP servers with their own middleware (`fynd-rpc`)
* Run the complete solver as a standalone service (`fynd` binary)

## Design Decisions

* **Concurrency Model**: Hybrid async/threaded -- I/O on tokio, route finding on dedicated OS threads
* **Data Sharing**: `Arc<RwLock<>>` with write-preferring lock for MarketState (single writer, many readers)
* **Path-Finding**: Pluggable `Algorithm` trait with associated graph types, allowing each algorithm to use its preferred graph representation
* **Graph Management**: `GraphManager` trait with incremental updates from market events; built-in implementation uses `petgraph::StableDiGraph`
* **Multi-Solver Competition**: Multiple worker pools with different configurations compete per request; WorkerPoolRouter selects the best result
* **Output Format**: Structured `Quote` objects (routes, amounts, gas estimates) with optional encoded transaction
* **Derived Data Pipeline**: Pre-computed spot prices, component depths, and token gas prices fed to algorithms via a separate computation framework
* **Observability**: Prometheus metrics on port 9898, structured tracing, health endpoint

***

## Architecture Diagram

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           HTTP Layer (Actix Web)                            │
│                         Async I/O - Non-blocking                            │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │                           RouterApi                                  │   │
│  │        POST /v1/quote      GET /v1/health      GET /v1/info          │   │
│  └───────────────────────────────┬──────────────────────────────────────┘   │
└──────────────────────────────────┼──────────────────────────────────────────┘
                                   │
                                   │ QuoteRequest
                                   ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                            WorkerPoolRouter                                 │
│           Orchestrates multiple solver pools, selects best solution         │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │  • Fan-out: Send each order to ALL solver pools in parallel           │  │
│  │  • Timeout: Configurable deadline per request                         │  │
│  │  • Early return: Optional min_responses for fast path                 │  │
│  │  • Selection: Choose best solution by amount_out_net_gas              │  │
│  │  • Encoding: Optionally encode solution into on-chain transaction     │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
                    ┌──────────────┴──────────────┐
                    │                             │
                    ▼                             ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│  Worker Pool A                  │ │  Worker Pool B                  │
│  (most_liquid, 2 hops, fast)    │ │  (most_liquid, 3 hops, deep)    │
│  ┌───────────┐                  │ │  ┌───────────┐                  │
│  │ TaskQueue │ (per-pool)       │ │  │ TaskQueue │ (per-pool)       │
│  └─────┬─────┘                  │ │  └─────┬─────┘                  │
│        │                        │ │        │                        │
│  ┌─────┴─────┐  ┌───────────┐   │ │  ┌─────┴─────┐  ┌───────────┐   │
│  │  Worker 1 │  │  Worker N │   │ │  │  Worker 1 │  │  Worker M │   │
│  │(SolverWkr)│  │(SolverWkr)│   │ │  │(SolverWkr)│  │(SolverWkr)│   │
│  └───────────┘  └───────────┘   │ │  └───────────┘  └───────────┘   │
└─────────────────────────────────┘ └─────────────────────────────────┘
                    │                             │
                    └──────────────┬──────────────┘
                                   │ Reads shared data
                                   ▼
┌────────────────────────────────────────────────────────────────────────────────────┐
│                         MarketState (Arc<RwLock<>>, via MarketData handle)          │
│  ┌────────────────────────────────────────────────────────────────────────────┐    │
│  │  components: HashMap<ComponentId, ProtocolComponent>                       │    │
│  │  simulation_states: HashMap<ComponentId, Box<dyn ProtocolSim>>             │    │
│  │  tokens: HashMap<Address, Token>                                           │    │
│  │  gas_price: Option<BlockGasPrice>                                          │    │
│  │  protocol_sync_status: HashMap<String, SynchronizerState>                  │    │
│  │  last_updated: Option<BlockInfo>                                           │    │
│  └────────────────────────────────────────────────────────────────────────────┘    │
└──────────────────────────────────▲─────────────────────────────────────────────────┘
                                   │ WRITE lock
                                   │
┌──────────────────────────────────┴──────────────────────────────────────────┐
│                              TychoFeed                                      │
│                     Background task (single instance)                       │
│  ┌────────────────────────────────────────────────────────────────────┐     │
│  │  Tycho Stream ──► Update MarketState ──► Broadcast Event            │     │
│  └────────────────────────────────────────────────────────────────────┘     │
│                                   │                                         │
│                                   ▼ broadcast::Sender<MarketEvent>          │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
              ┌──────────┐   ┌──────────┐   ┌──────────┐
              │SolverWkr │   │SolverWkr │   │SolverWkr │
              │GraphMngr │   │GraphMngr │   │GraphMngr │
              │ updates  │   │ updates  │   │ updates  │
              │ graph    │   │ graph    │   │ graph    │
              │ on event │   │ on event │   │ on event │
              └──────────┘   └──────────┘   └──────────┘

┌───────────────────────────────────────────────────────────────────────┐
│                     Derived Data Pipeline                             │
│                                                                       │
│  TychoFeed events ──► ComputationManager                              │
│                          │                                            │
│                          ├─ SpotPriceComputation                      │
│                          ├─ ComponentDepthComputation (needs spots)   │
│                          ├─ TokenGasPriceComputation (needs spots)    │
│                          │                                            │
│                          ▼                                            │
│                     DerivedData Store ──► broadcast events            │
│                                              │                        │
│                                    ┌─────────┼──────────┐             │
│                                    ▼         ▼          ▼             │
│                              Worker 1  Worker 2  Worker N             │
│                              (update edge weights on graph)           │
└───────────────────────────────────────────────────────────────────────┘
```

***

## Components

### 1. API Layer (RouterApi)

**Crate:** `fynd-rpc` **Location:** `fynd-rpc/src/api/`

Actix Web HTTP handlers. Validates requests, delegates to WorkerPoolRouter, returns JSON responses.

**Endpoints:**

* `POST /v1/quote` -- Submit quote requests
* `GET /v1/health` -- Health check (data freshness, derived data readiness, gas-price staleness, solver pool count)
* `GET /v1/info` -- Instance info (chain ID, router address, Permit2 address)
* `GET /metrics` -- Prometheus metrics (separate server, port 9898 by default)

***

### 2. WorkerPoolRouter

**Crate:** `fynd-core` **Location:** `fynd-core/src/worker_pool_router/`

Orchestrates quote requests across multiple worker pools:

1. Allocates the worker pools that serve each order, before dispatch. Each order is classified (`OrderClass`) and matched against each worker pool's configuration (`SolverPoolHandle::serves`) — today on the caller's access to exclusive liquidity, so a request without access is never dispatched to an exclusive-access worker pool
2. Fans out each order to its allocated worker pools in parallel
3. Manages per-request timeouts with optional early return
4. Refines gas estimates using `estimate_gas_usage` (tycho-execution) before cross-pool ranking — algorithms use a fast naive estimate internally; this step applies a more accurate one that accounts for token transfers and protocol overhead
5. Selects the best solution by refined `amount_out_net_gas`
6. Optionally encodes winning solutions into on-chain transactions (when `EncodingOptions` are provided)
7. Reports failures with error types and metrics

***

### 3. Worker Pool

**Crate:** `fynd-core` **Location:** `fynd-core/src/worker_pool/`

Manages dedicated OS threads for CPU-bound route finding. Each pool has:

* A name and algorithm assignment
* A bounded `TaskQueue` (via `async_channel`)
* N `SolverWorker` instances on separate threads

Pools can use either a built-in algorithm by name (e.g., `"most_liquid"`) or a custom `Algorithm` implementation via `WorkerPoolBuilder::with_algorithm`. Pools are configured via `worker_pools.toml` for built-in algorithms, or programmatically via the builder for custom algorithms. Multiple pools can use the same algorithm with different parameters (e.g., fast 2-hop vs deep 3-hop).

***

### 4. SolverWorker

**Crate:** `fynd-core` **Location:** `fynd-core/src/worker_pool/worker.rs`

Each worker:

1. Initializes a graph from market topology
2. Runs a prioritized `select!` loop: shutdown > market events > derived events > solve tasks
3. Maintains a `ReadinessTracker` for derived data requirements
4. Calls the algorithm's `find_best_route` with the local graph and shared market data

***

### 5. Algorithm Trait

**Crate:** `fynd-core` **Location:** `fynd-core/src/algorithm/`

Pluggable interface for route-finding algorithms:

* Specifies preferred graph type and graph manager via associated types
* Stateless: receives graph as parameter
* Declares derived data requirements (fresh vs stale)

**Built-in algorithms:**

* `MostLiquidAlgorithm` -- BFS path enumeration, depth-weighted scoring, ProtocolSim simulation, gas-adjusted ranking.
* `BellmanFordAlgorithm` -- Bellman-Ford relaxation with gas-aware edge weights, configurable via `AlgorithmConfig.gas_aware`.
* `PathFrankWolfeAlgorithm` -- Frank-Wolfe path-based optimization for multi-hop routing.
* `WaterFillAlgorithm` -- portfolio split router: exhaustive plus bounded amount-aware candidate discovery, then the best net of a single path, a coarse disjoint floor, a refined 256-chunk disjoint split, and a shared-component fill-and-spill; gas-aware net ranking when derived token gas prices are available.

***

### 6. Encoding

**Crate:** `fynd-core` **Location:** `fynd-core/src/encoding/`

Encodes solved routes into on-chain transactions. When `EncodingOptions` are provided, delegates to `TychoEncoder` to produce ABI-encoded calldata for the appropriate router function (`singleSwap`, `sequentialSwap`, `splitSwap`, and their Permit2/Vault variants). Supports optional `ClientFeeParams` for client fee configuration.

***

### 7. Graph Module

**Crate:** `fynd-core` **Location:** `fynd-core/src/graph/`

Graph management infrastructure:

* `GraphManager` trait: initialize + incremental updates from events
* `PetgraphStableDiGraphManager`: Implementation using `petgraph::StableDiGraph`
* `EdgeWeightUpdaterWithDerived`: Updates edge weights from derived data (component depths)
* `Path` type: Sequence of edges for route representation

***

### 8. MarketState / MarketData

**Crate:** `fynd-core` **Location:** `fynd-core/src/feed/market_data.rs`

`MarketState` is the single source of truth for all market state. Contains components, simulation states, tokens, gas prices, sync status, and block info. Protected by `Arc<RwLock<>>` (write-preferring). `MarketData` is the cheap-to-clone shared handle used to access it — call `read()` for a base view or `read_labeled(label)` for an overlay-aware view.

Provides `extract_subset_with_overlay()` for creating filtered snapshots that algorithms can use without holding the main lock.

***

### 9. TychoFeed

**Crate:** `fynd-core` **Location:** `fynd-core/src/feed/tycho_feed.rs`

Background task that connects to Tycho's WebSocket API, processes component/state updates, updates `MarketState` (via `MarketData`), and broadcasts `MarketEvent`s. Applies TVL filtering with hysteresis (components are added at `min_tvl` and removed at `min_tvl / tvl_buffer_ratio`), token recency filtering (`traded_n_days_ago`), blocklisting, and token quality filtering.

***

### 10. Derived Data System

**Crate:** `fynd-core` **Location:** `fynd-core/src/derived/`

Pre-computes analytics from raw market data:

* `SpotPriceComputation`: Spot prices for all component pairs
* `ComponentDepthComputation`: Liquidity depth at configured slippage
* `TokenGasPriceComputation`: Token prices relative to gas token

Computations run in dependency order. Workers use `ReadinessTracker` to wait for required data before solving.

***

### 11. Gas Price Fetcher

**Crate:** `fynd-core` **Location:** `fynd-core/src/feed/gas.rs`

Background worker that fetches gas prices from the RPC node. Signaled by TychoFeed after each block update.

***

### 12. Builder

**Crate:** `fynd-rpc` **Location:** `fynd-rpc/src/builder.rs`

`FyndRPCBuilder` assembles the entire system: creates feed, worker pools, computation manager, worker pool router, and HTTP server. `FyndRPC` runs the system and handles graceful shutdown.

***

### 13. CLI Binary

**Crate:** `fynd` **Location:** `src/main.rs` and `src/cli.rs`

Command-line application that parses CLI arguments, sets up observability (tracing, metrics), and uses `FyndRPCBuilder` to run the complete routing service.

***

## Data Flow

### Quote Request Flow

```
Client POST /v1/quote
    │
    ▼
RouterApi (validate)
    │
    ▼
WorkerPoolRouter (allocate pools, then fan out)
    │
    ├──► Pool A Queue ──► Worker ──► Algorithm ──► Quote
    ├──► Pool B Queue ──► Worker ──► Algorithm ──► Quote
    ├──► Pool C Queue ──► Worker ──► Algorithm ──► Timeout
    │
    ▼
WorkerPoolRouter (select best by amount_out_net_gas)
    │
    ▼ (optional)
Encoder (encode solution into on-chain transaction)
    │
    ▼
JSON Response to Client
```

### Market Update Flow

```
Tycho WebSocket Stream
    │
    ▼
TychoFeed
    ├──► Write MarketState (RwLock write)
    ├──► Broadcast MarketEvent
    │       ├──► Worker 1 GraphManager (update graph)
    │       ├──► Worker 2 GraphManager (update graph)
    │       └──► Worker N GraphManager (update graph)
    └──► Trigger Gas Price Fetcher
    └──► ComputationManager
            ├──► SpotPriceComputation
            ├──► ComponentDepthComputation
            ├──► TokenGasPriceComputation
            └──► Broadcast DerivedDataEvent
                    └──► Workers (update edge weights + readiness)
```

***

## Threading Model

```
Actix/Tokio Runtime (async I/O)
├── HTTP Server handlers
├── TychoFeed (WebSocket client)
├── WorkerPoolRouter (async fan-out)
├── Gas Price Fetcher
└── Computation Manager

Worker Pool A (dedicated OS threads)
├── Thread 1: SolverWorker (local graph + single-thread tokio rt)
├── Thread 2: SolverWorker
└── Thread N: SolverWorker

Worker Pool B (dedicated OS threads)
├── Thread 1: SolverWorker
└── Thread M: SolverWorker
```

**Communication channels:**

* HTTP -> WorkerPoolRouter: direct call (same async runtime)
* WorkerPoolRouter -> Workers: `async_channel` per pool (bounded, backpressure)
* Workers -> WorkerPoolRouter: `oneshot` channel (single response)
* TychoFeed -> Workers: `broadcast` channel (MarketEvent)
* ComputationManager -> Workers: `broadcast` channel (DerivedDataEvent)
* All -> MarketState (via MarketData): `Arc<RwLock<>>` (read-heavy)


# Performance

Throughput and latency benchmarks across routing configurations.

This page documents Fynd solver performance benchmarks across different configurations. All benchmarks use the `fynd-benchmark scale` subcommand, which builds a solver in-process for each worker count, runs a sustained load test, and reports throughput and latency statistics. See [https://github.com/propeller-heads/fynd/blob/main/docs/reference/guides/benchmarking.md](https://github.com/propeller-heads/fynd/blob/main/docs/reference/guides/benchmarking.md "mention") for how to run these yourself.

All results below were produced using `scripts/bench-remote.sh`, which provisions an EC2 instance, builds the solver from source, and runs the full scaling sweep automatically. Pool configuration files used by each benchmark are in [`tools/benchmark/`](https://github.com/propeller-heads/fynd/blob/main/docs/tools/benchmark/README.md). To reproduce the `most_liquid` results:

```bash
WORKER_COUNTS="1,2,3,4,6,8" \
NUM_REQUESTS=10000 \
POOL_CONFIG="tools/benchmark/most_liquid_2hop.toml" \
TYCHO_URL="$TYCHO_URL" \
TYCHO_API_KEY="$TYCHO_API_KEY" \
RPC_URL="$RPC_URL" \
  bash scripts/bench-remote.sh
```

## CPU Scaling: 2-Hop Routing

Measures how throughput scales with worker thread count for 2-hop route finding using the `most_liquid` algorithm.

### Setup

| Parameter              | Value                                                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Instance               | AWS `c7a.8xlarge` (32 vCPU, AMD EPYC)                                                                                                   |
| Algorithm              | `most_liquid`                                                                                                                           |
| Max hops               | 2                                                                                                                                       |
| Protocols              | `uniswap_v2`, `uniswap_v3`, `uniswap_v4`, `sushiswap_v2`, `pancakeswap_v2`, `pancakeswap_v3`, `ekubo_v2`, `fluid_v1`                    |
| Requests per iteration | 10,000                                                                                                                                  |
| Concurrency            | `fixed:48`                                                                                                                              |
| Warmup                 | 30s after health check                                                                                                                  |
| Config                 | [`tools/benchmark/most_liquid_2hop.toml`](https://github.com/propeller-heads/fynd/blob/main/docs/tools/benchmark/most_liquid_2hop.toml) |

### Results

| Workers | Throughput (req/s) | Median RT (ms) | P99 RT (ms) | RPS/Worker |
| ------: | -----------------: | -------------: | ----------: | ---------: |
|       1 |             397.19 |            120 |         129 |     397.19 |
|       2 |             743.16 |             64 |          69 |     371.58 |
|       3 |            1035.84 |             46 |          50 |     345.28 |
|       4 |            1444.04 |             33 |          36 |     361.01 |
|       6 |            2109.26 |             22 |          25 |     351.54 |
|       8 |            2820.08 |             16 |          18 |     352.51 |

### Analysis

Throughput scales nearly linearly across all tested worker counts (\~350-397 req/s per worker). The solver crosses 1000 req/s at **3 workers** (1036 req/s). Latency stays tight throughout — P99 is only 18ms at 8 workers.

**Recommendation.** For `most_liquid` 2-hop routing at 1000 req/s sustained throughput, provision at least 3 CPU cores. Use 4 cores for comfortable headroom.

## CPU Scaling: 3-Hop Routing

Measures how throughput scales with worker thread count for 3-hop route finding using the `most_liquid` algorithm.

### Setup

| Parameter              | Value                                                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Instance               | AWS `c7a.8xlarge` (32 vCPU, AMD EPYC)                                                                                                   |
| Algorithm              | `most_liquid`                                                                                                                           |
| Max hops               | 3                                                                                                                                       |
| Protocols              | `uniswap_v2`, `uniswap_v3`, `uniswap_v4`, `sushiswap_v2`, `pancakeswap_v2`, `pancakeswap_v3`, `ekubo_v2`, `fluid_v1`                    |
| Requests per iteration | 10,000                                                                                                                                  |
| Concurrency            | `fixed:48`                                                                                                                              |
| Warmup                 | 30s after health check                                                                                                                  |
| Config                 | [`tools/benchmark/most_liquid_3hop.toml`](https://github.com/propeller-heads/fynd/blob/main/docs/tools/benchmark/most_liquid_3hop.toml) |

### Results

| Workers | Throughput (req/s) | Median RT (ms) | P99 RT (ms) | RPS/Worker |
| ------: | -----------------: | -------------: | ----------: | ---------: |
|       1 |              22.30 |           2138 |        2531 |      22.30 |
|       2 |              39.46 |           1208 |        1465 |      19.73 |
|       4 |              75.95 |            627 |         784 |      18.99 |
|       8 |             146.52 |            319 |         440 |      18.32 |
|      12 |             177.23 |            260 |         386 |      14.77 |
|      16 |             298.22 |            146 |         243 |      18.64 |
|      20 |             352.63 |            117 |         226 |      17.63 |
|      24 |             243.00 |            163 |         320 |      10.13 |
|      28 |             384.13 |             92 |         251 |      13.72 |
|      32 |             366.06 |             86 |         272 |      11.44 |

### Analysis

Throughput is non-monotonic across worker counts. The solver peaks at **384 req/s at 28 workers** and does not reach 1000 req/s on this instance. P99 stays bounded (≤440ms), a significant improvement over the pre-lock-PR results where P99 spiked to 794ms at 24 workers.

**Recommendation.** For `most_liquid` 3-hop routing with this full protocol set, provision at least 28 CPU cores. The increased computational complexity of 3-hop search means throughput variability is expected.

## Comparison: 2-Hop vs 3-Hop

| Target RPS | 2-Hop Workers | 3-Hop Workers | Notes                                                            |
| ---------: | ------------: | ------------: | ---------------------------------------------------------------- |
|      1,000 |             3 |             — | 3-hop peaks at \~384 req/s at 28 workers; 1000 req/s not reached |

`most_liquid` 3-hop does not reach 1000 req/s on a 32-vCPU instance with 8 protocols. The combinatorial growth in the 3-hop search space creates a hard throughput ceiling for this algorithm.

## CPU Scaling: Bellman-Ford 2-Hop

Measures how throughput scales with worker thread count for 2-hop route finding using the `bellman_ford` algorithm.

### Setup

| Parameter              | Value                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Instance               | AWS `c7a.8xlarge` (32 vCPU, AMD EPYC)                                                                                                     |
| Algorithm              | `bellman_ford`                                                                                                                            |
| Max hops               | 2                                                                                                                                         |
| Protocols              | `uniswap_v2`, `uniswap_v3`, `uniswap_v4`, `sushiswap_v2`, `pancakeswap_v2`, `pancakeswap_v3`, `ekubo_v2`, `fluid_v1`                      |
| Requests per iteration | 10,000                                                                                                                                    |
| Concurrency            | `fixed:48`                                                                                                                                |
| Warmup                 | 30s after health check                                                                                                                    |
| Config                 | [`tools/benchmark/bellman_ford_2hop.toml`](https://github.com/propeller-heads/fynd/blob/main/docs/tools/benchmark/bellman_ford_2hop.toml) |

To reproduce:

```bash
WORKER_COUNTS="1,2,3,4,6,8" \
NUM_REQUESTS=10000 \
POOL_CONFIG="tools/benchmark/bellman_ford_2hop.toml" \
PROTOCOLS="uniswap_v2,uniswap_v3,uniswap_v4,sushiswap_v2,pancakeswap_v2,pancakeswap_v3,ekubo_v2,fluid_v1" \
TYCHO_URL="$TYCHO_URL" \
TYCHO_API_KEY="$TYCHO_API_KEY" \
RPC_URL="$RPC_URL" \
  bash scripts/bench-remote.sh
```

### Results

| Workers | Throughput (req/s) | Median RT (ms) | P99 RT (ms) | RPS/Worker |
| ------: | -----------------: | -------------: | ----------: | ---------: |
|       1 |              85.31 |            562 |         586 |      85.31 |
|       2 |             154.58 |            310 |         328 |      77.29 |
|       3 |             220.12 |            217 |         228 |      73.37 |
|       4 |             290.93 |            164 |         173 |      72.73 |
|       6 |             406.87 |            117 |         124 |      67.81 |
|       8 |             518.54 |             92 |          99 |      64.82 |

### Analysis

Throughput scales near-linearly across all tested worker counts. Per-worker efficiency declines gradually from \~85 req/s at 1 worker to \~65 req/s at 8 workers. The solver does not cross 1000 req/s within the tested 8-worker range; linear extrapolation places that threshold at approximately **16 workers**.

**Recommendation.** For Bellman-Ford 2-hop routing at 1000 req/s sustained throughput, provision at least 16 CPU cores.

## CPU Scaling: Bellman-Ford 3-Hop

Measures how throughput scales with worker thread count for 3-hop route finding using the `bellman_ford` algorithm.

### Setup

| Parameter              | Value                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Instance               | AWS `c7a.8xlarge` (32 vCPU, AMD EPYC)                                                                                                     |
| Algorithm              | `bellman_ford`                                                                                                                            |
| Max hops               | 3                                                                                                                                         |
| Protocols              | `uniswap_v2`, `uniswap_v3`, `uniswap_v4`, `sushiswap_v2`, `pancakeswap_v2`, `pancakeswap_v3`, `ekubo_v2`, `fluid_v1`                      |
| Requests per iteration | 10,000                                                                                                                                    |
| Concurrency            | `fixed:48`                                                                                                                                |
| Warmup                 | 30s after health check                                                                                                                    |
| Config                 | [`tools/benchmark/bellman_ford_3hop.toml`](https://github.com/propeller-heads/fynd/blob/main/docs/tools/benchmark/bellman_ford_3hop.toml) |

To reproduce:

```bash
WORKER_COUNTS="1,2,4,8,12,16,20,24,28,32" \
NUM_REQUESTS=10000 \
POOL_CONFIG="tools/benchmark/bellman_ford_3hop.toml" \
PROTOCOLS="uniswap_v2,uniswap_v3,uniswap_v4,sushiswap_v2,pancakeswap_v2,pancakeswap_v3,ekubo_v2,fluid_v1" \
TYCHO_URL="$TYCHO_URL" \
TYCHO_API_KEY="$TYCHO_API_KEY" \
RPC_URL="$RPC_URL" \
  bash scripts/bench-remote.sh
```

### Results

| Workers | Throughput (req/s) | Median RT (ms) | P99 RT (ms) | RPS/Worker |
| ------: | -----------------: | -------------: | ----------: | ---------: |
|       1 |              65.36 |            735 |         780 |      65.36 |
|       2 |             121.68 |            394 |         416 |      60.84 |
|       4 |             233.09 |            205 |         221 |      58.27 |
|       8 |             429.44 |            111 |         121 |      53.68 |
|      12 |             584.86 |             81 |          90 |      48.74 |
|      16 |             760.92 |             62 |          71 |      47.56 |
|      20 |             874.51 |             54 |          65 |      43.73 |
|      24 |             974.94 |             48 |          57 |      40.62 |
|      28 |            1201.92 |             39 |          49 |      42.93 |
|      32 |            1219.96 |             38 |          49 |      38.12 |

### Analysis

Throughput scales near-linearly up to 8 workers (\~54 req/s per worker). Beyond that, per-worker efficiency gradually declines — from \~49 req/s at 12 workers to \~38 req/s at 32 workers — as the instance approaches its CPU ceiling. The solver crosses 1000 req/s at **28 workers** (1202 req/s). Median latency falls from 735ms at 1 worker to 38ms at 32 workers; P99 stabilises at 49ms from 28 workers onward.

**Recommendation.** For Bellman-Ford 3-hop routing at 1000 req/s sustained throughput, provision at least 28 CPU cores. Use 32 cores for headroom under variable load.

## Comparison: Bellman-Ford 2-Hop vs 3-Hop

| Target RPS | 2-Hop Workers | 3-Hop Workers |  Ratio |
| ---------: | ------------: | ------------: | -----: |
|      1,000 |          \~16 |            28 | \~1.8x |

Bellman-Ford 3-hop requires roughly **1.8× more CPU cores** than 2-hop to reach the same throughput target. This is a much smaller penalty than seen with `most_liquid` (8×), reflecting Bellman-Ford's more uniform search cost growth across hop counts — it already explores the full path space at 2 hops, so adding a third hop grows the search space less dramatically relative to the base cost.

## Algorithm Comparison: most\_liquid vs bellman\_ford

All results in this section use identical hardware, protocol set, and request load.

### 2-Hop

| Workers | most\_liquid (req/s) | bellman\_ford (req/s) | Ratio |
| ------: | -------------------: | --------------------: | ----: |
|       1 |               397.19 |                 85.31 |  4.7x |
|       2 |               743.16 |                154.58 |  4.8x |
|       3 |              1035.84 |                220.12 |  4.7x |
|       4 |              1444.04 |                290.93 |  5.0x |
|       6 |              2109.26 |                406.87 |  5.2x |
|       8 |              2820.08 |                518.54 |  5.4x |

`most_liquid` is consistently **\~5× faster** for 2-hop routing. Its greedy liquidity-ranked search terminates early once the best path is found, while Bellman-Ford explores all paths exhaustively.

### 3-Hop

| Workers | most\_liquid (req/s) | bellman\_ford (req/s) | Winner               |
| ------: | -------------------: | --------------------: | -------------------- |
|       8 |               146.52 |                429.44 | bellman\_ford (2.9x) |
|      16 |               298.22 |                760.92 | bellman\_ford (2.6x) |
|      20 |               352.63 |                874.51 | bellman\_ford (2.5x) |
|      24 |               243.00 |                974.94 | bellman\_ford (4.0x) |
|      28 |               384.13 |               1201.92 | bellman\_ford (3.1x) |
|      32 |               366.06 |               1219.96 | bellman\_ford (3.3x) |

Both algorithms use the same 8-protocol set. At 3 hops the ranking **reverses** from the 2-hop result: `bellman_ford` is consistently **2.5–4× faster** than `most_liquid`, and keeps scaling while `most_liquid` plateaus. The `most_liquid` greedy search requires pre-computed edge weights (spot price × depth) that are recomputed on every block, creating a periodic pause that limits scaling; Bellman-Ford carries no pre-computed edge state so its per-block update is a no-op.


