Build with Signal
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 assetamount 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
feefrom X and the pool tradesX − feefor tokens. - A sale: the pool pays out G pair asset for the tokens; the hook takes
feefrom G and the seller receivesG − 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 < sniperSecondsIt 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) 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.
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:
- Signal's share comes first:
terms.protocolBpsof 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. - The base fee's remainder is divided by
terms.split, which sums to 10,000:creator,holders,liquidity,burn. - The extras' remainder (opening above base, size, deviation) goes where
terms.surplusTosays:0holders,1burn,2liquidity.
| 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.
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 newsplitor 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 2026prover()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.