← All posts
April 29, 2026·11 min read·EN

Tutorial: add a 0.10% protocol fee on swap output using Uniswap V3 SwapRouter02's built-in sweepTokenWithFee and unwrapWETH9WithFee — no audited contract required.

How to Add a Protocol Fee to a Uniswap V3 Frontend (No Custom Contract)

When I started building BasedSwap — a multichain DEX frontend — the first revenue model question was: how do I take a small fee on every swap without deploying my own audited contracts?

It turns out Uniswap's V3 SwapRouter02 has this exact mechanism built in, and it's underdocumented. This post walks through how it works and how to integrate it.

The mechanism: sweepTokenWithFee and unwrapWETH9WithFee

Uniswap V3's SwapRouter02 supports a "multicall" pattern: you can chain multiple operations into a single transaction. Two of those operations are sweepTokenWithFee (for ERC-20 outputs) and unwrapWETH9WithFee (for native ETH outputs), which split the output between the recipient and a fee recipient.

Function signatures:

function sweepTokenWithFee(
  address token,
  uint256 amountMinimum,
  address recipient,
  uint256 feeBips,
  address feeRecipient
) external payable;

function unwrapWETH9WithFee(
  uint256 amountMinimum,
  address recipient,
  uint256 feeBips,
  address feeRecipient
) external payable;

feeBips is the protocol fee in basis points, capped at 100 (1.0%). The function transfers amount * feeBips / 10000 to feeRecipient and the rest to recipient.

Why this is great for indie projects

  • No custom contract. You don't need an audited contract. SwapRouter02 has been battle-tested with billions in volume.
  • No liquidity. You're using Uniswap's existing pools.
  • Same address across chains. SwapRouter02 has the same deployment address on most major chains, so your code is portable.
  • Built-in cap. 100 bips max means you can't accidentally charge an absurd fee — Uniswap rejects anything higher.

The catch

You can't use exactInputSingle directly with this pattern. The trick is:

  1. Call exactInputSingle with recipient = ADDRESS_THIS (a special sentinel meaning "router holds the tokens")
  2. In the same multicall, call sweepTokenWithFee to disburse the held tokens

If your exactInputSingle sends the tokens directly to the user, the router doesn't have them anymore and sweepTokenWithFee reverts.

The integration code

Here's the pattern in viem (TypeScript):

import { encodeFunctionData } from "viem";

const FEE_BIPS = 10n; // 0.10%
const FEE_RECIPIENT = "0xYourFeeWallet";
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";

// Step 1: encode the swap, with router as recipient
const swapCall = encodeFunctionData({
  abi: SWAP_ROUTER_ABI,
  functionName: "exactInputSingle",
  args: [{
    tokenIn,
    tokenOut,
    fee: poolFeeTier,
    recipient: ADDRESS_THIS,    // ← router holds tokens
    amountIn,
    amountOutMinimum,
    sqrtPriceLimitX96: 0n,
  }],
});

// Step 2: encode the disbursement with fee
const sweepCall = encodeFunctionData({
  abi: SWAP_ROUTER_ABI,
  functionName: "sweepTokenWithFee",
  args: [
    tokenOut,
    amountOutMinimum,
    userAddress,
    FEE_BIPS,
    FEE_RECIPIENT,
  ],
});

// Step 3: bundle into one transaction
await writeContract({
  address: SWAP_ROUTER_02,
  abi: SWAP_ROUTER_ABI,
  functionName: "multicall",
  args: [[swapCall, sweepCall]],
  value: tokenIn === ETH_SENTINEL ? amountIn : 0n,
});

Native token (ETH) outputs

If the user is swapping into native ETH, use unwrapWETH9WithFee instead. The swap produces WETH; the unwrap call:

  1. Unwraps WETH → ETH
  2. Splits the ETH between user and fee recipient
const unwrapCall = encodeFunctionData({
  abi: SWAP_ROUTER_ABI,
  functionName: "unwrapWETH9WithFee",
  args: [
    amountOutMinimum,
    userAddress,
    FEE_BIPS,
    FEE_RECIPIENT,
  ],
});

Verification

After a swap, you can verify the fee was taken correctly by checking the transaction's internal transactions on a block explorer. You should see:

  • WETH → SwapRouter02 (output of swap)
  • SwapRouter02 → feeRecipient (the fee, equal to output × feeBips / 10000)
  • SwapRouter02 → recipient (the rest, output × (10000 - feeBips) / 10000)

If the math doesn't add up to exactly feeBips/10000, something is wrong with your encoding.

Common mistakes

Forgetting `ADDRESS_THIS` in the swap call. If you set recipient: userAddress in exactInputSingle, the user gets all the tokens and the sweep call reverts. Must be ADDRESS_THIS.

Wrong sentinel. ADDRESS_THIS is 0x...02, not 0x...01 (which is MSG_SENDER). Easy to mix up.

Charging `feeBips > 100`. SwapRouter02 reverts. Maximum is 1.0%.

Ignoring decimals on `amountMinimum`. The amountMinimum argument to the sweep call should match what the swap is expected to produce, not the gross-of-fee number.

Address reference

SwapRouter02 deployment addresses (verified from official Uniswap docs):

ChainSwapRouter02
Ethereum0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
Optimism0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
Arbitrum0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
Polygon0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
Base0x2626664c2603336E57B271c5C0b26F421741e481
BNB Chain0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2
Avalanche0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE

The economics

At 0.10% (10 bips) — what BasedSwap charges — a $1000 swap generates $1 in protocol fee. Reasonable benchmarks for what's possible:

  • 100 swaps/day × $200 avg = $20/day
  • 1,000 swaps/day × $200 avg = $200/day
  • 10,000 swaps/day × $500 avg = $5,000/day

Uniswap V3's own protocol fee (when enabled) is structured the same way internally, just at a different rate. The mechanism is mature.

Why I wrote this

I spent two days on the Uniswap docs and Discord trying to figure out the right multicall pattern. The official docs cover exactInputSingle and the sweep functions separately, but never together as a fee-taking pattern. I'm hoping this saves the next builder some time.

If you're building a DEX frontend, you probably also want to track per-wallet activity (for future airdrops to your users). I cover that in my next post.


Try the pattern live on [BasedSwap](https://basedswap-azure.vercel.app) — works across 7 chains. Source code on [GitHub](https://github.com/kingboss568/basedswap).

Try BasedSwap

Multichain DEX across 7 networks · daily check-in points · routed through Uniswap V3

Open the App

More posts