# 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:

| Component | Goes to | Set by |
|---|---|---|
| curve / hook fee | split between the protocol and the creator | the launch config and fee policy |
| creator tax | entirely to the creator, never split | `TokenParams.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/:address` → `claimable.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: Creator | Fees go to: Holders |
|---|---|---|
| protocol share (30% of the fee) | protocol | protocol |
| creator share (70% of the fee) | creator | **token holders** |
| creator tax (0–10%, if set) | creator | **token holders** |
| referral share | referrer | referrer |

### 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](/documentation/contracts) under FociRewardsDistributorFactory and
FociRewardsDistributor, with ABIs on the [ABI page](/documentation/abi).

### 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.