> ## Documentation Index
> Fetch the complete documentation index at: https://docs.defiloops.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bounded execution

> How a large order is sized, bounded and split, and why costBps is not what you pay

**The swap router is ours.** Trades here are not handed to an aggregator: the
quoting, the routing across pools and the on-chain settlement are all DeFiLoops
code, which is why a plan can ask questions an aggregator's API does not answer:
what size the book will take at a cost you name, what a bound would leave of an
order, and how much of a resting order fills right now.

This page is how it answers "how much can I trade", what the number it returns
actually means, and the three different things people mean by "don't fill me at
a bad price".

Two neighbouring questions have their own pages:
[Slippage is not impact](/slippage) for what the two bps bounds each protect
against, and [Does splitting cost less?](/splitting) for whether breaking a trade
into slices saves anything. The splitting on THIS page is across pools at one
moment; that one is across time.

Written against `swap-router` at `2becf83` and the order engine at `94808f0`.
Every measured figure on this page names the block it was taken at. Pool state
moves; the numbers are illustrations of shape, not quotes.

## The question is inverted on purpose

A trader does not ask what the impact of \$500k is. They ask **how much they can
trade**, and the honest answer depends on what they are willing to pay. So
`/v1/sizing` takes an acceptable **cost** and returns a **size**, not the other
way round.

That inversion is also what makes it affordable. Asking "what does size S cost"
needs a quote per size, and finding the largest size under a cost is then a
search. Asking "what size costs C" is **one probe per level**: the pool is given
a price limit, it stops itself when its marginal rate reaches that limit, and it
reports what it moved. The answer falls out of the probe instead of being
searched for.

The default ladder is geometric (5, 10, 25, 50, 100 bps) because the
interesting region is near zero, where a few bps doubles the size, and nobody
makes a different decision between 400 and 500 bps.

Measured on Base at block 50769467, USDC to WETH, 11 candidate pools:

| `costBps` (marginal bound) | `maxAmountIn` USDC | `avgCostPips` | average cost |
| -------------------------- | ------------------ | ------------- | ------------ |
| 5                          | 12,265             | 6             | 0.06 bps     |
| 10                         | 213,719            | 256           | 2.56 bps     |
| 25                         | 818,992            | 1,007         | 10.07 bps    |
| 50                         | 2,153,466          | 1,433         | 14.33 bps    |

The 5 bps rung is the volatile one. Across runs on the same fork it has come
back at 12,265, 24,498 and 58,360 USDC: the winning pool changes, and a rung
measured over one small pool sits next to one measured over nine. That is why
every level publishes `poolsAnswered` alongside its size: two levels measured
over different pool counts are not comparable, and the sizes alone do not say so.

## `costBps` is a marginal bound, not what you pay

This is the single most misread number in the API, and it is misread in the
direction that makes execution look **twice as expensive as it is**.

`costBps` is the price the pool is *stopped at*. It is where the **last unit**
trades, not the blended price of the whole fill. The average the trade receives
is better, by roughly half:

<Warning>
  Reading `costBps` as the cost of the trade reads a number about twice too large.
  A level labelled 5 bps holds a fill that `/v1/quote` prices at 2. `avgCostPips`
  is the field to compare against a quote's `priceImpactPips`: same basis, same
  arithmetic.
</Warning>

For a **single constant-product pool** this is exact: if a trade displaces the
marginal price by δ, the average price paid is displaced by δ/2. That is where
"roughly half" comes from.

It does not survive contact with a real book. Measured across the same ladder at
block 50769564, the ratio between the marginal bound and the average it actually
delivers is neither 2 nor constant:

| `costBps` (marginal) | average delivered | ratio |
| -------------------- | ----------------- | ----- |
| 10                   | 2.46 bps          | 4.07x |
| 25                   | 9.97 bps          | 2.51x |
| 50                   | 14.23 bps         | 3.51x |
| 100                  | 39.31 bps         | 2.54x |

The book is concentrated liquidity across eleven pools with a winner that
changes between levels, so the "half" heuristic is directional and nothing more.

<Warning>
  Do not estimate the average by halving `costBps`. On this pair it would have
  been wrong by 25% to 100% at every level. Read `avgCostPips`, which is measured
  rather than derived.
</Warning>

The same block, priced the other way round through `/v1/quote`:

| size USDC | `priceImpactPips` | actual cost |
| --------- | ----------------- | ----------- |
| 12,265    | 15                | 0.15 bps    |
| 50,000    | 62                | 0.62 bps    |
| 100,000   | 124               | 1.24 bps    |
| 200,000   | 248               | 2.48 bps    |
| 213,719   | 265               | 2.65 bps    |

The row that matters: the size the ladder labels **"10 bps"** costs **2.65 bps**.

`avgCostPips` is `null` rather than `0` when the dust probe for that pool failed.
Unknown and free are different answers, and an earlier version reported both as
zero.

### The bound, stated as a price

`costBps` *is* a price bound, but expressed as a distance from a reference you
had to reconstruct yourself, which is a large part of why it gets read as the
cost of the trade. Each level now states both prices outright:

* **`price`**: what one whole `tokenIn` buys at this level's executed average
* **`marginalPrice`**, the price the bound actually names: `costBps` below this
  level's own reference
* **`quoteDecimals`** on the response, so either can be rendered

Measured on Base at block 50769843, USDC to WETH:

| `costBps` | `price`              | `marginalPrice`      |
| --------- | -------------------- | -------------------- |
| 5         | 0.000414170747810985 | 0.000414063024200179 |
| 10        | 0.000413944693557095 | 0.000413611659634596 |
| 25        | 0.000413633807526173 | 0.000412990621106616 |
| 50        | 0.000412799098369107 | 0.000411324597142097 |

`marginalPrice` is worse than `price` at every level, and it has to be: the pool
is *stopped* at the bound, and the average it delivered on the way there is
better. The gap between the two columns is exactly what `avgCostPips` measures.

Both are **in the direction of the trade and never inverted**. A price that
flips to the more human-looking side needs a buy/sell flag to say which way it
went, and a caller who reads that flag wrong is out by the square of the rate.
Ask for the other direction instead: `tokenIn=WETH` on the same pair reads
`2412.991864` USDC per WETH with `quoteDecimals: 6`.

<Warning>
  `price` and `marginalPrice` are **display only**. The `(maxAmountIn, amountOut)`
  pair is the contract; these are rounded decimal renderings of it. Size an order
  from the pair: round-tripping through the decimal loses wei.
</Warning>

### Cost excludes the LP fee, on purpose

The fee is unavoidable and identical at every size, so folding it into "cost"
would report a floor nobody can trade below as though it were something the
trader chose. `costBps` is measured against the best rate the pool could possibly
give; its spot rate **already net of the fee**.

### Each level is measured against its own pool

`avgCostPips` is computed against a dust trade on **that level's winning pool**,
and the pair is published as `referenceAmountIn` / `referenceAmountOut`. The
top-level reference is the best rate across every candidate, which is right for
pricing levels against each other and wrong for pricing a fill.

Measuring a fill that routed through a 3 bp pool against a reference taken from
a 1 bp pool folds 2 bps of pure fee difference into the answer and calls it
cost. A fill test read 1,567 pips of "cost" on an 80,000 USDC trade that way, of
which roughly 200 was fee difference and none of it was a charge anyone made.

## Basis points do not add across fills

The second recurring error, and it points the same way.

Four fills that each cost 5 bps of their own notional cost **5 bps blended**, not
20\. It is a rate on notional, not a counter. But the correction that matters more
is the one underneath it:

**Splitting an order across time saves nothing unless the price recovers between
fills.** Four back-to-back swaps on one pool are mathematically identical to one
swap of the total: the second starts wherever the first left the price. This is
AMM path independence, and it is exact.

Measured at block 50769511, USDC to WETH:

```
4 x 50,000 USDC with full price recovery : 82.810819 WETH
1 x 200,000 USDC in one fill             : 82.795394 WETH
difference                               :  0.015425 WETH  = $37.25
```

So the whole prize for splitting $200k into four rested bites is **$37.25\*\*, or
1.86 bps, and it is collected only if arbitrage pushes the pool back toward the
external mid between bites. Back to back, it is exactly zero.

<Note>
  1.86 bps is seconds of ordinary ETH movement. An order that waits hours for
  rungs to refill is exposed to drift that dwarfs what patience earns. Splitting
  across **pools** is free; splitting across **time** is a trade.
</Note>

## Splitting across pools: a sum, not a maximum

Routing was single-path, so at a given bound the service reported the **best
single pool's depth** and discarded every other pool's depth at the same price.

That is the wrong aggregation for a bounded order, and the reason is a one-line
argument. For a bound P the constraint is **per pool**: no pool may execute past
marginal price P. Pools share no state, so the feasible set is a product and the
problem separates. Each pool's maximum is the amount driving *its* marginal price
to exactly P, and the total is the sum. There is nothing to search, and the code
already computed every term, because `fills_at_limits` probes every candidate at
a common absolute rate in one batched call. The `max` on the next line threw the
rest away.

An earlier experiment had measured splitting and concluded the prize was "3–10
bps, and only above \~\$1M". That was correct for the question it asked: at a
*fixed size*, does splitting get a better average price? A bounded order asks the
opposite question. The price is fixed by construction, so the average cannot
improve and there are no bps to win. **What splitting buys is quantity.**

Measured through the real quote path at block 50769511: 2,000,000 USDC, bound
25 bps below spot:

|        | `allowSplit=false`   | `allowSplit=true`     |
| ------ | -------------------- | --------------------- |
| filled | 945,639 USDC (47.3%) | 2,000,000 USDC (100%) |
| legs   | 1                    | 9                     |
| rate   | 2,420.25 USDC/WETH   | 2,420.62 USDC/WETH    |

**+111.5% fillable size, for 1.54 bps of rate.** The nine leg weights sum to
exactly 10,000, and leg 3 is the *same pool at the same 945,639* that the
single-pool answer returned: the split adds eight pools on top of it rather
than replacing it.

The uplift is a capacity number, not a benefit number. It is worth nothing to an
order the best single pool could already swallow whole. The decision-relevant
figure is how much of an order actually fills (recorded in the service's own
`split_value_by_size` run, live Base, 9 routable pools):

```
     order            5 bps           10 bps           25 bps           50 bps
----------  ---------------  ---------------  ---------------  ---------------
       10k      100% = 100%      100% = 100%      100% = 100%      100% = 100%
      100k      45% -> 100%      100% = 100%      100% = 100%      100% = 100%
      250k       18% -> 41%      45% -> 100%      100% = 100%      100% = 100%
      500k        9% -> 21%       22% -> 52%      63% -> 100%      100% = 100%
     1000k        5% -> 10%       11% -> 26%       31% -> 74%      100% = 100%
```

`=` means the splitter changed nothing. Read as a product statement: the
splitter turns **partial fills into complete fills**, and it earns its keep
exactly where a trader asks for a good price. Below roughly \$45k it does nothing
at any bound; at bounds of 50 bps or looser it does nothing until multi-million
sizes.

<Warning>
  `allowSplit` defaults to **false** in the service, and that is deliberate. A
  split quote handed to a client that reads only the scalar `execution` fields
  would execute one leg and fill a fraction of what it was quoted. Opting in is
  the client declaring that it reads `fill.execution.legs`. It also requires
  `limitRate`: without a bound there is nothing for the legs to stop at, and the
  request is refused.
</Warning>

**DeFiLoops sends it on every limit route.** Not a plan parameter: at a fixed
rate, filling more of the owner's own order is what they asked for, so it is the
default rather than a flag nobody sets. Measured on Base: an offer of 450,000
USDC at the rate a 150,000 fill fetches took 74,994 through the best single pool
and **145,572 across eight**.

Three things had to be true first, and the flag is the client's declaration that
they are:

|                          |                                                                                                                                                                                                         |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| one limit per leg        | every leg stops at the same RATE, but the `sqrtPriceLimitX96` expressing it differs per pool, because each pool's LP fee is undone against its own. Matching one winner leaves the other legs unbounded |
| the total, not `legs[0]` | the scalar `execution.amountIn` describes the first leg. Sizing calldata from it spends one pool's share and reports the whole order filled                                                             |
| every leg routed         | a fill across eight pools and a route naming one executes an eighth, reverts nothing, and reports success                                                                                               |

Each of those failures is silent. That is why the service ships the flag off and
makes the client say otherwise.

### Seeing the split before you commit to a size

`allowSplit` executes a split, but you have to have chosen a size first. The
ladder now answers the prior question: **how much is there, across everything, at
this bound?** Each level carries `pools[]`: every pool that answered, biggest
slice first, the winner included: plus `splitAmountIn` and `splitAmountOut`, the
sums across them.

This costs no extra work. `fills_at_limits` already probes every candidate at
every level in one batched round; the `max` that picks the single-path winner
used to discard the rest. Measured on Base at block 50769722:

| `costBps` | `maxAmountIn` (best single pool) | `splitAmountIn` (all pools) | uplift  | pools |
| --------- | -------------------------------- | --------------------------- | ------- | ----- |
| 5         | 758 USDC                         | 805 USDC                    | +6.2%   | 2     |
| 10        | 164,661 USDC                     | 297,772 USDC                | +80.8%  | 8     |
| 25        | 769,933 USDC                     | 1,638,645 USDC              | +112.8% | 9     |
| 50        | 2,153,466 USDC                   | 4,301,968 USDC              | +99.8%  | 10    |

Each slice carries its own `maxAmountIn`, `amountOut`, `saturated`, `zeroForOne`
and reference pair: `zeroForOne` because it follows the pool's own sorted token
order and a client recomputing it gets it backwards for half the pools.

<Warning>
  `splitAmountIn` is **not executable from this endpoint**. `/v1/sizing` builds no
  routes: size an order to it, send a plain `/v1/quote`, and you get the
  single-pool fill and a silent partial. Reaching it requires `limitRate` together
  with `allowSplit`. It is also a **lower bound**: direct concentrated pools only,
  so it ignores the multi-hop routes a quote can reach.
</Warning>

That last clause is not a hedge. The same test, run against two forks on the
same day, landed on both sides of it:

| the router's single path | outcome for a split derived from sizing |
| ------------------------ | --------------------------------------- |
| direct, one hop          | **+235 pips**: the split wins           |
| **two hops**             | **−150 pips**: the single path wins     |

When the best single path is multi-hop, a split assembled from direct pools
cannot reach the route the quote used, and loses to it. So `splitAmountIn`
answers "how much depth sits in the direct pools at this bound", which is the
right question for sizing a bounded order, and the wrong one for picking a
route. Compare against a real quote before assuming the split is better.

### One claim that was corrected

An early version of this work claimed the blend "can never price worse than the
best single pool". That is true only **at equal volume**. A split that fills
112% more is necessarily deeper into the book: 1.54 bps in the run above, 0.10
bps in another. The guarantee the product makes is the **bound**, and every leg
stops at it. Filling more costs rate, and that is the trade.

## Three constraints, routinely confused

They are not variations of one idea. They are a tolerance, a rate, and a cap.

| knob           | what it is                                                                                                                                                           |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `slippageBps`  | A percentage of whatever the market is **at quote time**. It derives a floor and protects the trip to the chain. It is not a price anybody chose.                    |
| `limitRate`    | A rate the pool **stops at**: an `amountIn` / `minAmountOut` pair plus `allowPartialFill`. The pool consumes what clears at that rate or better and leaves the rest. |
| `maxImpactBps` | A ceiling on the trade's own footprint. It **shrinks the offer** until the reported impact fits, rather than stopping the pool at a price.                           |

`slippageBps` and `limitRate` together are **refused**: with a limit, the floor
*is* the caller's own rate, and a second floor derived from the market is a
different number pretending to be the same one. `limitRate` and `maxImpactBps`
may be sent together: the limit stops the fill at a price, the cap shrinks the
offer, so both can bite at once, and `boundBy` names which one did:
`"limit"`, `"impact"`, `"both"`, or empty.

`sizedDown` is present when `maxImpactBps` made the trade smaller rather than
refusing it. The quote is complete and buildable either way; the field only
explains why it is not the size that was offered.

A plain floor with **no** `limitRate` is a fourth thing again; it is checked at
the end, so it is fill-or-kill. An order for \$1m whose price supports 40% moves
nothing at all. That is why a plan's `limit_price` reaches the router as a
`limitRate` only when the step is partially fillable, and takes the plain route
otherwise.

### `costBps` and `maxImpactBps` are not the same knob

Both take a number in basis points and both hand back a size, which is exactly
why they get substituted for one another. They differ on **basis** and on
**mechanism**.

Both can be sent to `/v1/quote`. `costBps` there does what `/v1/sizing` does:
the same ladder, at one level, and then prices the size it arrived at, so the
`minAmountOut` you get back belongs to the `amountIn` you get back. Sizing on
one call and pricing on another does not have that property, and the gap is not
theoretical: a floor derived from the amount you asked for is one a smaller
trade can never clear.

|                        | `costBps`                                                                | `maxImpactBps`                              |
| ---------------------- | ------------------------------------------------------------------------ | ------------------------------------------- |
| endpoint               | `/v1/sizing`, and `/v1/quote` as a bound                                 | `/v1/quote`                                 |
| basis                  | **Marginal**: where the last unit trades                                 | **Average**: the whole trade's footprint    |
| you supply             | a cost                                                                   | a size *and* a cap                          |
| you get back           | the largest size that fits                                               | a **smaller size** than you offered         |
| mechanism              | stops the **pool** at a price limit                                      | shrinks the **offer** until impact fits     |
| cost to compute        | **one probe per level**: the pool stops itself and reports what it moved | **a bisection**: a full quote per iteration |
| directly comparable to | nothing: use its `avgCostPips`                                           | `priceImpactBps`                            |
| tells you it bound     | `saturated` is false                                                     | `sizedDown` is present                      |

That "cost to compute" row is not academic. Sizing gets its whole ladder from one
batched round of probes, because a price limit is something the pool enforces on
itself. `maxImpactBps` has no such trick; it bisects the offer and takes a real
quote at every step, so a very large offer against a tight cap is slow enough to
time out. Measured here: an 8,000,000 USDC offer never returned; 1,000,000 did.

The consequence that costs people money: **the same number is not the same
request.** Because `costBps` is marginal and `maxImpactBps` is average,
`costBps=25` admits a trade whose average footprint is about 10 bps, so
`costBps=25` is roughly `maxImpactBps=10`, not `maxImpactBps=25`.

Read down the `avgCostPips` column of the ladder to convert between them. At
block 50769564 on USDC to WETH that mapping was:

```
costBps  10  ~  maxImpactBps   2.5
costBps  25  ~  maxImpactBps  10
costBps  50  ~  maxImpactBps  14
costBps 100  ~  maxImpactBps  39
```

Measured independently: a 1,000,000 USDC offer shrunk by `maxImpactBps`, against
the ladder's own rungs on the same pair:

| knob           | setting | size returned    | impact delivered |
| -------------- | ------- | ---------------- | ---------------- |
| `maxImpactBps` | 2       | 156,578 USDC     | 194 pips         |
| `maxImpactBps` | 5       | 391,445 USDC     | 485 pips         |
| `maxImpactBps` | 10      | **803,244 USDC** | **995 pips**     |
| `costBps`      | 25      | **818,992 USDC** | **997 pips**     |

The last two rows are the same trade reached from opposite directions: one asked
for a 10 bps *average* cap, the other for a 25 bps *marginal* bound, and they
landed within 2% of each other on size and within 2 pips on cost. That is the
conversion, confirmed rather than argued.

Note also how tightly `maxImpactBps` lands under its own cap: 194 against 200,
485 against 500, 995 against 1000. The bisection converges on just-under, which
is what makes it expensive.

There is one more asymmetry worth holding onto. `maxImpactBps` **cannot make a
trade bigger**; it only ever shrinks what you offered, so it needs you to have
picked a size first. `costBps` has no offer to shrink; it answers the sizing
question outright. If you already know your size and want it capped, use
`maxImpactBps`. If you are asking how much to trade at all, use `/v1/sizing`:
reaching for `maxImpactBps` there means guessing a size and then discovering the
guess was wrong.

## The shape of the book, not just its depth

The ladder also reports where the curve bends, because "how much" and "through
what" are one decision.

* **`segments`**: runs of consecutive levels served by the same pool. The curve
  is the lower envelope of the per-pool curves, so it kinks wherever the winner
  changes.
* **`routeChanges`**, a segment boundary. **Informational, never a warning**: a
  crossover is usually the router finding a better venue, so the envelope's slope
  *improves*. Flagging it as a cliff would warn the trader at the exact point the
  routing got better. Both pools are carried, because a deep pair usually has
  several pools at different fee tiers and a real crossover otherwise renders as
  "univ3 to univ3" and reads as a bug.
* **`knee`**, where cost starts buying materially less size. Detected **inside**
  a segment and never across one, at a threshold of 60% of the size-per-bps the
  segment opened with.
* **`saturated`**: the pool absorbed everything offered and would have taken
  more. The number is then a **floor, not a maximum**. This is the one failure
  mode of the endpoint that looks entirely correct.

## Partial fills, and the literal that shrinks with the bite

A resting order fills in **bites**. Each bite runs the whole downstream pipeline
at a fraction of full size, so every amount in the plan has to shrink with it.

Symbolic references shrink for free: a step spending "what the swap produced"
is automatically a tenth when the swap was a tenth. Literals do not, and the
sizing **refuses the plan** rather than running one step at the number the
document wrote while the money around it moved. A bite of a tenth executing the
whole trade is not an error anybody would catch by reading the plan.

But symbolic is not always available. Repaying WETH bought with USDC and then
taking back cbBTC collateral has no step producing cbBTC to point at, and
pointing at the WETH would withdraw a WETH-shaped number of a token with
different decimals and a different price.

So `ValueRef.ScalesWith` names a step whose **sizing factor** a literal follows.
The author states the amount for the whole order and says what it scales with:
at full size it is itself, at a tenth it is a tenth.

```json theme={null}
{
  "ID": "takeback",
  "Chain": "base",
  "Amount": { "Literal": "20000000", "ScalesWith": "buy" },
  "Params": { "asset": "cbBTC", "max_ltv_bps": "6000" },
  "Adapter": { "ID": "lend.withdraw", "Version": { "Major": 1, "Minor": 1 } }
}
```

0.2 cbBTC is the take-back at **full** order size. `ScalesWith: "buy"` makes a
bite that filled a tenth withdraw a tenth.

`ScalesWith` is preserved through compilation rather than consumed by it. A plan
is stored and re-read for every bite, so a rewritten amount that lost its
`ScalesWith` would make the *next* bite scale from this bite's number instead of
the order's.

## Saying it in a plan

Everything above is the router's vocabulary. A plan reaches it through three
parameters on `swap.router`, and picking between them is the whole of the skill.

| plan parameter   | router knob              | who chooses the size                     |
| ---------------- | ------------------------ | ---------------------------------------- |
| `limit_price`    | `limitRate`              | the pool: it fills what clears and stops |
| `max_impact_bps` | `maxImpactBps`           | the router: it shrinks what you offered  |
| `max_cost_bps`   | `costBps` on `/v1/quote` | the book: it is asked before you choose  |

`limit_price` is a rate written as two whole numbers, `"out units/in units"`,
never a decimal. A rate as a float has to be reconciled with two token
decimalisations somewhere, and every place that happens is a place a limit can be
silently multiplied by a million.

### Buy the dip at a price you named

You want cbBTC, but only at 80,000 or better. The price is the point; the size is
whatever the market gives you there.

```json theme={null}
"Params": {
  "asset_in": "USDC", "asset_out": "cbBTC",
  "max_slippage": "100",
  "limit_price": "1250000/1000000000",
  "partial": "true"
}
```

1,000 USDC must buy at least 0.0125 cbBTC. With `partial` true this becomes a
resting order that takes what clears and comes back for the rest; with it false
the trade is fill-or-kill at that rate.

**Reach for this whenever a person said a number.** It is the only one of the
three that the chain enforces: it becomes the signed `MinOut`.

### Rebalance without moving the market

A treasury shifting 2,000,000 USDC into WETH does not care about a specific
price. It cares that the trade does not print a candle.

```json theme={null}
"Params": {
  "asset_in": "USDC", "asset_out": "WETH",
  "max_slippage": "10",
  "max_impact_bps": "5"
}
```

Offer the two million; the router shrinks it until the impact it reports is at
most 0.05% and quotes that smaller trade. `sizedDown` on the answer says it did,
and `boundBy` says the cap is why.

**Reach for this when somebody says "do not move the market".** No price is
named, so you take the market rate for whatever size fits.

### Trade whatever fits cheaply, when nobody knows what fits

An agent told to convert "as much as is sensible" has no size to offer. Asking
the book first is the honest answer.

```json theme={null}
"Params": {
  "asset_in": "USDC", "asset_out": "WETH",
  "max_slippage": "10",
  "max_cost_bps": "5"
}
```

The plan is quoted with `costBps: 5`, which returns the largest trade that fills
within 5 bps **and the floor for that trade**, and both are signed. On one
measurement of Base the size was 58,360 USDC, and 12,264 twenty minutes later,
because trades in between had moved the pool.

Both numbers come from one call on purpose. The floor is derived from the amount
that is quoted, so a size chosen afterwards is always too late for it: sizing
with `/v1/sizing` and then signing a floor for the original amount produces a
bound the smaller trade cannot clear, and the swap reverts `InsufficientOutput`.

**Reach for this when the size is the unknown.** It is the only parameter that
answers *how much*, and the only one that asks the book rather than telling it.

### They compose

Any combination is allowed except `limit_price` with `min_out`, which is refused
because the two can disagree and picking a winner would trade at a price nobody
chose. A resting order that wants all three says: fill no worse than this rate,
take no more than the book offers at 5 bps, and do not move the pool more than 10
bps getting there.

```json theme={null}
"Params": {
  "asset_in": "USDC", "asset_out": "WETH",
  "max_slippage": "10",
  "limit_price": "413000000000000000000/1000000000000",
  "max_cost_bps": "5",
  "max_impact_bps": "10",
  "partial": "true"
}
```

### Only one of them is a promise

`limit_price` becomes `MinOut`, which is in the signed intent and enforced on
chain: a fill below it reverts. The two size bounds are not signed and do not
need to be; they can only make a trade **smaller**, and spending less than the
signed amount is what a partial fill already is.

That asymmetry is why a plan cannot use a size bound to spend more than it asked
for, and why adding them moved the wire schema between signer and worker without
touching the EIP-712 type or any adapter address.

**Zero is refused for both size bounds.** Zero impact and zero cost each describe
a trade nothing fills, so a plan carrying one would look bounded while being
unfillable. Omit the parameter to mean "no bound".

## What is built, and what is not

|                                                                           | state                                                                                                   |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `/v1/sizing` ladder, marginal and average cost, per-level reference       | Built                                                                                                   |
| `segments`, `routeChanges`, `knee`, `saturated`                           | Built                                                                                                   |
| Multi-pool bounded split via `allowSplit`                                 | Built. Off by default in the service; **DeFiLoops sends it on every limit route**, requires `limitRate` |
| Split proven on the deployed router, not only probed                      | Yes: forked at the plan's own block, attached to the live proxy, both routes executed                   |
| `splitAmountIn`, `splitAmountOut` and per-level `pools[]` on `/v1/sizing` | Built. The ladder now publishes every pool that answered at a level, not only the winner                |
| `costBps` as a bound on `/v1/quote`                                       | Built. One call sizes and prices, so `minAmountOut` belongs to the `amountIn` it comes back with        |
| Multi-hop splits                                                          | Out of scope: deliberately                                                                              |
| MEV protection anywhere in the path                                       | Open question, not built                                                                                |

<Note>
  The split proof is executed rather than modelled: a fork at the same block the
  plan was computed at, attached to the **live proxy** rather than a fresh deploy,
  funding a trader and running both routes for real. It filled 92% more at a
  marginally better price, and the predicted total missed the executed total by
  0.03%: entirely `portionBps` rounding.
</Note>


## Related topics

- [Running it without you](/automation.md)
- [Does splitting cost less?](/splitting.md)
- [DeFiLoops](/index.md)
- [Slippage is not impact](/slippage.md)
- [Non-custodial, and unilateral control](/kernel.md)
