Construct Fee Claim TX
Build, sign, and send creator fee claim transactions against the FeeHook contracts.
The launchpad's fee hook tracks ethOwed[address] for every wallet that should receive a slice of swap fees — the launcher, every recipient passed in LaunchOpts.creatorRecipients, the partner wallet (when registered), and the protocol fee wallet. Anyone with a non-zero balance calls claim() from their own wallet to pull it.
Network and targets
- Chain: Ethereum Mainnet (
chainId = 1) - FeeHookV3 (current):
0x0d62529346ac2c61f5c0582210D01214687bc0CC - FeeHookV2 (legacy, kept for V2 tokens):
0x8A22e2A5768C72751F2Da3E3b904365203b100cC
Every token launched after the V3 upgrade routes its swap fees to FeeHookV3. Tokens deployed before then still accrue on FeeHookV2, so creators of older tokens claim there. Check both hooks if you launched in different eras — the function surface is identical so a single ABI works for both.
Function surface
function ethOwed(address who) external view returns (uint256);
function claim() external returns (uint256 amount);ethOwed(addr)— pure read, returns the wei currently claimable byaddrfrom this hook.claim()— pulls the caller's fullethOwedbalance into their wallet, sets the ledger entry to 0, and returns the amount transferred. Reverts withnothing to claimifethOwed(msg.sender) == 0.
Recipients claim themselves
If a token was launched with a non-empty LaunchOpts.creatorRecipients:
- Each recipient address has its own
ethOwed[recipient]entry on the hook. - Each recipient signs their own
claim()from their own wallet — there's no "send claim to recipients" call. The hook keeps a per-address ledger; nothing is shared. - Recipient splits only apply to swap fees accrued after the launch. Swaps from before any recipient change keep accruing under the old split.
The same applies to V3's partner field: if the protocol admin has registered a partner with setPartnerBps(partner, bps), that wallet just calls FeeHookV3.claim() from their own address.
Constructed claim tx shape
to = FeeHook address (V3 or V2)
value = 0
data = selector(claim())
chainId = 1
EIP-1559 fees: maxPriorityFeePerGas, maxFeePerGasYou only pay gas — the call sends no ETH.
ABI fragment
[
{
"name": "ethOwed",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "who", "type": "address" }],
"outputs": [{ "name": "", "type": "uint256" }]
},
{
"name": "claim",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [],
"outputs": [{ "name": "amount", "type": "uint256" }]
}
]Worked example (viem) — claim from both hooks
import {
createPublicClient,
createWalletClient,
defineChain,
http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const mainnet = defineChain({
id: 1,
name: 'Ethereum',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['https://eth.llamarpc.com'] } },
});
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });
const FEEHOOK_V3 = '0x0d62529346ac2c61f5c0582210D01214687bc0CC' as const;
const FEEHOOK_V2 = '0x8A22e2A5768C72751F2Da3E3b904365203b100cC' as const;
const ABI = [
{
name: 'ethOwed',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'who', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
},
{
name: 'claim',
type: 'function',
stateMutability: 'nonpayable',
inputs: [],
outputs: [{ name: 'amount', type: 'uint256' }],
},
] as const;
// 1. Read both balances in parallel.
const [owedV3, owedV2] = await Promise.all([
publicClient.readContract({ address: FEEHOOK_V3, abi: ABI, functionName: 'ethOwed', args: [account.address] }),
publicClient.readContract({ address: FEEHOOK_V2, abi: ABI, functionName: 'ethOwed', args: [account.address] }),
]);
console.log('claimable on v3:', owedV3);
console.log('claimable on v2:', owedV2);
// 2. Send one claim tx per non-zero hook (sequential — each tx bumps the nonce).
const txs: `0x${string}`[] = [];
for (const [hook, owed] of [
[FEEHOOK_V3, owedV3],
[FEEHOOK_V2, owedV2],
] as const) {
if (owed === 0n) continue;
const hash = await walletClient.writeContract({
address: hook,
abi: ABI,
functionName: 'claim',
});
await publicClient.waitForTransactionReceipt({ hash });
txs.push(hash);
}
console.log('claim txs:', txs);If you only launched after the V3 upgrade you can skip the V2 read + send entirely.
Common claim issues
| Problem | Cause |
|---|---|
nothing to claim revert | ethOwed(msg.sender) == 0 on the hook you targeted |
| Tx succeeds but no ETH arrived | You're checking the wrong hook — read both (V3 and V2) |
| Wallet on wrong chain | Switch to Ethereum Mainnet (chainId = 1) |
| Insufficient gas balance | claim() sends value = 0, but the wallet still pays gas |
| Expected to claim for a recipient I added | Recipients claim from their own wallet — not the launcher's |