pons v2, from first principles.

Overview

pons v2 is a launch protocol. A creator deploys a token, the public buys it from a bonding curve, and once the curve is bought out the launch graduates into a Uniswap v4 pool whose liquidity is locked permanently. Every step is a transaction your own wallet signs. pons never takes custody of tokens or funds.

The important change from v1 is that a launch no longer starts life as a pool. It starts on a curve that holds the entire supply, and the pool is created only at graduation, seeded from what the curve collected. That removes the launch-day liquidity problem: there is nothing to snipe before the curve opens, and no separate migration step for a creator to get wrong.

What that means in practice is that a launch trades in two places over its life. First against the curve, then in a normal Uniswap pool. Nothing is required of you when it switches over, and nothing about your holding changes when it does. The tokens in your wallet are the same tokens before and after.

A launch does not have to be priced in ETH. It can be paired against another token, and when it is, that token becomes the currency of the whole launch. You buy and sell in it, and the creator is paid in it. See custom pairs and payouts.

Launch lifecycle

Every launch follows the same four steps. There is no path where a creator decides to do something different halfway through.

  1. 01

    Create

    The creator sets a name, symbol, image, description, and links, and pays a launch fee. The entire supply is minted straight to the curve. No one, including the creator, is holding a bag of tokens set aside before trading opens.

  2. 02

    Trade the curve

    Anyone can buy and sell. The price rises as people buy and falls as they sell, and you can always sell back to the curve. There is no waiting for the creator to add liquidity.

  3. 03

    Graduate

    Once the curve sells out, it closes. Everything it collected is handed over to build the pool, along with the tokens that were held back for exactly this purpose.

  4. 04

    Pool

    A Uniswap pool is created and its liquidity is locked permanently. Trading carries on there, and the token behaves like any other token on Uniswap.

Graduation happens automatically, inside whichever purchase happens to finish the curve. If that automatic step does not complete for any reason, anyone at all can push the launch forward. It does not need the creator, and it does not need pons. A launch cannot be left stranded because the person who created it lost interest.

Bonding curve

A bonding curve is a vending machine for a token. It holds the whole supply from the moment the launch is created, and it will always sell you tokens and always buy them back. The price is not set by anyone. It is worked out from how much of the supply has been bought so far.

The more people buy, the higher the price goes. The more people sell, the lower it goes. Because the curve is always willing to trade, you are never waiting for someone else to take the other side.

The one moment you cannot sell to the curve is after it has sold out. At that point it is holding exactly the reserves the pool is about to be built from, and letting anyone sell into them would change the pool every launch is supposed to arrive at. Selling reopens in the pool once the launch has graduated, which normally happens in the same transaction that finished the curve.

The price does not start at zero. Each launch opens at a set starting price and climbs from there, which is why the first buyer does not get the supply for nothing. Large buys move the price more than small ones, so the price you end up paying on a big order is worse than the price you saw quoted. This is the same behaviour as any exchange with limited liquidity.

Fees are charged in the asset the launch is priced in, never in the launch token itself. Buying costs a fee on what you spend, and selling costs a fee on what you receive. You are never handed a fee bill in a token you were trying to get rid of.

Graduation

Graduation is the moment a launch stops trading on its curve and becomes a normal Uniswap pool. It happens when the curve has sold everything it was ever going to sell.

Not all of the supply is for sale on the curve. A fixed share is held back from the start, and that held-back share is what becomes the liquidity in the pool. It is decided when the launch is created and nobody can change it later, so there is no moment where a creator decides how much liquidity to provide, and no opportunity for them to provide less than expected.

Because the reserved share is fixed, every launch on the same settings graduates into a pool of the same size, at the same price. It does not matter whether the curve was bought out by one large purchase or by hundreds of small ones. You can work out in advance what the pool will look like, and it will look that way every time.

If your purchase is larger than what is left on the curve, you are not rejected. You buy what remains, you are charged only for what you actually received, and the rest is returned to you in the same transaction. This matters most for the buy that finishes a launch, because otherwise anyone could block it by slipping a small purchase in first. Your protection against a bad price still applies: you can receive fewer tokens than you asked for, but never at a worse price per token than you agreed to.

Custom pairs

Most launches are priced in ETH. They do not have to be. A launch can be paired against any token pons has approved for the purpose, and when it is, that token becomes the currency of the whole launch.

Say a launch is paired against a tokenised stock. Buyers spend that stock token to buy in and receive it back when they sell, the target the launch has to hit before it graduates is counted in it, the Uniswap pool it graduates into is paired against it, and the creator is paid in it. The launch is priced against that stock token rather than against ETH, and nothing anywhere in the process quietly converts between the two.

What it changes for you

If you are buying, you spend the pairing asset instead of ETH, and you get it back when you sell. Your wallet will ask you to approve the token first, which is the standard extra step for spending any ERC-20, and you should hold the pairing asset before you try to buy.

The number you see quoted is a price in that asset, not in ETH or dollars. A token priced at 0.5 of a pairing asset can rise against that asset while falling in dollar terms, if the pairing asset itself is dropping. Watch both.

If you are launching, you choose the pairing asset when you create the token, and it is fixed from then on. You cannot switch a launch to a different asset later, and your fees will arrive in whatever you chose.

Not every token can be used

Only assets pons has explicitly approved can be used for pairing. There is no way for anyone else to add one, so a creator cannot pair a launch against a worthless token of their own making in order to manufacture a price.

Approval is a judgement that an asset is suitable for pairing. It is not an endorsement of that asset and not a guarantee about it. pons can stop allowing new launches against an asset at any time, which has no effect on launches already trading against it.

The risk you are taking on

A launch priced in something other than ETH carries that something's risk on top of its own. If the pairing asset falls, your position falls with it even if the launch itself holds steady. If the pairing asset becomes hard to trade, so does the launch, because the pool is paired directly against it and there is no route around that.

It also affects how much a launch needs to raise before it graduates, since that target is measured in the pairing asset. If the asset doubles in value, the real cost of graduating that launch doubles too.

Snipe protection

A launch is most vulnerable in its first moments. A bot watching for new curves can buy the opening of one before anybody else has seen it exists, then sell into the people who arrive a minute later. Every launch therefore opens with a tax on buying, set high enough that being first is not worth it, and decaying to nothing within seconds.

The tax starts at 99% of a buy and decays exponentially to zero across the first 5 seconds, so it falls away fastest at the start. It is near 25% one second in and around 3% at two seconds, which makes the opening moments unprofitable to race while leaving the launch open to an ordinary buyer almost immediately. It applies only to buys. Selling is never taxed by it.

What is collected is not burned. It joins the launch's trading fee and is distributed the same way, so the value a sniper gives up flows back to the launch rather than out of it.

Each launch takes its own copy of these terms when it is created, so a curve already trading keeps what it launched under and cannot have the window changed underneath it afterwards.

The creator is never a sniper on their own launch. The launching address and the creator fee recipient are exempted automatically, and a creator can name further addresses at creation for a team bundling its opening buys across several wallets. Exemptions are fixed at creation and cannot be added later.

If you are quoting a trade

Curve maths alone will overstate what a buy returns while the tax is still decaying. Read currentSnipeTaxBps(recipient) from the curve and apply it, or the quote you show will not match what the trade settles at. Note that it is keyed on the recipient of the tokens rather than the sender, so a router must ask about the wallet being bought for. It returns zero once the window has passed, which is the ordinary case for almost every trade.

Fees

You pay two things when you trade, and they are worth telling apart. There is a standard trading fee, which every launch has and which is shared out between pons, the creator, and the buyback. Then there is an optional creator tax, which the creator sets when they create the token and which goes entirely to them.

The creator tax is the one to check before you trade, because it varies from token to token. It is capped, so a creator cannot set it to anything they like, and it is fixed at creation, so it cannot be raised on you later. A launch with no creator tax is charging you the standard fee and nothing else.

Of the standard fee, pons takes its share first. If the creator has turned on buybacks, a slice of what remains is spent buying the token back. Whatever is left, plus the whole creator tax, goes to the creator. That division is set when the launch is created and stays the same for the life of the token, before and after it graduates.

You pay the same rate whether the token is on its curve or in its Uniswap pool. The pool itself is set up to charge no fee of its own, so trading a graduated pons token does not cost you a Uniswap fee on top of the pons one.

Payouts

A creator is paid in whatever their launch is priced in. A launch priced in ETH pays its creator ETH. A launch paired against a stablecoin pays that stablecoin. A launch paired against a tokenised stock pays that stock token. Nothing is converted to ETH along the way, and a creator is never paid in a currency their launch did not trade in.

Before graduation this is simple, because the curve charges its fee in the pairing asset on every trade in both directions.

After graduation it takes more work behind the scenes. Depending on which way a swap goes, the fee collected can land in the launch token rather than the pairing asset, so a pool builds up a mix of the two. Before anything is paid out, the launch-token side is sold back into the pool and converted, so what reaches the creator is the pairing asset either way. Those conversions are capped so they cannot move the price much, and if one cannot be done at a reasonable price it is simply left for later rather than forced through.

Getting paid

Fees are not pushed to your wallet as they are earned. They build up as a balance you withdraw whenever you want. This is deliberate: if payouts were pushed automatically, a single recipient whose wallet could not accept a transfer would be able to jam fee distribution for everyone else.

If you have launched several tokens against different pairing assets, you have a separate balance in each one, and you withdraw each separately. There is no single combined balance.

The one payout that does not arrive in the pairing asset is the buyback. Buybacks purchase the launch token itself, so what you eventually receive from one is that token. See buyback and vesting.

Buyback and vesting

A creator can choose to have part of their own fee spent buying their token back off the market. It comes out of the creator's share, not out of yours, and it is optional.

Bought-back tokens are not burned. They are locked away and released gradually over five years, split between the creator and the protocol. Nobody receives a lump sum, and there is no point at which a large pile of tokens can suddenly return to the market.

The five-year clock is weighted, so later buybacks do not ride on the progress of earlier ones. A large buyback made this month cannot become withdrawable immediately just because the launch has been buying back for years.

Releasing what has vested is done by either side of the split, the creator or pons, and whichever one calls it pays out both. So a creator who has stopped paying attention does not strand the protocol's share, and a creator can always take their own without waiting on anyone.

If a buyback cannot be done sensibly, because there is too little liquidity or it would move the price too far, it is skipped and that money goes to the creator as normal. A buyback going wrong cannot hold up anyone else's fees.

Creator controls

Once a launch is live, its creator controls almost nothing about it. That is the point. The supply is fixed, the pricing cannot be rewritten, the pairing asset cannot be swapped, the tax cannot be raised, and the terms on which it graduates were set the moment it was created. Exactly two things stay adjustable.

Where fees go
The creator can point their fees at a different wallet. This moves future earnings only, and it moves their buyback share with it.
Whether to buy back
The creator can turn buybacks on or off. Only they can turn it on, since it is funded from their own share. pons can turn it off but never on.

Neither of those touches your tokens. There is no mint, no way to freeze or blacklist a wallet, no way to add a tax after launch, and no way for anyone to reach the liquidity once it is locked.

Community takeovers

When a creator walks away from a token, the community around it can take over the creator's side of it. A community takeover, or CTO, redirects the creator's fees, and their share of any buyback, to whoever is actually running the project now.

A takeover changes who gets paid. It does not change the token. The supply stays fixed, the pricing stays the same, the pairing asset stays the same, the locked liquidity stays locked, and the tax cannot be raised above what the launch started with. Whoever takes over cannot mint tokens, cannot touch the liquidity, and cannot change how trading works. If you hold the token, a takeover does not alter what you are holding.

Two routes

The simple one is voluntary. A creator who is stepping away hands their fees to a community wallet themselves, and it takes effect straight away.

The second exists for tokens where the creator has vanished or lost access to their wallet. pons can propose a new recipient, and that proposal is deliberately slow and public. Nothing happens for three days. After that anyone can carry it out, and if no one does within a further three days it expires. pons can call it off during the wait.

The delay is the point of the design. The proposal is public the moment it is made, and it states both the new wallet and the date it takes effect, so holders find out before it happens rather than afterwards. If you disagree with a proposed takeover, the waiting period is your window to sell, organise, or make the case against it.

One detail worth knowing: a creator moving their fees elsewhere during the wait does not call off the proposal. That is intentional. If it did, someone who had stolen a creator's wallet could sidestep the recovery that was proposed precisely because the wallet was stolen.

Requesting one

Takeovers are requested through the pons CTO form and reviewed by the team. Use it where a token has clearly been abandoned and there is an active community asking to take it on. Being approved is an administrative decision. It is not an endorsement, and it says nothing about whether a token is safe or worth buying.

Never share a private key or seed phrase. pons will never ask you to send funds to process an application.

Migration

Migration is for coins pons did not launch: a token whose market has died, whose creator is gone, but whose community is still there. It moves the community onto a replacement token in a fresh pons pool that opens at the price the old market held, funded by selling the old coins the community deposits. The takeover contract that runs it has no owner and no admin over funds. Every step below is either yours to take or open for anyone to trigger.

Depositing

A migration opens with a deposit window split into epochs. Deposit the old coin and you are credited a claim on the replacement at that epoch's rate: the first epoch credits one for one, and each later epoch credits less, so the people who commit earliest are treated best. Your credit is fixed the moment you deposit and nothing that happens later changes it.

Most migrations also give each epoch a deposit cap. When a cap fills, that epoch closes on the spot and the next one opens at the next rate, so a rush of demand walks the ladder down early rather than waiting out the calendar. The last cap filling closes the whole window.

Nothing converts until deposits reach the migration's mandate, a minimum the takeover must gather to proceed, and nothing sells until the first epoch closes. Until the first sale actually executes you can withdraw in full, with one exception: deposits sitting in an epoch whose cap filled are committed, because that fill closed the epoch and repriced everyone behind it, and that cannot be undone. If the mandate is never met, everything is refundable and the migration simply expires.

The sale and the new pool

After an epoch closes, its pooled deposits sell into the old market in paced tranches: a cap per trade, a cooldown between trades, and a mandatory minimum output taken from the old pool's time-averaged price. Anyone can call the sale, and no caller can price it, so a bot selling on your behalf cannot sell you out cheaply even if the bot is hostile. pons runs a keeper so nobody has to do this by hand.

The recovered funds open the replacement pool at the price the old market traded at before the sale began, captured at the first deposit and guarded against manipulation: a reading from an empty or nearly empty market is refused, and one far from the price the migration deployed against waits instead of anchoring. Later epochs settle into the same pool as extra depth. The supply minted into the pool is bounded by what the old market says the recovery is worth, and any recovery beyond that bound is returned to depositors rather than minted against.

Claiming

Once your epoch settles you can claim: the replacement token mints to you against your credit, vesting linearly over a few days so the fresh pool never absorbs the whole supply in one block. If the sale recovered more than the pool could take at parity, that surplus is yours to collect as well, split pro rata by credit once every epoch has settled. Claims stay open for a fixed window, ninety days on current deployments, after which the replacement token's minting closes permanently.

If it fails

A migration that cannot finish, because the market could not pay enough or a batch could not sell in time, ends in rescue rather than limbo. Past the settlement deadline, every depositor in an unsettled epoch reclaims their share of whatever was recovered plus their share of the unsold old coins. Epochs that did settle keep their claims. If the old coin itself refuses to transfer, its leg is banked and can be collected whenever the coin moves again, so a frozen coin can never hold your recovered funds hostage.

The value a migration recovers depends entirely on what the old market can pay. A deposit is a commitment to sell the old coin at market; it is not a guarantee of the replacement's price.

Safety and recovery

When a launch graduates, its liquidity is locked and never comes back out. There is no unlock button, no waiting period after which someone can withdraw it, and no privileged wallet that can reach it. Not the creator, and not pons. Any supply left over at graduation is locked in the same place.

This is the part worth understanding, because rug pulls almost always work by the creator removing the liquidity. On a graduated pons token that is not possible. It is not a promise not to, it is that the function does not exist.

Graduation happens in two steps rather than one, and there is a safety valve in case the second step cannot complete, usually because something about the pairing asset has changed since it was approved. If a launch is stuck in between for seven full days, pons can return what was collected instead of leaving it stranded. The week-long wait means this can never be used to interfere with a graduation that was going to work. A launch that has been through this is permanently marked as such, so you can tell.

Some things cannot be recovered by anyone, deliberately. Locked liquidity, supply locked at graduation, and tokens you send directly to a contract address by mistake are all gone for good.

Risk disclosures

Launch tokens are volatile and can lose all value. Anyone can create a launch with any name, symbol, and image, including ones that deliberately imitate an existing project. Names and symbols are not unique and are not verified by pons.

  • Always check the token address. It is the only identifier that cannot be copied.
  • A creator can set a creator tax at launch, within the protocol cap. Read it before trading.
  • Reaching graduation is not a signal of quality. It only means the curve sold out.
  • A launch paired against another token carries that token's risk on top of its own.
  • Transactions are submitted by your wallet and may be irreversible.

Integration

Everything reads and writes onchain.

There is no pons API in the trust path. Index the factory and the curves for a source of truth that does not depend on our infrastructure.

Contracts

The system is a small set of contracts with one job each. The factory is the entry point for launching and graduating. Everything else is either per launch or a shared singleton.

Launch factory
Deploys every launch, holds launch configuration, and drives graduation.
Bonding curve
One per launch. Prices and settles every pre-graduation trade.
Launch token
One per launch. A fixed-supply ERC-20 minted entirely to its curve.
Meme hook
Singleton Uniswap v4 hook. Accrues and distributes post-graduation fees.
Fee escrow
Holds claimable protocol and creator balances in ETH and ERC-20.
Buyback vault
Holds bought-back supply and releases it linearly over five years.
Launch locker
Permanently holds each graduated pool position and any excess supply.
Launch and buy
Optional router that creates a launch and buys into it in one transaction.
Launch deployer
Deploys each curve and token at creator-selected CREATE2 addresses.
Graduation executor
Mints the full-range Uniswap v4 position directly into the locker.
Graduation guard
Checks that graduation reserves can seed a valid Uniswap v4 pool.

Deployed addresses

Live on Robinhood Chain, chain id 4663. Resolve each launch's own curve and token from the factory rather than hardcoding them, since those are created per launch.

Factory
Meme hook
Fee escrow
Buyback vault
Launch locker
Launch and buy
Launch deployer
Graduation executor
Graduation guard
Resolve a token from the factory that launched it

A hook binds to one factory permanently, so a launchpad is replaced as a whole set rather than upgraded in place. These are the addresses for the current one. A token created before it still trades and still pays out, through the hook and escrow it launched against, so treat the stack a token belongs to as something you look up rather than assume. Reading creator balances from the wrong escrow reports zero rather than failing, which is easy to miss.

Launching a token

Launching is one call to the factory. You choose a launch config, which fixes supply, curve fee, phantom reserve, graduation threshold, and the pool parameters, and you choose the quote asset. The launch fee is sent as value on the call.

Enumerating launch configs

Configs are held in an append-only list, so the id you launch with is stable forever. Existing configs can be edited or disabled by the owner, and a disabled one reverts with LaunchConfigDisabled, so read the list at create time rather than caching ids indefinitely.

Reading available configs
const factoryAbi = parseAbi([
  "struct LaunchConfig { uint256 supply; uint256 curveFeeBps; uint256 phantomQuote; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; bool enabled; }",
  "function launchConfigCount() view returns (uint256)",
  "function getLaunchConfig(uint256 id) view returns (LaunchConfig)",
]);

/**
 * Returns the launch configs currently open for new launches. Disabled
 * configs stay readable so historic launches remain explainable, but a
 * create flow should not offer them.
 */
async function openLaunchConfigs() {
  const count = await client.readContract({
    address: factory,
    abi: factoryAbi,
    functionName: "launchConfigCount",
  });

  const configs = await Promise.all(
    Array.from({ length: Number(count) }, (_, id) =>
      client.readContract({
        address: factory,
        abi: factoryAbi,
        functionName: "getLaunchConfig",
        args: [BigInt(id)],
      }),
    ),
  );

  return configs
    .map((config, id) => ({ id: BigInt(id), ...config }))
    .filter((config) => config.enabled);
}

Pass an economics pin so a launch cannot settle on terms you did not read. Fetch it with previewLaunchEconomics immediately before launching and pass the result as expectedEconomics. If the owner changes the config in between, the launch reverts rather than going through on different terms.

Launching with a pinned quote
import { parseAbi, toHex } from "viem";

const factoryAbi = parseAbi([
  "struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
  "struct TokenParams { string name; string symbol; string logo; string description; Socials socials; address creatorFeeRecipient; uint16 creatorTaxBps; bool buybackEnabled; bytes32 expectedEconomics; bytes32 salt; }",
  "function launchToken(TokenParams params, uint256 launchConfigId, address pairToken) payable returns (address token, address curve)",
  "function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)",
  "function launchFee() view returns (uint256)",
]);

const launchConfigId = 0n;
const pairToken = "0x0000000000000000000000000000000000000000"; // native ETH

// The curve and token are deployed with CREATE2, so their addresses follow
// from this salt and the launch's own terms rather than from the order
// launches land in. Any value you have not used before will do. Salts are
// namespaced per authenticated initiating account, so no one else's choice
// collides with yours, and mining one is how you claim a vanity address.
const salt = toHex(crypto.getRandomValues(new Uint8Array(32)));

// Pin the terms you were quoted. If the owner changes launch economics
// between your read and your transaction, the launch reverts instead of
// settling on terms you never agreed to.
const [expectedEconomics, launchFee] = await Promise.all([
  client.readContract({
    address: factory,
    abi: factoryAbi,
    functionName: "previewLaunchEconomics",
    args: [launchConfigId, pairToken],
  }),
  client.readContract({
    address: factory,
    abi: factoryAbi,
    functionName: "launchFee",
  }),
]);

const hash = await wallet.writeContract({
  address: factory,
  abi: factoryAbi,
  functionName: "launchToken",
  args: [
    {
      name: "Example",
      symbol: "EXMPL",
      logo: "ipfs://...",
      description: "An example launch.",
      socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
      creatorFeeRecipient: creator, // zero address defaults to the caller
      creatorTaxBps: 0,             // optional creator tax, capped by the protocol
      buybackEnabled: true,
      expectedEconomics,
      salt,
    },
    launchConfigId,
    pairToken,
  ],
  value: launchFee,
});

Deterministic launch addresses

Every launch includes a 32-byte salt. The launch deployer combines it with the initiating wallet and the launch's constructor arguments to derive the curve and token through CREATE2. That makes both addresses predictable before the transaction is sent, while preventing another wallet from occupying them by submitting the same salt first.

Use a fresh random salt for an ordinary launch. A creator choosing a vanity address can mine one offline, then confirm the result with predictLaunchAddresses on the launch deployer. Reusing a salt with otherwise identical terms reverts because that curve and token pair already exists.

A zero creatorFeeRecipient defaults to the caller. creatorTaxBps is the optional creator tax and is rejected above the protocol cap, which you can read from maxCreatorTaxBps(). Launching may be restricted to whitelisted addresses. canLaunch(address) answers that in one call, returning true while the public gate is open and, while it is closed, only for whitelisted addresses. Prefer it over reading launchEnabled() and whitelistedLaunchers(address) separately.

Exempting addresses from the opening tax

An overload of launchToken takes a final address[] of addresses exempt from the opening snipe tax, for a team bundling its opening buys across several wallets. At most 32 entries, past which it reverts with ExemptionListTooLong, and the list is fixed at creation. The three-argument form launches without extra exemptions. Either way the launching address and the creator fee recipient are exempted for you, so a solo creator needs nothing here.

Launching with exemptions
// Same launch, plus extra wallets exempt from the opening snipe tax.
// The launcher and the creator fee recipient are always exempted anyway.
const factoryAbi = parseAbi([
  "struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
  "struct TokenParams { string name; string symbol; string logo; string description; Socials socials; address creatorFeeRecipient; uint16 creatorTaxBps; bool buybackEnabled; bytes32 expectedEconomics; bytes32 salt; }",
  "function launchToken(TokenParams params, uint256 launchConfigId, address pairToken, address[] snipeTaxExemptions) payable returns (address token, address curve)",
]);

const hash = await wallet.writeContract({
  address: factory,
  abi: factoryAbi,
  functionName: "launchToken",
  args: [params, launchConfigId, pairToken, [teamWalletA, teamWalletB]],
  value: launchFee,
});

Launching and buying in one transaction

Creating a launch and then buying into it as a second transaction leaves a gap that anyone watching can trade in first. The launch-and-buy router closes it by doing both inside one call, so a creator's opening buy cannot be front-run by the launch that made it possible.

It is a separate contract rather than part of the factory, and it is optional. It holds its callers to the factory's own launch gate, so it grants nobody access they would not already have. Two differences from a direct launch are worth noting: creatorFeeRecipient must be explicit because the token recipient, initiating wallet and fee recipient may all differ. The factory's trusted-forwarder path still records and namespaces the launch to the initiating wallet rather than to the router. The buy's recipient is exempted from the opening tax automatically so the first buy clears untaxed.

Launching and buying atomically
// One transaction: create the launch, then buy into it.
// Nothing can trade between the two, so the opening buy cannot be front-run.
const routerAbi = parseAbi([
  "struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
  "struct TokenParams { string name; string symbol; string logo; string description; Socials socials; address creatorFeeRecipient; uint16 creatorTaxBps; bool buybackEnabled; bytes32 expectedEconomics; bytes32 salt; }",
  "function launchAndBuy(TokenParams params, uint256 launchConfigId, address pairToken, uint256 quoteIn, uint256 minTokensOut, address recipient, address[] snipeTaxExemptions) payable returns (address token, address curve, uint256 tokensOut)",
]);

const quoteIn = parseEther("0.5");

// Native launch: the fee and the buy travel together as value.
// ERC-20 pair: send only the fee, and approve the router for quoteIn first.
const hash = await wallet.writeContract({
  address: launchAndBuy,
  abi: routerAbi,
  functionName: "launchAndBuy",
  args: [
    params,       // creatorFeeRecipient must be set, zero is rejected here
    launchConfigId,
    pairToken,
    quoteIn,
    minTokensOut, // still enforced when the curve clamps the fill
    recipient,
    [],           // extra wallets to exempt; recipient is added for you
  ],
  value: pairToken === zeroAddress ? launchFee + quoteIn : launchFee,
});

Choosing a quote asset

Pass the zero address for a native ETH launch, or an approved ERC-20 to launch against that asset instead. Approval is owner-gated and closed by default, so a create flow should only offer assets that pass both checks below. An unapproved asset reverts with PairTokenNotApproved.

Economics are recorded per asset because the phantom reserve is a quantity of the quote asset, and only means anything relative to that asset's decimals. A reserve sized in wei would misprice a six-decimal stablecoin by twelve orders of magnitude. Sizing does not distort the curve, since only the ratio of threshold to phantom reserve determines what fraction of supply reaches the pool, so a launch against an approved ERC-20 trades identically to a native launch of the same size. Format all amounts using the asset's decimals rather than assuming 18.

Checking a quote asset before offering it
const factoryAbi = parseAbi([
  "function approvedPairTokens(address pairToken) view returns (bool)",
  "function pairTokenEconomics(address pairToken) view returns (uint256 phantomQuote, uint256 graduationThreshold, uint8 decimals)",
]);

/**
 * Returns the quote assets a create flow may safely offer. An asset that
 * fails either read will revert at launch, so it should not be listed.
 */
async function usableQuoteAssets(candidates) {
  const usable = [];
  for (const asset of candidates) {
    const [approved, economics] = await Promise.all([
      client.readContract({
        address: factory,
        abi: factoryAbi,
        functionName: "approvedPairTokens",
        args: [asset],
      }),
      client.readContract({
        address: factory,
        abi: factoryAbi,
        functionName: "pairTokenEconomics",
        args: [asset],
      }),
    ]);

    const [phantomQuote, graduationThreshold, decimals] = economics;
    if (!approved || phantomQuote === 0n || graduationThreshold === 0n) continue;

    // Threshold is denominated in the asset's own decimals, so format it
    // with these decimals rather than assuming 18.
    usable.push({ asset, graduationThreshold, decimals });
  }
  return usable;
}

Buying and selling

Before graduation, trades go directly to the launch curve rather than through a router. Buys take an amount of the quote asset, sells take an amount of the launch token, and both take a minimum output and a recipient.

Trading against the curve
const curveAbi = parseAbi([
  "function buy(uint256 quoteIn, uint256 minTokensOut, address recipient) payable returns (uint256 tokensOut)",
  "function sell(uint256 tokensIn, uint256 minQuoteOut, address recipient) returns (uint256 quoteOut)",
  "function isNativeQuote() view returns (bool)",
  "function pairToken() view returns (address)",
]);

// Native-quote launch: quoteIn must equal the value sent.
await wallet.writeContract({
  address: curve,
  abi: curveAbi,
  functionName: "buy",
  args: [quoteIn, minTokensOut, recipient],
  value: quoteIn,
});

// Custom-pair launch: approve the curve first and send no value.
await wallet.writeContract({
  address: pairToken,
  abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]),
  functionName: "approve",
  args: [curve, quoteIn],
});

await wallet.writeContract({
  address: curve,
  abi: curveAbi,
  functionName: "buy",
  args: [quoteIn, minTokensOut, recipient],
});

For a native-quote launch, quoteIn must equal the value sent, and any refund is returned in the same transaction. For a custom-pair launch, approve the curve first and send no value.

A buy that would take the curve past its reserved allocation is filled up to that allocation and refunded the difference, so the tokens received can be fewer than a quote taken a moment earlier suggested. Always read tokensOut from the return value or the CurveBuy event rather than assuming the requested amount, and expect a possible CurveBuyRefunded alongside it.

minTokensOut bounds the price rather than the quantity. A clamped fill is checked against the rate you asked for, not against the total, so a partial fill that honours your price still succeeds instead of reverting. Size the argument from the rate you were quoted.

Once a launch graduates, the curve stops accepting trades and reverts with CurveGraduated. Route to the Uniswap v4 pool from that point. Note that the sell side closes slightly earlier than the buy side: once readyToGraduate() is true the curve is holding the pool's reserves and rejects sells with the same error, even in the window before graduation has actually run.

Pushing a stalled graduation

Graduation normally completes inside the buy that finishes the curve. When it cannot, usually because that transaction was near its gas limit, the curve emits AutoGraduationFailed and the launch sits in the swept phase until someone finishes it.

createGraduatedPool(token) completes it. It is permissionless and retryable, so any caller can move a finished launch into its pool and a failed attempt can simply be tried again. A launch stays in the swept phase until a seed succeeds, so reserves are never stranded by a transient failure. Reverts with WrongGraduationPhase if the launch is not waiting to be seeded.

Getting a quote

The curve exposes no quote function. Pricing is deterministic and cheap to reproduce, so a router or an aggregator computes it from the curve's reserves and its two fee rates rather than calling the contract. The arithmetic below is the curve's own, in the same integer order, so a quote produced this way matches what the trade settles at when nothing moves in between.

Quoting in both directions
const curveAbi = parseAbi([
  "function getReserves() view returns (uint256 quoteReserve, uint256 tokenReserve)",
  "function sellableTokens() view returns (uint256)",
  "function feeBps() view returns (uint256)",
  "function creatorTaxBps() view returns (uint256)",
  "function currentSnipeTaxBps(address recipient) view returns (uint256)",
]);

const BPS = 10_000n;
const ceilDiv = (a, b) => (a + b - 1n) / b;

/**
 * Constant product in the curve's own integer order. No fee is applied
 * here, because both directions charge their fees outside this step.
 */
function amountOut(inAmount, reserveIn, reserveOut) {
  return (inAmount * reserveOut) / (reserveIn + inAmount);
}

function amountIn(outAmount, reserveIn, reserveOut) {
  return (outAmount * reserveIn) / (reserveOut - outAmount) + 1n;
}

/** Quote asset in, launch token out. */
async function quoteBuy(curve, quoteIn, recipient) {
  const read = (functionName, args) =>
    client.readContract({ address: curve, abi: curveAbi, functionName, args });

  const [reserves, sellable, feeBps, creatorTaxBps, rawSnipeBps] = await Promise.all([
    read("getReserves"),
    read("sellableTokens"),
    read("feeBps"),
    read("creatorTaxBps"),
    read("currentSnipeTaxBps", [recipient]),
  ]);
  const [quoteReserve, tokenReserve] = reserves;

  // The snipe tax is capped so the buyer always nets at least 1% of spend.
  let snipeBps = rawSnipeBps;
  if (snipeBps > 0n) {
    const maxSnipeBps = BPS - feeBps - creatorTaxBps - 100n;
    if (snipeBps > maxSnipeBps) snipeBps = maxSnipeBps;
  }

  // Every fee comes off the input before the curve prices the trade.
  let spent = quoteIn;
  const fee = (spent * feeBps) / BPS;
  const tax = (spent * creatorTaxBps) / BPS;
  const snipeTax = (spent * snipeBps) / BPS;
  let tokensOut = amountOut(spent - fee - tax - snipeTax, quoteReserve, tokenReserve);

  // A buy that would cross the reserved allocation fills to the edge, and
  // the input is repriced from the token side so the rest is refunded.
  if (tokensOut > sellable) {
    tokensOut = sellable;
    const net = amountIn(sellable, quoteReserve, tokenReserve);
    const grossed = ceilDiv(net * BPS, BPS - feeBps - creatorTaxBps - snipeBps);
    spent = grossed < quoteIn ? grossed : quoteIn;
  }

  return { tokensOut, spent, refund: quoteIn - spent };
}

/** Launch token in, quote asset out. */
async function quoteSell(curve, tokensIn) {
  const read = (functionName) =>
    client.readContract({ address: curve, abi: curveAbi, functionName });

  const [reserves, feeBps, creatorTaxBps] = await Promise.all([
    read("getReserves"),
    read("feeBps"),
    read("creatorTaxBps"),
  ]);
  const [quoteReserve, tokenReserve] = reserves;

  // A sell is priced first and the fees come off the output. There is no
  // snipe tax on this side.
  const gross = amountOut(tokensIn, tokenReserve, quoteReserve);
  const fee = (gross * feeBps) / BPS;
  const tax = (gross * creatorTaxBps) / BPS;
  return gross - fee - tax;
}

Which reserves to price against

Use getReserves(). It returns the pricing reserves, which include the phantom quote and exclude fees already accrued and waiting to be swept. realQuoteReserve() describes what the curve physically holds and is the wrong input for a quote. The marginal price, quoteReserve ÷ tokenReserve, is a spot rate for display only and carries no slippage.

The two directions are not symmetric

A buy charges its fees on the way in. The trade fee, the creator tax, and any snipe tax come off the quote amount first, and only the remainder reaches the curve, so a buyer moves the price less than their spend implies. A sell is priced first and the fees come off the quote output. Quoting a sell as a mirrored buy overstates the proceeds.

Only buys carry the snipe tax. Read currentSnipeTaxBps(recipient) keyed to the wallet that will receive the tokens, since exemptions are held per recipient, and expect it to be large in the opening seconds of a launch. See snipe protection.

Partial fills near graduation

The curve never sells past its reserved allocation. A buy that would cross it is filled to the edge, its input is repriced from the token side, and the difference is refunded in the same transaction. A quote that ignores sellableTokens() overstates both the tokens received and the amount actually spent on the last buy of a launch.

Because the fill can shrink, size minTokensOut from the rate rather than from the total. The curve compares the price you accepted against the price you received, so a clamped fill that honours your rate still settles.

When to stop quoting

Buys close when sellableTokens() reaches zero. Sells close earlier, as soon as readyToGraduate() returns true, because the curve is holding the pool's reserves from that moment even though graduated may still read false. A quote engine that gates only on graduated will offer sells that revert. Once a launch has graduated, route both directions to the Uniswap v4 pool.

Reading state

The factory holds one record per launch that covers everything an interface needs to route and label a token, including which phase it is in and which quote asset it uses.

Reading the launch record
const factoryAbi = parseAbi([
  "struct LaunchedToken { address token; address curve; address deployer; address creatorFeeRecipient; address pairToken; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; uint16 creatorTaxBps; bool buybackEnabled; uint8 phase; uint256 sweptQuote; uint256 sweptTokens; uint256 sweptAt; bool exists; }",
  "function getLaunchedToken(address token) view returns (LaunchedToken)",
]);

const launch = await client.readContract({
  address: factory,
  abi: factoryAbi,
  functionName: "getLaunchedToken",
  args: [token],
});

// phase: 0 NotGraduated, 1 Swept, 2 PoolCreated, 3 Rescued
const isLive = launch.phase === 0;
const isTradingOnV4 = launch.phase === 2;

Phases

phase is the authoritative signal for where a launch is and which venue to route to. Do not infer it from balances or events.

0 NotGraduated
Trading on the curve. Covers both a freshly created launch and one part way through its curve.
1 Swept
Curve closed and drained, pool not yet created. Transient under normal operation.
2 PoolCreated
Trading on Uniswap v4. Route swaps to the pool.
3 Rescued
Recovery path used. Off the normal path, and worth surfacing explicitly. See safety and recovery.

Curve reserves

The pricing reserve includes the phantom quote, a virtual balance that sets the opening price without anyone depositing up front. It is counted for pricing and is never withdrawable, which is why the pricing reserve always reads higher than the amount actually collected.

quoteReserve()
Pricing reserve, including the phantom amount.
realQuoteReserve()
Quote actually collected and still held, net of fees.
tokenReserve()
Tokens the curve still holds, including the reserved floor.
sellableTokens()
Tokens still buyable before the curve closes.
reservedTokens()
Supply held back for the pool. Fixed at launch.
readyToGraduate()
True once sellableTokens() reaches zero.

The reserved allocation is not a separate parameter. It is the token balance corresponding to the graduation threshold on the same curve, so reserving it does not move where a launch graduates, it only stops the curve selling through that point.

Reserved for the poolsupply × phantomQuote ÷ (phantomQuote + threshold)

Price and progress

Both forms of graduation progress agree by construction, so use whichever suits the interface: quote raised against the threshold, or tokens sold against the tradable allocation.

Price and graduation progress
const curveAbi = parseAbi([
  "function getReserves() view returns (uint256 quoteReserve, uint256 tokenReserve)",
  "function realQuoteReserve() view returns (uint256)",
  "function graduationThreshold() view returns (uint256)",
  "function sellableTokens() view returns (uint256)",
  "function readyToGraduate() view returns (bool)",
  "function graduated() view returns (bool)",
]);

const [quoteReserve, tokenReserve] = await client.readContract({
  address: curve,
  abi: curveAbi,
  functionName: "getReserves",
});

// Marginal price of one token in the quote asset. quoteReserve already
// includes the phantom reserve, so this is the price a very small buy pays.
const price = Number(quoteReserve) / Number(tokenReserve);

// Graduation progress. Both forms agree by construction, so use whichever
// reads better in your interface.
const [raised, threshold] = await Promise.all([
  client.readContract({ address: curve, abi: curveAbi, functionName: "realQuoteReserve" }),
  client.readContract({ address: curve, abi: curveAbi, functionName: "graduationThreshold" }),
]);

const progress = Number(raised) / Number(threshold);

Fee rates

Surface both rates before a trade. feeBps is the base trade fee and creatorTaxBps is the creator's own tax, fixed at launch and charged on top. Their sum is what the trader actually pays on the quote leg. Both are immutable for the life of the launch, so they are safe to cache per token.

getLaunchFeePolicy(token) returns how the base fee is divided and what the pool charges after graduation. The protocol share is taken first, the buyback slice comes out of what remains and only when the launch has buybacks enabled, and the creator receives the rest plus the entire creator tax.

Reading fee rates
// Pre-graduation, both rates live on the launch's own curve.
const curveAbi = parseAbi([
  "function feeBps() view returns (uint256)",
  "function creatorTaxBps() view returns (uint256)",
  "function buybackEnabled() view returns (bool)",
]);

const [feeBps, creatorTaxBps] = await Promise.all([
  client.readContract({ address: curve, abi: curveAbi, functionName: "feeBps" }),
  client.readContract({ address: curve, abi: curveAbi, functionName: "creatorTaxBps" }),
]);

// Total cost to the trader, in basis points of the quote leg.
const totalTradeCostBps = feeBps + creatorTaxBps;

// How that fee is divided, and the rate the pool charges after graduation.
const policyAbi = parseAbi([
  "struct FeePolicy { address protocolFeeRecipient; uint16 protocolFeeShareBps; uint16 buybackBurnBps; uint16 hookFeeBps; uint16 maxInternalPriceImpactBps; }",
  "function getLaunchFeePolicy(address token) view returns (FeePolicy)",
]);

const policy = await client.readContract({
  address: factory,
  abi: policyAbi,
  functionName: "getLaunchFeePolicy",
  args: [token],
});

Token metadata

Name, symbol, and decimals are ordinary ERC-20 reads. Everything else the creator supplied at launch comes back in a single call.

Reading creator metadata
// Name, symbol, and decimals are standard ERC-20 reads. Everything the
// creator set at launch beyond that lives in one call.
const tokenAbi = parseAbi([
  "struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
  "function getTokenInfo() view returns (address tokenDeployer, string tokenLogo, string tokenDescription, Socials tokenSocials)",
]);

const [deployer, logo, description, socials] = await client.readContract({
  address: token,
  abi: tokenAbi,
  functionName: "getTokenInfo",
});

Buyback vest

A launch with buybacks enabled accumulates its own token in the vault, vesting linearly over VESTING_DURATION, five years. vestingStart is weighted and moves forward as new buybacks land, so compute progress from the live start rather than from the launch date.

Reading and releasing a vest
const vaultAbi = parseAbi([
  "function totalLocked(address token) view returns (uint256)",
  "function totalReleased(address token) view returns (uint256)",
  "function vestedAmount(address token) view returns (uint256)",
  "function releasable(address token) view returns (uint256)",
  "function vestingStart(address token) view returns (uint256)",
  "function VESTING_DURATION() view returns (uint256)",
  "function release(address token) returns (uint256 released)",
]);

const [locked, released, vested, releasable, start, duration] = await Promise.all([
  "totalLocked", "totalReleased", "vestedAmount", "releasable", "vestingStart",
].map((fn) =>
  client.readContract({ address: buybackVault, abi: vaultAbi, functionName: fn, args: [token] }),
).concat(
  client.readContract({ address: buybackVault, abi: vaultAbi, functionName: "VESTING_DURATION" }),
));

// vestingStart moves forward as new buybacks land, so progress is derived
// from the live start rather than from the launch date.
const progress = Number(vested) / Number(locked);

// Callable only by the vest's own beneficiaries, meaning the creator fee
// recipient or the protocol recipient. Any other caller reverts with
// NotVestBeneficiary. Either side releasing pays out both, splitting the
// released amount and crediting each in the escrow.
if (releasable > 0n) {
  await wallet.writeContract({
    address: buybackVault,
    abi: vaultAbi,
    functionName: "release",
    args: [token],
  });
}

Pending takeovers

A protocol-proposed creator fee recipient change is readable before it takes effect via pendingCreatorFeeRecipient(token), which returns the proposed address alongside its effectiveAt and expiresAt timestamps. Both are zero when nothing is pending. Surfacing this is what gives holders the advance notice the timelock is designed to provide.

Claiming fees

Fees are credited to an escrow rather than pushed to recipients, so a recipient that cannot receive a transfer can never block a sweep for everyone else. Recipients withdraw on their own schedule.

The escrow keeps a native ledger and a per-token ledger. A native launch credits the first, a custom-pair launch credits the second under its quote asset, and a released buyback vest credits it under the launch token. A creator with launches against several quote assets therefore holds several separate balances and claims each one independently.

Claiming across assets
const escrowAbi = parseAbi([
  "function balanceOf(address recipient) view returns (uint256)",
  "function balanceOfToken(address recipient, address token) view returns (uint256)",
  "function claim()",
  "function claimToken(address token)",
]);

// A native-quote launch credits the ETH ledger.
const ethOwed = await client.readContract({
  address: feeEscrow,
  abi: escrowAbi,
  functionName: "balanceOf",
  args: [creator],
});

// A custom-pair launch credits the quote asset's ledger instead, and a
// released buyback vest credits the launch token's ledger. A creator with
// several launches may therefore be owed several different assets.
const assetOwed = await client.readContract({
  address: feeEscrow,
  abi: escrowAbi,
  functionName: "balanceOfToken",
  args: [creator, quoteAsset],
});

if (ethOwed > 0n) {
  await wallet.writeContract({ address: feeEscrow, abi: escrowAbi, functionName: "claim" });
}
if (assetOwed > 0n) {
  await wallet.writeContract({
    address: feeEscrow,
    abi: escrowAbi,
    functionName: "claimToken",
    args: [quoteAsset],
  });
}

Fees reach the escrow only after a sweep

A trade does not credit the escrow directly. Before graduation the fee accrues on the launch's own curve, and after graduation it accrues on the hook, in both cases until a sweep moves it. So an escrow balance reading zero does not mean a launch has earned nothing, and an interface that shows only the escrow will understate what a creator is owed.

Pre-graduation the sweep is sweepFees(minBuybackTokensOut) on the curve, and post-graduation it is sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut) on the hook. Both are callable by the protocol's sweep operator or by the creator. A creator's own call is refused with InternalSwapRequiresOperator when the sweep would need an internal swap, which is the case when a buyback has to be executed or when launch-token fees have to be converted, since those legs move the price and are bounded by the operator. To show a creator their true unswept position, read quoteFeeBalance and creatorTaxBalance on the curve, or pendingFees and pendingCreatorTax on the hook, and add them to the escrow balance.

A creator can redirect future payouts with transferCreatorFeeRecipient(token, newRecipient), callable only by the current recipient. It takes effect immediately, moves the buyback vest beneficiary with it, and applies whether the launch is still on its curve or already in its pool. It does not move balances already credited, so claim those first. Reverts with NotCreatorFeeRecipient from any other caller.

Events to index

Indexing the factory gives you every launch, and indexing each curve gives you that launch's trade history. Together they are enough to reconstruct the full state of the protocol without any pons service.

Indexing launches and trades
import { parseAbiItem } from "viem";

// Every launch in the protocol, with the curve that prices it.
const launches = await client.getLogs({
  address: factory,
  event: parseAbiItem(
    "event TokenLaunched(address indexed token, address indexed curve, address indexed deployer, address pairToken, uint256 launchConfigId, uint256 graduationThreshold)",
  ),
  fromBlock: deploymentBlock,
  toBlock: "latest",
});

// Trades on a single curve. A buy that finishes a launch may be partially
// filled, so read tokensOut and quoteIn from the event rather than assuming
// the amount the caller requested.
//
// fee is the base trade fee and, on a buy inside the launch window, the
// snipe tax folded in with it. tax is the creator tax, reported separately.
const trades = await client.getLogs({
  address: curve,
  events: [
    parseAbiItem(
      "event CurveBuy(address indexed buyer, address indexed recipient, uint256 quoteIn, uint256 tokensOut, uint256 fee, uint256 tax)",
    ),
    parseAbiItem(
      "event CurveSell(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 quoteOut, uint256 fee, uint256 tax)",
    ),
  ],
  fromBlock: launchBlock,
  toBlock: "latest",
});
TokenLaunched
Factory. A new launch, its curve, and its quote asset.
CurveBuy, CurveSell
Curve. Trades, with the base fee and the creator tax reported separately. On a buy, any snipe tax is included in fee rather than broken out.
CurveBuyRefunded
Curve. A clamped final buy returned unspent quote.
CurveCompleted
Curve. The curve closed and handed over its balances.
LaunchSwept
Factory. Launch entered the swept phase.
PoolGraduated
Factory. Pool created and the position locked.
PoolRegistered
Hook. A graduated pool became known to the hook.
FeesSwept, PoolFeesSwept
Curve and hook. Fee splits before and after graduation.
CreatorFeeRecipientChangeProposed
Factory. A takeover was proposed, with its effective time.
CreatorFeeRecipientUpdated
Factory. The creator payout address changed.
Credited, Claimed
Escrow. Native payouts accrued and withdrawn.
CreditedToken, ClaimedToken
Escrow. The ERC-20 equivalents, carrying the asset address. Every custom-pair payout and every released vest lands here rather than on the native pair, so indexing only the native events will miss most creator revenue.
BuybackLocked
Curve. Quote spent on a buyback and tokens locked.
Locked, Released
Vault. Tokens entering the vest, with the recomputed vesting start, and vested tokens split out to creator and protocol.
AutoGraduationFailed
Curve. Automatic graduation could not complete within the gas available. The launch is finished but still needs a push, so treat this as a work queue.
PoolConversionSkipped, PoolBuybackSkipped
Hook. A sweep left fees unconverted, or skipped a buyback, rather than forcing a bad price.

Uniswap v4 pools

A graduated launch trades as an ordinary Uniswap v4 pool holding the launch token and its quote asset. There is nothing pons-specific about swapping it, so any v4-aware router or aggregator can trade it without integrating against pons at all.

The liquidity is a single full-range position minted at graduation and transferred straight to the locker, where it stays permanently. No one can withdraw it, including the protocol. Trading fees still accrue against it, and those are what the hook distributes.

Reconstructing the pool

Everything needed to build the pool key is on the launch record. The two currencies are the launch token and its quote asset sorted by address, tick spacing comes from the record, the hook is the shared pons hook, and the fee field is zero.

Building the pool key and reading pending fees
import { encodeAbiParameters, keccak256 } from "viem";

// Uniswap v4 sorts the two currencies by address. Native ETH is the zero
// address, so it always takes the currency0 slot.
const [currency0, currency1] =
  launch.pairToken.toLowerCase() < launch.token.toLowerCase()
    ? [launch.pairToken, launch.token]
    : [launch.token, launch.pairToken];

const poolKey = {
  currency0,
  currency1,
  fee: launch.poolFee,           // zero: the hook charges the fee, not the pool
  tickSpacing: launch.tickSpacing,
  hooks: memeHook,
};

const poolId = keccak256(
  encodeAbiParameters(
    [
      { type: "address" }, { type: "address" },
      { type: "uint24" }, { type: "int24" }, { type: "address" },
    ],
    [poolKey.currency0, poolKey.currency1, poolKey.fee, poolKey.tickSpacing, poolKey.hooks],
  ),
);

// Fees the hook is holding for this pool, per currency, before a sweep.
const hookAbi = parseAbi([
  "function pendingFees(bytes32 poolId, address currency) view returns (uint256)",
  "function pendingCreatorTax(bytes32 poolId, address currency) view returns (uint256)",
]);

const pendingQuote = await client.readContract({
  address: memeHook,
  abi: hookAbi,
  functionName: "pendingFees",
  args: [poolId, launch.pairToken],
});

The zero fee is not a mistake. The core pool charges nothing, and the hook charges the swap fee instead, which is what allows it to be split under the same policy the curve used rather than accruing to a liquidity provider that does not exist.

What the hook does

The hook runs on two callbacks. Before a pool is initialised it checks that the pool was registered by the pons factory, so an unrelated pool cannot attach itself to the hook. After a swap it takes the protocol fee and creator tax on the unspecified currency of that swap and records them against the pool. Every other Uniswap callback is disabled and reverts.

It does not gate trading, does not restrict who can swap, does not tax transfers of the launch token, and does not hold user funds between transactions. The only balances it holds are accrued fees awaiting a sweep, which you can read per pool and per currency with pendingFees and pendingCreatorTax.

Sweeping

A sweep converts any launch-token-denominated fee into the quote currency against the pool's own liquidity, then splits the combined total into protocol, buyback, and creator, credits the escrow, and locks any bought-back tokens in the vault. Sweeps that need an internal swap are restricted to a trusted operator and bounded by a maximum price impact. When no conversion is needed, the creator can distribute already-quoted fees themselves.

Per-pool configuration is readable from launches(poolId), which returns the memecoin, the quote token, which side is currency0, the creator and protocol recipients, and the live fee rates for that pool.

Errors

The contracts use custom errors throughout. These are the ones an integration is most likely to surface to a user.

SlippageExceeded
The trade could not meet the minimum output at the price available.
CurveGraduated
The launch has finished on the curve. Route to the pool instead.
LaunchEconomicsMismatch
The pinned terms no longer match. Re-read previewLaunchEconomics and retry.
PairTokenNotApproved
The chosen quote asset is not approved for launches.
PairTokenDecimalsMismatch
The quote asset reports different decimals than were recorded for it.
NativeValueMismatch
On a native launch, the value sent did not equal quoteIn.
UnexpectedNativeValue
Value was sent to a custom-pair launch.
LaunchFeeNotPaid
The launch fee sent did not match launchFee().
CreatorTaxTooHigh
The creator tax exceeds maxCreatorTaxBps().
NotWhitelisted
Launching is currently restricted to approved addresses.
TimelockNotElapsed, TimelockExpired
A proposed creator fee recipient change was executed too early or too late.

Audits

pons v2 is under review by three independent security teams. All three engagements are in progress. Reports will be published here in full, including findings we accepted and any we did not, once the reviews close.

  • SB Security
  • Dingbats
  • Pashov Audit Group

Running three reviews in parallel is deliberate. Independent teams find different classes of issue, and overlapping coverage on the areas that carry the most value, graduation and fee distribution, is worth more than a single deeper pass.

Read this before integrating
  • No audit has closed. Treat v2 as unaudited until the reports are published here.
  • v2 is deployed and the addresses are listed under contracts. Public launches are closed, so only whitelisted addresses can create a token for now. Check canLaunch(address) rather than assuming.
  • An audit is not a guarantee. It reduces risk, it does not remove it.

If you find something, report it to contact@ponsfamily.com rather than disclosing it publicly, and give us a chance to fix it first.

Support

For integration questions, partnership enquiries, proposing a quote asset for approval, or early access to v2 addresses on a test network, reach us at contact@ponsfamily.com.

The v1 protocol is documented separately and continues to operate. See the v1 documentation for tokens launched before v2.