Create a Secondary Pool
After a token graduates on a V4-family chain, its token↔$LP24 pool still has to be created. Anyone can do it, it costs only gas, and it is safe to retry.
Gnosis runs a single V3 pool per token with no secondary. This guide applies to Base, BSC, Polygon and Robinhood.
Why it is a separate call
Graduation delivers and earmarks the funds but does not build the pool. Creating it requires swapping native for $LP24 at whatever price the $LP24 pool happens to be at in that instant, and graduation must not be able to fail because that price was momentarily bad. So the two are split: the graduation transaction custodies the funds, and pool creation happens later, retryably.
Finding tokens that need one
const SECONDARY_ABI = [
"function createSecondary(address token)",
"function hasSecondary(address token) view returns (bool)",
"function secondaryPoolIdOf(address token) view returns (bytes32)",
"function nativeEarmark(address token) view returns (uint256)"
];
const secondary = new ethers.Contract(SECONDARY_HOOK, SECONDARY_ABI, signer);
// PrimaryHook logs SecondaryReserved at graduation — that is the trigger
const reserved = await hook.queryFilter(
hook.filters.SecondaryReserved(), fromBlock, "latest"
);
const pending = [];
for (const ev of reserved) {
const token = ev.args.token;
if (!(await secondary.hasSecondary(token))) pending.push(token);
}SECONDARY_ABI = json.load(open("SecondaryHook.abi.json"))
secondary = w3.eth.contract(address=SECONDARY_HOOK, abi=SECONDARY_ABI)
# PrimaryHook logs SecondaryReserved at graduation — that is the trigger
reserved = hook.events.SecondaryReserved().get_logs(from_block=from_block)
pending = []
for ev in reserved:
token = ev["args"]["token"]
if not secondary.functions.hasSecondary(token).call():
pending.append(token)Creating it
// Not payable — the contract already holds the funds
const receipt = await (await secondary.createSecondary(token)).wait();
const created = receipt.logs
.map(l => { try { return secondary.interface.parseLog(l); } catch { return null; } })
.find(e => e?.name === "SecondaryCreated");
console.log(created.args.poolId, created.args.tokenIsCurrency0);# Not payable — the contract already holds the funds
receipt = send(secondary.functions.createSecondary(token))
created = secondary.events.SecondaryCreated().process_receipt(receipt)[0]
print(created["args"]["poolId"], created["args"]["tokenIsCurrency0"])The call swaps the earmarked native for $LP24 on the primary $LP24 pool, initializes the token↔$LP24 pool at the resulting ratio, and mints a full-range position owned by the hook. You bring nothing and receive nothing — this is a keeper action, not a trade.
Preconditions
| Check | How to read it |
|---|---|
| Token graduated | PrimaryHook.getTokenInfo(token).deployer != 0 |
| No secondary yet | hasSecondary(token) == false |
| Token side delivered | IERC20(token).balanceOf(SECONDARY_HOOK) >= 40,000,000 × 10⁹ (40M) |
| Native earmarked | nativeEarmark(token) >= SECONDARY_NATIVE_RAW |
| $LP24 pool live | $LP24 has graduated and its pool has a price |
The native side is tracked as a per-token earmark that only the PrimaryHook can credit, during graduation. Transferring a graduated token's 40M to the SecondaryHook yourself will not let you create a pool — there is no earmark for a token the PrimaryHook never funded, and the call reverts with No native earmark for token.
Retrying after a failure
The most common failure is the slippage floor on the $LP24 acquisition swap, which rejects a realized output more than 200 bps (2%) below the reserve-derived expectation. That is harmless: the earmark is consumed up front but rolls back with everything else, so the custodied funds survive intact and the call can be retried once the $LP24 pool settles.
try {
await (await secondary.createSecondary(token)).wait();
} catch (e) {
// Funds and earmark are untouched — retry later
console.warn("deferred", token, e.shortMessage);
}from web3.exceptions import ContractLogicError
try:
send(secondary.functions.createSecondary(token))
except ContractLogicError as e:
# Funds and earmark are untouched — retry later
print("deferred", token, e)After creation
The pool charges a flat 60 bps (0.6%) in the launched token, in both directions, burned immediately to the dead address. There is no fee distribution to trigger, no deployer share, and no reinvestment — the only ongoing effect is supply reduction on every trade. Liquidity is permanently locked in the hook.
Common reverts
| Revert | Cause |
|---|---|
Token not registered in PrimaryHook | Token has not graduated |
Secondary exists | Already created — check hasSecondary first |
Token funds not received | The hook does not hold 40,000,000 × 10⁹ (40M) of this token |
No native earmark for token | Never funded by the PrimaryHook, or already consumed |
No secondary for PROTOCOL_TOKEN | $LP24 cannot pair with itself |