Create
Documentation
Guides

Fees and referrals

Where fees come from#

Every trade pays a fee on the quote leg, in the quote asset, from the first trade onward. There are two components:

ComponentGoes toSet by
curve / hook feesplit between the protocol and the creatorthe launch config and fee policy
creator taxentirely to the creator, never splitTokenParams.creatorTaxBps at launch

Both are frozen into the launch at creation. A later policy change by the owner affects only launches created after it, so an existing token's economics cannot be altered underneath its holders.

On Arc mainnet the curve fee is 1% of the quote leg, and after graduation the hook charges the same 1% (the V4 pool's own LP fee is zero). The protocol keeps 30% of that fee and the creator receives 70%. Creator tax is 0–10%, chosen at launch, all of it to the creator. Launching costs 5 USDC.

Referrals#

A referred trade is cheaper for the trader and pays the referrer:

  • the trader pays a discount on the standard fee — 0.9% instead of 1%,
  • the referrer receives a share of what is paid — 20% of that fee, taken off the top,
  • the pool books the remainder.

Bindings are permanent and never rewritten. A user can bind themselves ahead of time with referralRegistry.setReferrer(address), or a referrer can be passed to the four-argument curve.buy on their first trade.

On the curve, an invalid referrer reverts the buy. In the pool, the hook is far more forgiving — a rejected binding just charges the standard fee rather than failing the swap.

One security property worth knowing if you build a router: pool-phase hookData can only ever name the referrer, never the trader. The trader always comes from msgSender(). So nobody can bind a stranger's referrer with a dust swap.

Claiming#

Everything owed to anyone accumulates in the fee escrow, and every claim is pull-only by msg.sender — you cannot claim on someone else's behalf. That is deliberate: it means a recipient who cannot receive a transfer (a blocklisted address, a reverting contract) can never block a sweep for everybody else.

ts
// One balance per asset: a wallet that earned on a USDC launch and a WETH launch claims twice.
for (const currency of [USDC, WETH /* every quote your launches used */]) {
  const owed = await client.readContract({
    address: FEE_ESCROW, abi: escrowAbi,
    functionName: "balanceOfToken", args: [account, currency],
  });
  if (owed > 0n) {
    await walletClient.writeContract({
      address: FEE_ESCROW, abi: escrowAbi, functionName: "claimToken", args: [currency],
    });
  }
}

GET /v1/users/:addressclaimable.creator.byCurrency and claimable.referral.byCurrency list exactly which assets hold something for you, each with its decimals.

Warning
The escrow stores one balance per (recipient, token). It does not distinguish creator fees from referral fees — that split is attribution derived off-chain from events. So claimToken withdraws both at once, and a UI showing them as two separately claimable pots is lying about what the button does. Show the split as attribution; make one claim.

Fees are paid in the launch's quote asset — an ERC-20 for every approved pair token — so claimToken is the path, with amounts in that asset's own decimals. The native claim() is unreachable unless a launch quotes in the native asset, which no approved pair token does.

Pool-phase fees need a sweep first#

Curve fees accrue in the curve and are swept to the escrow by curve.sweepFees(). Pool fees accrue in the hook and are swept by hook.sweepPoolFees(poolId, minOut) — which may need to convert memecoin-denominated inventory back to the quote asset against the pool's own liquidity, which is why it takes a slippage bound.

Referral accruals in the pool are settled with hook.claimReferralFees(referrer, currency), which is permissionless — anyone can settle anyone's accrual into the escrow. The referrer then claims from the escrow as above. Two steps, not one.

Holder rewards#

A launch can send its creator side of the fees to its holders instead of to the creator. On the create page this is Fees go to: Holders. Nothing else about the economics changes: the protocol still keeps its 30% of the 1% curve/hook fee, referrals still work the same way, and the creator tax is still charged — it just lands with the holders too.

Fees go to: CreatorFees go to: Holders
protocol share (30% of the fee)protocolprotocol
creator share (70% of the fee)creatortoken holders
creator tax (0–10%, if set)creatortoken holders
referral sharereferrerreferrer

How it works#

The launchpad already lets any address be a launch's creatorFeeRecipient and claim from the escrow. Holder rewards point that field at a FociRewardsDistributor — a small contract per token, deployed from the rewards factory at an address that is predictable before the launch exists. So the launch transaction names the distributor as its fee recipient from block one, and the escrow credits it by address from the very first trade, whether or not the contract has been deployed yet.

From there the flow is:

  1. Sweep. Curve fees are swept from the curve into the escrow (the keeper does this for every live launch; pool fees are swept from the hook after graduation). This is the same step every launch goes through.
  2. Harvest. distributor.harvest() pulls the distributor's escrow balance into its unallocated pot. Permissionless; the keeper runs it once at least about $1 of the quote asset is waiting.
  3. Snapshot. Once a week, holder balances are snapshotted at the indexer's latest block and each holder's share is balance / eligible supply, rounded down. Protocol addresses that hold supply but can never claim are excluded from the supply and get no share: the launch's bonding curve, the Uniswap V4 PoolManager and the locker (the pool's liquidity is locked there forever — paying it would burn the money), the hook, the escrow, the factory and graduation executor, the launch-and-buy router, and the distributor itself.
  4. Publish. The shares become a Merkle tree and the distributor owner publishes its root with publishEpoch(root, total). This is the one step that is not automated: publishing a root is the only way to decide who gets paid, so it is signed from a hardware wallet by a person, not by the always-on keeper key. Epochs below about $1 of the quote asset are skipped and roll into the next week.
  5. Claim. Holders claim their share against the root for 90 days. Whatever is not claimed by then is rolled back into unallocated by rollOver(epochId) and funds a later epoch.

Everything is checkable: the tree is built from public Transfer events, the exclusion list is on chain (isExcluded), and leafFor(index, account, amount) returns the exact leaf encoding, so anyone can rebuild an epoch from chain data and compare roots.

Warning
Choosing Holders is a one-way door. creatorFeeRecipient can only be moved by the current recipient, and a distributor has no function that could ever call transferCreatorFeeRecipient — so once a launch pays its holders, it pays them for the life of the token. There is no way for the creator to reclaim the stream later.

Holder rewards need an ERC-20 quote asset (every approved pair token on Arc is one) and the rewards factory deployed on the network; the create page only offers the option when both are true.

Predicting the recipient#

The distributor's address is known before the launch exists, so the launch transaction can name it as creatorFeeRecipient directly — no second transaction, no window in which fees go to the wrong place. It is a CREATE2 address on the rewards factory, keyed by the creator, the launch's salt and the quote asset:

ts
// The salt you will pass as TokenParams.salt. Reuse it — the prediction is a function of it.
const salt = keccak256(toBytes(crypto.randomUUID()));

const distributor = await client.readContract({
  address: REWARDS_FACTORY, abi: rewardsFactoryAbi, functionName: "predict",
  args: [creator, salt, pairToken], // creator = the account that will sign launchToken
});

await walletClient.writeContract({
  address: FACTORY, abi: factoryAbi, functionName: "launchToken",
  args: [{ ...params, salt, creatorFeeRecipient: distributor }, launchConfigId, pairToken],
});

Or let the API do it — POST /v1/launch/predict with feesTo: "holders" returns the same address as distributor alongside the predicted token and curve, and the create page uses exactly this:

ts
const p = await fetch(`${API}/v1/launch/predict`, {
  method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${siwe}` },
  body: JSON.stringify({ ...params, salt, feesTo: "holders" }),
}).then((r) => r.json());

p.distributor; // → TokenParams.creatorFeeRecipient
p.token;       // the token address this launch will get

Three things have to line up, or deploy refuses and the fee stream sits unclaimed at the predicted address until they do: the creator must be the launch's deployer (the account that signs, or the account launchAndBuy launches for), the salt must be the launch's own TokenParams.salt, and the quote asset must be the launch's pairToken. Salts are namespaced per creator (keccak256(abi.encode(creator, salt))), so nobody can pre-empt your address with a launch of their own. Once the launch is indexed, the keeper calls rewardsFactory.deploy(token, salt), which checks all three against the launchpad's record and puts the contract in place. distributorOf(token) is zero until then.

The full surface — predict, deploy, claim, harvest, rollOver and the views — is in the contracts reference under FociRewardsDistributorFactory and FociRewardsDistributor, with ABIs on the ABI page.

Claiming as a holder#

The portfolio page shows a Holder fees tile with what is claimable now and an estimate of what is still accruing toward the next epoch, and claims everything open in one transaction. If you are building your own client, the API hands you the proofs:

ts
const { claimable } = await fetch(`${API}/v1/users/${account}`).then((r) => r.json());

for (const t of claimable.holders.byToken) {
  // One open epoch per weekly distribution — batchClaim settles them all at once.
  const open = t.epochs.map((e) => e.calldata);
  await walletClient.writeContract({
    address: t.distributor, abi: distributorAbi, functionName: "batchClaim",
    args: [
      open.map((c) => BigInt(c.epochId)),
      account,
      open.map((c) => BigInt(c.amountRaw)),
      open.map((c) => BigInt(c.index)),
      open.map((c) => c.proof),
    ],
  });
}

claim(epochId, account, amount, index, proof) does the same for a single epoch. Anyone may submit a claim, but the proof fixes the recipient — a claim always pays account, never msg.sender. An address on the exclusion list cannot claim even with a valid proof, and each (epoch, account) pair claims once (hasClaimed).

Claimed amounts arrive as the launch's quote asset (claimable.holders.byToken[].pairToken names it, decimals included), straight from the distributor — not through the escrow, so they are separate from any creator or referral balance you also hold.

What the numbers mean#

  • Claimable is the sum of your leaves in epochs whose 90-day window is still open.
  • Pending is an estimate: your share of what the token has accrued since the last epoch (curve fees not yet swept, pool fees still in the hook, harvested-but-unpublished unallocated), computed from your current balance. It changes with every trade and every transfer until the snapshot, so treat it as a preview, not a promise.