# Routing (/developers/concepts/routing)



The quote engine turns a set of pools read at one chain state into a route and a set of
numbers a transaction can be held to. It runs in four stages, in order.

Stages one to three choose an allocation. Stage four prices it. Every figure the user is
shown comes from stage four, which is integer arithmetic that matches the validators.

## Stage 1: candidate path enumeration [#stage-1-candidate-path-enumeration]

Nodes are the registered stablecoins plus ADA. Edges are pools.

With that few nodes the enumeration of all simple paths up to `routing.maxPathLength` is
exhaustive rather than heuristic, so nothing is missed by a search cutoff. A path never
revisits an asset, so it cannot loop, and every intermediate asset must be in
`routing.intermediateAssets`. The user's own input and output are never intermediates.

Each path carries how it would settle, computed by `pathAtomicity`:

* A single pool takes its own venue's class.
* A multi hop path where every pool is Kernel is `atomic`.
* A multi hop path inside one Commitment venue that supports multi routing is
  `single-order`, because it becomes one order with one end to end minimum.
* Anything else is `needs-settlement`.

Paths that would settle leg by leg are dropped unless the request passes `allowNonAtomic`.

## Stage 2: reduction to effective pools [#stage-2-reduction-to-effective-pools]

Every venue Zerochord routes across is constant product once you look at the right reserves.
Minswap V2 is constant product on the reserves in its datum. Dano Finance's concentrated
liquidity pool is constant product on its virtual reserves, and the concentrated part only
decides what those virtual reserves are.

So a path of any length collapses to one triple of `(reserveIn, reserveOut, fee)`, exactly.
For pool 1 then pool 2 with fee multipliers g1 and g2:

```text
reserveIn  = R1in * R2in        / (R2in + g1 * R1out)
reserveOut = g1 * R1out * R2out / (R2in + g1 * R1out)
g          = g1 * g2
```

The reserves are carried as a rational with a shared denominator, and the fee multiplier as
an exact rational. Flooring the composed reserves introduced a ranking error of about thirty
basis points on small pools, which is enough to pick the wrong path.

## Stage 3: exact split solve [#stage-3-exact-split-solve]

Maximising total output over an allocation of one input across concave paths is a convex
problem solved by equalising marginal outputs, which is water filling. Fixed per-order costs
make it mixed integer, and at this size mixed integer means evaluating all subsets.

Each candidate carries a `fixedCostOut`: the cost of using that path at all, converted into
output base units. A venue that charges a non-refundable execution fee makes a small
allocation not worth taking, and the subset enumeration is what decides that.

Because every path reduced to a constant product triple in stage two, the water filling has a
closed form and the whole solve is a bisection on one scalar per subset. That bisection is the
only place a floating point number appears in the engine, and its answer is an allocation
rather than a quoted output.

`routing.maxCandidatePaths` bounds how many candidates enter the enumeration.

## Stage 4: integer requote [#stage-4-integer-requote]

The winning allocation is walked hop by hop, calling each venue's own `quote` with that
venue's own arithmetic and rounding. That result is the quote.

Nothing from stage three survives into the numbers on screen. An off-by-one in rounding
produces an order the validator rejects, not a slightly worse price, so the requote uses the
same integers the validator will.

### When a venue refuses the allocation [#when-a-venue-refuses-the-allocation]

The reduction in stage two models a pool's curve but not its capacity. A concentrated
liquidity pool stops filling once the price leaves its range, and the reduced form does not
know that. So an allocation can be chosen that a venue then refuses to quote.

Dropping that branch would be wrong twice over: its share of the input would vanish, so the
route would deliver less than the user paid for, and the comparison that chose the split would
no longer describe the route being offered.

Instead the offending path is removed from the candidate set and the split is solved again.
The whole input is always allocated to paths that can take it, and the engine refuses to
return a quote that does not spend the whole input.

## The floor: never worse than one path [#the-floor-never-worse-than-one-path]

Stages three and four can disagree. Composing a multi hop path floors its reserves, so the
reduction can predict a few base units more than walking the pools really yields. Where the
disagreement crosses the gap between two candidates, the solver can hand back a split that
prices worse than one path used alone.

So the engine checks rather than trusting the two stages to agree. Every candidate the solver
considered is requoted alone, at the full input, with the same integer arithmetic the quote is
made of. If any of them beats the chosen route, the route becomes that single path.

The comparison is on the solver's own objective, output net of each path's fixed cost, not on
gross output. Comparing gross would let the floor replace a route that pays one venue fee with
one that pays three, on the strength of an output difference the extra fees more than consume.

**The guarantee this buys, stated exactly: the quote is never worse than the best single
candidate path the solver considered.** It is not a claim about paths the pre-filter dropped.

## Price impact [#price-impact]

Price impact is the shortfall against the price a vanishingly small trade would get on the
very same route, in hundredths of a basis point.

The reference is the best marginal rate among the paths the route actually uses, read straight
off the exact reduction as a rational:

```text
lim x->0 out(x)/x = reserveOut * gammaNumerator / (reserveIn * gammaDenominator)
```

It is not measured by quoting a small probe amount. Probing was the first implementation and
it was wrong: a probe of a ten thousandth of the branch floors to one base unit on a small
trade, and one base unit through a pool floors its output to zero or one. The reported impact
then either sat at zero or moved with the trade size in the wrong direction. The closed form
has no probe, no floating point and no such regime.

## The minimum output [#the-minimum-output]

```text
minimumOut = amountOut - ceil(amountOut * slippageBps / 10 000)
```

The deduction rounds up, so the on-chain bound is never looser than the quote implies.

`slippageBps` comes from the request, or from `routing.defaultSlippageBps` when the request
omits it.

## Pools whose fee can move [#pools-whose-fee-can-move]

A pool that can change its fee after the quote cannot honour a fill-at-the-quoted-rate
promise. `routing.dynamicFeePolicy` decides what happens:

* `exclude` drops such pools from both the route and the baseline.
* `label` keeps them and relies on the risk flag reaching the interface.

## Related [#related]

<Cards>
  <Card title="The saving claim" href="/developers/concepts/saving-claim" />

  <Card title="Venue adapters" href="/developers/internals/venue-adapters" />

  <Card title="The Quote object" href="/developers/reference/quote" />
</Cards>
