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, and you can exit at any point before the launch graduates.

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.

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. Once tokens have vested, anyone can trigger the release, so it does not depend on the creator being around.

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.

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.
Addresses

v2 addresses are not published yet. Three independent audits are in progress, and addresses will be listed here once the reviews close and the contracts are deployed. Until then, resolve the per-launch curve and token from the factory rather than hardcoding anything.

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 } 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; }",
  "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

// 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,
    },
    launchConfigId,
    pairToken,
  ],
  value: launchFee,
});

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, so check launchEnabled() and whitelistedLaunchers(address) before showing a create flow.

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.

Once a launch graduates, the curve stops accepting trades and reverts with CurveGraduated. Route to the Uniswap v4 pool from that point.

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.

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 creatorTaxBpsis 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);

// Permissionless. Splits the released amount between the creator and the
// protocol and credits both 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],
  });
}

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.
const trades = await client.getLogs({
  address: curve,
  events: [
    parseAbiItem(
      "event CurveBuy(address indexed buyer, address indexed recipient, uint256 quoteIn, uint256 tokensOut, uint256 fee)",
    ),
    parseAbiItem(
      "event CurveSell(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 quoteOut, uint256 fee)",
    ),
  ],
  fromBlock: launchBlock,
  toBlock: "latest",
});
TokenLaunched
Factory. A new launch, its curve, and its quote asset.
CurveBuy, CurveSell
Curve. Trades, with the fee charged on each.
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 contracts are not deployed to mainnet, and addresses are not published.
  • 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.