Documentation
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.
// 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.
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:
- 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.
- Harvest.
distributor.harvest()pulls the distributor's escrow balance into itsunallocatedpot. Permissionless; the keeper runs it once at least about $1 of the quote asset is waiting. - 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. - 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. - Claim. Holders claim their share against the root for 90 days. Whatever is not
claimed by then is rolled back into
unallocatedbyrollOver(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.
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:
// 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:
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 getThree 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:
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.