Documentation
Trading the pool
Once a launch reaches phase 2 the curve is closed and the token trades as an ordinary Uniswap V4 pool — with the foci hook attached, which takes the protocol and creator fee out of every swap.
You cannot call PoolManager directly#
V4 swaps only work inside an unlock callback, so you need a router. foci does not ship one;
use Uniswap's UniversalRouter.
Two consequences:
- Approvals go through Permit2, not a plain ERC-20 allowance. See Approvals — this is where most pool integrations fail first.
- Referral attribution depends on the router. The hook identifies the trader by calling
msgSender()on whoever called it. UniversalRouter implements that (viaBaseActionsRouter), so referrals work. A router that does not implementIMsgSenderleaves the trader unattributed and the swap is charged the undiscounted fee — it does not revert, it just quietly costs the user more.
Building the pool key#
const memecoinIsCurrency0 = token.toLowerCase() < usdc.toLowerCase();
const poolKey = {
currency0: memecoinIsCurrency0 ? token : usdc,
currency1: memecoinIsCurrency0 ? usdc : token,
fee: 0, // ALWAYS zero — the hook charges, not V4's LP fee
tickSpacing: 200,
hooks: MEME_HOOK,
};
const zeroForOne = spendToken.toLowerCase() === poolKey.currency0.toLowerCase();fee is zero by construction: the factory rejects any launch config with a non-zero pool
fee, because the hook takes the fee instead. Do not copy a 3000 from a V4 example.
Quoting#
The foci API refuses to quote a graduated launch by design. Use Uniswap's V4Quoter, which simulates the real swap through the real hook — so the hook fee is included by construction, rather than reimplemented off-chain and drifting.
const { result } = await client.simulateContract({
address: V4_QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle",
args: [{ poolKey, zeroForOne, exactAmount: amountIn, hookData: "0x" }],
});
const [amountOut] = result;quoteExactInputSingle is state-mutating (it reverts internally to unwind), so simulate it —
a plain readContract will not work.
Swapping#
const swap = encodeAbiParameters(
parseAbiParameters("((address,address,uint24,int24,address),bool,uint128,uint128,uint256,bytes)"),
[[[poolKey.currency0, poolKey.currency1, 0, 200, MEME_HOOK],
zeroForOne, amountIn, minOut, 0n, "0x"]],
);
const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [spendToken, amountIn]);
const take = encodeAbiParameters(parseAbiParameters("address, uint256"), [takeToken, minOut]);
// SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL
const actions = "0x060c0f";
const input = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [actions, [swap, settle, take]]);
await walletClient.writeContract({
address: UNIVERSAL_ROUTER, abi: universalRouterAbi, functionName: "execute",
args: ["0x10", [input], BigInt(Math.floor(Date.now() / 1000) + 600)], // 0x10 = V4_SWAP
});Slippage belongs in the TAKE_ALL minimum — that is the router's own bound and it reverts
rather than delivering less, so there is no need to re-check the output afterwards.
The fee comes off the unspecified leg#
The hook charges on whichever side of the swap you did not specify. On an exact-input sell
that is the output, so the amount you receive is net while the V4 Swap event reports the
gross figure.
Anything reading Swap alone will overstate proceeds by the fee. If you are indexing,
subtract the hook's take.