whisk
Concepts

Fees and quotes

What a Quote contains, who pays the bridge fees, and how to charge your own platform fee.

A Quote is the engine's answer to "if I send X to Y, what happens?". It's pure data. Nothing is signed or submitted until the user confirms, so you can build a quote, inspect it, throw it away, and build another one freely.

What a Quote contains

type Quote = {
  route: Route; // same-chain "send" or cross-chain "bridge"
  recipient: ResolvedRecipient;
  amountIn: string; // total debited from the sender ("You pay")
  amountOut: string; // what the recipient receives ("Recipient gets")
  amountBurned?: string; // the on-chain transfer amount fed to App Kit
  token: Token;
  fees: FeeBreakdown; // every line item
  estimatedDurationMs?: number;
};

Three amounts, because on a bridge they can all differ:

  • amountIn is what leaves the sender's wallet. This is the "You pay" figure on the review screen.
  • amountOut is what the recipient receives. This is the "Recipient gets" figure.
  • amountBurned is the amount handed to CCTP on the source chain. Fees are deducted from it during the bridge. It equals amountOut in receiver mode and is grossed up in sender mode (see below). The widget uses this internally; you rarely read it directly.

Where fees come from

Two sources contribute, and they behave differently.

Circle App Kit fees are the cost of moving USDC across chains:

FeeWhen it appliesSized by
CCTP protocol feeFast Transfers only (Standard is free)A few basis points of the amount, by source chain
Forwarding Service feeWhen the destination mint is relayed for youDestination-chain gas
GasSource-chain approve/burnPaid in the source chain's native token

The CCTP protocol and Forwarding fees come out of the bridged USDC. Gas is paid separately in the native token, so it never reduces the USDC the recipient receives.

The Forwarding fee is the one to watch. It tracks destination-chain gas, not the transfer amount, so it's a near-fixed cost. Bridging to an L2 (Base, Arbitrum, Optimism, Arc) costs cents; bridging to Ethereum L1 can cost dollars regardless of how much you send. Pick cheap destinations for small transfers.

Your platform fee is optional and added on top. See Adding a platform fee.

Who pays the bridge fees: feeBearer

By default the recipient absorbs the App Kit fees, the same way a wire transfer arrives short of the amount sent. For a checkout, payroll run, or invoice, that's usually wrong: the merchant needs to net an exact figure. feeBearer controls this.

const config = createWhiskConfig({
  wallets: [evm({ projectId })],
  chains: ["Base", "Arbitrum"],
  feeBearer: "sender", // default is "receiver"
});

"receiver" (default) — the bridge fees are deducted from the transfer. The sender is debited the amount; the recipient nets amount − fees.

amount = 100 USDC, fees = 0.30 USDC
You pay:        100.00 USDC
Recipient gets:  99.70 USDC

"sender" — the burn is sized up by the estimated fees so the recipient receives the full amount. The sender's debit grows to cover them.

amount = 100 USDC, fees = 0.30 USDC
You pay:        100.30 USDC
Recipient gets: 100.00 USDC

The accuracy caveat

In sender mode the gross-up uses the fee estimated at quote time. The Forwarding fee is priced from destination gas at mint time, which lands a little later, so the live cost can drift from the estimate. To keep the recipient whole, the gross-up pads the forwarding portion by a small margin (2%) before sizing the burn. The recipient therefore receives at least the requested amount; if gas comes in under the estimate, the unused margin is minted to them. Penny-exact delivery isn't possible with a variable-fee bridge unless you control the mint with a custom contract, so Whisk rounds in the recipient's favor.

Same-chain sends never deduct USDC fees (only native gas), so feeBearer is a no-op there: the recipient always receives the full amount.

Adding a platform fee

feePolicy charges your own fee on top of the transfer. Per Circle App Kit, the fee is split 90% to your recipient and 10% to Arc, and it's collected on the source chain.

const config = createWhiskConfig({
  wallets: [evm({ projectId })],
  chains: ["Base"],
  feePolicy: {
    value: "0.05", // absolute USDC amount, added on top
    recipient: "0xYourTreasury…", // your fee wallet (source chain)
  },
});

feePolicy and feeBearer are independent. Your platform fee is always added on top of the amount; feeBearer only decides who absorbs the network fees. With both set to "sender" plus a feePolicy, the sender is debited amount + network fees + your fee, and the recipient still nets the full amount.

Building a quote without rendering

Want a fee preview on a checkout summary before the widget mounts? Call the engine directly. It doesn't render, so this works in a click handler or anywhere you hold an adapter:

const quote = await engine.quote({
  recipient,
  amount: "100",
  sourceChain: "Base",
  destinationChain: "Arbitrum",
  adapter,
});

console.log(quote.amountIn); // "100.3" in sender mode
console.log(quote.amountOut); // "100"
console.log(quote.fees.entries);
// → [
//     { kind: "provider",  amount: "0.013", token: "USDC" },
//     { kind: "forwarder", amount: "0.287", token: "USDC" },
//   ]

Quotes go stale as gas prices shift, so re-quote before the user signs if they've lingered. The bundled <WhiskSend> handles that for you; it matters when you drive the engine yourself.

Next

Browse the components.

On this page