Why a direct pool earns no swap fee

There is no integrator-fee hook on a raw AMM router — what that means for your revenue, why a wrapper contract was rejected, and the four checkable conditions under which this decision gets revisited.

3 min readUpdated 4 August 2026reference, fees, revenue, amm, decision

A swap through a direct pool pays you nothing. Not a misconfiguration, not a setting somebody forgot — a property of the contract the swap goes through.

This page exists because that sentence is surprising, and because an operator who does not know it will look for the missing fee in the settings, find a dexFeeBps that is set, and conclude the product is broken.

The mechanism

feeRecipient and feeBps are an aggregator settlement-contract feature. 0x, 1inch and Odos run their own contracts between you and the liquidity, and those contracts take a cut and forward it. That is where the integrator fee comes from.

A Uniswap V2 router's swapExactTokensForTokens takes an amount, a minimum, a path, a recipient and a deadline. There is nowhere to put a fee, because the router is not a settlement layer — it is a thin wrapper over the pair contract's swap(). The same is true of exactInputSingle on a V3 router.

So on a direct route the fee resolver short-circuits before it reads the chain's feeRecipient at all, and reports VENUE_NO_FEE_HOOK. A chain with no fee recipient configured is a perfectly valid configuration for a direct-only pair, and refusing to quote one would be a bug.

What you do earn

The pool's own LP fee, on the liquidity you provided — 0.30% on a canonical V2 pair, 0.25% after the protocol switch, 0.17% on PancakeSwap V2, and whichever tier you chose on V3.

It is not platform revenue when it is earned. It becomes revenue when you withdraw and the withdrawal is recorded, and on a V2 pool it never can be — see Liquidity positions.

Why not a wrapper contract

The obvious fix is a small contract that takes the fee and forwards to the router. It was rejected, and the reasons are not stylistic:

It breaks the line the whole product is scoped around

This addon deploys zero contracts. That is the first sentence of its security statement, the thing an auditor checks first, and the reason it can claim to be non-custodial without qualification. A fee wrapper is a contract we wrote, we deployed, and users' funds pass through.

It moves funds through code we own

A wrapper sits between the user and the pool holding their tokens mid-transaction. Every bug in it is a bug that loses somebody else's money, and it would need an audit, a monitored upgrade path and an incident procedure — none of which this product has, because it has never needed them.

It is a new attack surface per chain

One deployment per chain, each with its own address to allowlist, verify and keep in a registry. A wrong address in that registry is a stranger's contract receiving the swap.

The revenue it would collect is small

A direct pool exists for pairs no aggregator will quote. Those are, by definition, the thinnest markets on the platform, and a percentage of very little is very little — measured against the cost above.

When this gets revisited

Uniswap's V3 SwapRouter02 carries sweepTokenWithFee, which can take a fee in a multicall composition. It is not used, and the four conditions for using it are written down here so that "we should look at this again" is a checkable statement rather than a feeling:

  1. A fork test proves the multicall composition on the target deployment, with the output landing at the user and the fee at the recipient — not a testnet, not a mock, the deployment that will actually be used.
  2. A fork test proves the failure path leaves no tokens recoverable by a third party. A half-executed multicall that strands funds in the router is a worse outcome than earning nothing.
  3. receipt.ts verifies a multicall shape. Today's verifier asserts receipt.to === router and a single Swap event from the quoted pool. A multicall invalidates both, and a receipt check that cannot describe the transaction it is checking is not a check.
  4. supportsFeeSweep is a per-deployment probed flag, never a global constant. Forks of the router do not all carry the function, and assuming they do turns a missing method into a reverted swap the user pays gas for.

Until all four hold, a direct route earns exactly zero by the nature of the router — which is a fact to state plainly, not a limitation to work around.

The settings that do apply

Setting What it does on a direct route
dexFeeBps Nothing. There is nowhere to collect it
dexDirectPoolMaxImpactBps Refuses a trade that would move the pool more than this
dexDirectPoolMaxSlippageBps Caps the tolerance written into the calldata
dexDirectPoolMinLiquidityUsd Refuses a pool too thin to trade against
dexDirectPoolWinMarginBps How much better a pool must be to beat an aggregator

The last one is the interesting one. A direct pool that ties with an aggregator should lose, because the aggregator route carries no conflict of interest — so the margin prices that conflict rather than pretending it is not there.