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:
amountInis what leaves the sender's wallet. This is the "You pay" figure on the review screen.amountOutis what the recipient receives. This is the "Recipient gets" figure.amountBurnedis the amount handed to CCTP on the source chain. Fees are deducted from it during the bridge. It equalsamountOutin 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:
| Fee | When it applies | Sized by |
|---|---|---|
| CCTP protocol fee | Fast Transfers only (Standard is free) | A few basis points of the amount, by source chain |
| Forwarding Service fee | When the destination mint is relayed for you | Destination-chain gas |
| Gas | Source-chain approve/burn | Paid 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 USDCThe 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.
