Pre-launch. The protocol is not publicly live yet. This documentation describes the contracts as implemented; deployed addresses are published at launch.

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.

Not on Gnosis

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

CheckHow to read it
Token graduatedPrimaryHook.getTokenInfo(token).deployer != 0
No secondary yethasSecondary(token) == false
Token side deliveredIERC20(token).balanceOf(SECONDARY_HOOK) >= 40,000,000 × 10⁹ (40M)
Native earmarkednativeEarmark(token) >= SECONDARY_NATIVE_RAW
$LP24 pool live$LP24 has graduated and its pool has a price
Sending tokens to the hook does nothing

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

RevertCause
Token not registered in PrimaryHookToken has not graduated
Secondary existsAlready created — check hasSecondary first
Token funds not receivedThe hook does not hold 40,000,000 × 10⁹ (40M) of this token
No native earmark for tokenNever funded by the PrimaryHook, or already consumed
No secondary for PROTOCOL_TOKEN$LP24 cannot pair with itself