# v4 fees and rules

Everything on this page is read from the hook: `terms(poolId)` for what the launch fixed, and the rate views for what a swap pays now. All rates are in basis points: `100` is 1%.

## What a swap pays

```
base   = poolFeeBps + (buy ? buyTaxBps : sellTaxBps)
rate   = base, or higher while the opening window runs        // feeRates(id, isBuy) → (rate, base)
total  = min(rate + sizeRate(id, amount) + devRate(id, isBuy, spotTp), 9900)    // 9000 on release 2
fee    = amount × total / 10000                                // in the pair asset
```

`amount` is always the pair asset: what a buy brings in, or what the pool pays a sale out, before the fee.

- **A buy of X pair asset:** the hook takes `fee` from X and the pool trades `X − fee` for tokens.
- **A sale:** the pool pays out G pair asset for the tokens; the hook takes `fee` from G and the seller receives `G − fee`.

| Part | Terms field | Range | Applies to |
| --- | --- | --- | --- |
| Pool fee | `poolFeeBps` | 0 to 500 | Buys and sales |
| Buy tax | `buyTaxBps` | 0 to 500 | Buys |
| Sell tax | `sellTaxBps` | 0 to 500 | Sales |
| Opening fee | `sniperStartBps`, `sniperSeconds` | Up to 9900 for up to 5,400 s (releases 4.1 and 4.2); up to 9000 for up to 600 s (release 2) | **Buys and sales** |
| Size extra | `blocks.sizeFeeBps`, `blocks.sizeThresholdBps` | Up to 500 | Trades large against the pool |
| Deviation extra | `blocks.devFeeBps` | Up to 500 | Trades that push the price away from its average |

The factory refuses a launch where pool fee + the larger tax + size extra + deviation extra exceeds **1,500**. Outside an opening window no swap pays more than 15%.

### The opening fee

If `sniperStartBps` is above the base rate, the rate starts there at `launchTs` and falls in a straight line to the base rate at `launchTs + sniperSeconds`:

```
rate = base + (sniperStartBps − base) × (sniperSeconds − elapsed) / sniperSeconds     // while elapsed < sniperSeconds
```

It is charged on sales exactly as on buys. A user who buys at second 30 of a 60-second, 90% opening and sells ten seconds later pays the opening rate twice. `feeRates(id, isBuy)` returns the live `rate` with the opening in it and the `base` it is falling to. Show both, and the seconds left.

### The size extra

`sizeRate(id, amount)` compares the trade with `poolState(id).reserve`, the hook's running count of the pair asset in the pool: buys add to it, sales take from it, the buyback and the bid add to it. Below `sizeThresholdBps` of the reserve it is zero. At twice the threshold it is the full `sizeFeeBps`. Between the two it rises in a straight line. While the reserve is zero (before any buy) it is zero.

For a sale, `amount` is the pair asset the pool pays out, which is known only once the swap is simulated. Simulate the sale, or bound the extra with `sizeFeeBps`.

### The deviation extra

The hook keeps a moving average of the pool's price tick (`prices(id)` returns `spotTp` and `emaTp`; the average moves toward the spot over 600 seconds and never inside the second a swap lands). A buy while the spot is above the average, or a sale while it is below, pays an extra that rises in a straight line to the full `devFeeBps` at 1,000 ticks (about 10.5%) of distance. A trade that brings the price back toward its average pays none.

Read off-chain, `devRate` uses the average as stored at the pool's last swap. The swap itself first moves the average toward the price by the time elapsed, so the view is an upper bound on this extra: a pool idle for ten minutes can show the full extra and charge none. Simulate the swap for the exact figure.

### A module

A launch may be born with a module (`moduleOf(id).impl` is non-zero): a contract the hook asks, read-only and with its own gas, for the base rate of each swap. The hook clamps the answer into a band fixed at launch (`floorBps` to `ceilingBps`, and the ceiling plus both extras stays within 1,500). `feeRates` reports the launch's own base, not the module's answer, so on such a pool simulate the swap to know the rate. A module can never block a sale, move funds or change liquidity.

## Rules that can refuse a buy

Each is optional, set at launch, and ends by itself. None of them ever applies to a sale.

| Rule | Terms fields | While it runs | Revert |
| --- | --- | --- | --- |
| Signal-only buys | `blocks.gateSeconds` (up to 86,400) | A buy must come through a router the factory approved | `Gated()` `0xfb727798` |
| Wallet cap | `blocks.capBps` (10 to 1,000 = 0.1% to 10% of supply), `blocks.capSeconds` (up to 86,400) | The buy's recipient may not end above the cap. The recipient is the one an approved router names, so a buy through any other router is refused | `WhaleCap()` `0xb11c7524` |
| Per-block budget | `blocks.budgetBps` (10 to 1,000), `blocks.budgetSeconds` (up to 86,400) | All buys in one block, from every wallet and router together, may take at most this share of the supply | `OverBudget()` `0x323e8637` |

Each window starts at `terms.launchTs`. A terminal that routes through its own contracts should send buys through Signal's router (see [quote and trade](v4-trading.md)) while a Signal-only window or a wallet cap runs, or tell the user the buy opens when the window ends. The router does not lift the wallet cap itself, and the per-block budget binds every router: size the buy inside them. The three errors decode from the [hook ABI](v4-deployments.md#interfaces).

Two refusals have no window: `ExactOutputUnsupported()` `0xf7ebfa18` for an exact-output swap, and `PartialFill()` `0xd964f528` for a buy the pool could not fill whole.

## Where the fee goes

Fees collect in the PoolManager as the hook's claim until someone calls `flush(poolId)`. Anyone may. A flush divides what has collected:

1. **Signal's share** comes first: `terms.protocolBps` of the fee, fixed for the pool at launch. On releases 4.1 and 4.2 it is taken from the extras as well as the base.
2. **The base fee's remainder** is divided by `terms.split`, which sums to 10,000: `creator`, `holders`, `liquidity`, `burn`.
3. **The extras' remainder** (opening above base, size, deviation) goes where `terms.surplusTo` says: `0` holders, `1` burn, `2` liquidity.

| Destination | What happens |
| --- | --- |
| Creator | Paid in the pair asset to `terms.creatorPayout`. Before that, a pot (`blocks.potBps` of the creator part) and up to four named co-recipients (`terms.legs`) take their shares. With `blocks.vestSeconds` the creator's part is released in a straight line from launch |
| Holders | Sent to the token itself. A holders token releases each arrival to its holders over five minutes; a holder claims with `claim()` or anyone pushes with `push(address[])` |
| Liquidity | Added to the pool as a permanent bid under the market |
| Burn | Buys the token from the pool and sends it to `0x…dEaD`, a bounded slice at a time (`blocks.burnCapBps` of the waiting amount, no more often than `blocks.burnMinInterval` seconds, and only while the price is near its average) |

**The pot** is won by every `blocks.potNth`-th counted buy, inside that buy, and only when the buy came through a router the factory approved, at any time in the pool's life. A buy through any other router takes the number and wins nothing; the pot waits for the next Nth. A buy whose own fee is below the pot's average contribution does not count toward the Nth.

A payout an asset refuses (a blocked recipient, a paused asset) does not stop the flush. It is recorded as owed (`owed(recipient, asset)`) and delivered by `withdrawOwed(recipient, asset)`, which anyone may call.

The pool's running totals are `stats(id)` (paid, by destination) and `poolState(id)` (`pendingBase` and `pendingSniper` waiting for a flush, `feesTotal` ever collected, `reserve`). `stats` counts an owed payout as paid to its recipient. To know what was delivered, read the events in [events and accounting](v4-events.md).

## What can change after launch

The rates and every rule above are fixed for the pool's life. Two things can move, both in the open:

- **Where the fee goes.** The launcher (`creatorOf(id)`) can propose a new `split` or new co-recipients. A proposal is announced (`SplitProposed`, `LegsProposed`) and can be switched on by anyone only after 24 hours (`activateChanges(id)`), with fees already collected divided first under the old destinations. Limits fixed at launch still hold: the creator part can never exceed its share at launch, holders can be paid only by a holders token, and a burn needs the buyback guard the pool was born with. `nextChanges(id)` returns open proposals.
- **A module's implementation**, on a pool born with a module slot. A new implementation is proposed by the launcher, attested by a key the factory owner names (`prover()`), then active after 24 hours. On 12 October 2026 `prover()` is the zero address on the 4.2 hook, so no new module can activate. Switching a module **off** needs no attestation: the launcher can propose it and, 24 hours later, the pool pays its launch base rate, which lies inside the same band.

## From the API

`/api/token/<address>` carries the same facts for a v4 token: `v4_launch.terms` (read from the hook at the launch block; on releases 4.1 and 4.2 a later change of destinations replaces `terms.split` and `terms.legs`, and the launch values move to `terms_birth`), and `v4_launch.state.buy_rate` and `sell_rate` (`{rate, base}` as last read, with `state_read_at`). During an opening window the cached rate is older than the chain. Read the hook.
