Build

Launch, trade and graduate

Create a token and its curve in one transaction, trade it on its curve or its pool, and create its pool once its curve is swept — the three actions a bot or a tracker sends to POO.MEME's own contracts. A launch pairs with the native coin or with any ERC-20 Pons itself approves as a pair asset, the same list Pons offers its own creators.

PooLauncher has no owner and no allow-list — anyone may call launch, and any module factory registered in its own PooModuleDirectory may be named in a request's own modules[]; isInstallable(factory) checks whether a given factory qualifies. Read pons() off PooLauncher to find Pons's own factory (Addresses and roles).

Build the request

Pons charges its own launch fee, set by Pons rather than by POO.MEME, and unrelated to the curve's own trading fee. It is always paid in the native coin, whatever the pair asset. Read it fresh before you send: it is the minimum msg.value a native-coin launch carries, and the exact msg.value an ERC-20 launch carries.

IPonsLaunchEntry
function launchFee() view returns (uint256)

LaunchRequest is a PooLauncher.launch argument, not a type you read back from anywhere — build it from what you already know:

FieldWhat it is
name, symbol, image, description, website, x, telegramThe token's own profile — image is a URL, not raw bytes.
creatorTaxBpsYour own creator fee rate, basis points, up to 990 (9.9%). POO.MEME's fixed 0.1% is added by PooLauncher on top of this — never part of your own number.
snipeFeeExemptionsUp to 32 addresses exempt from the first few seconds' own decaying fee — the launching wallet already is, without being listed here (The first few seconds). More than 32 refuses TooManySnipeFeeExemptions, the zero address refuses ZeroAddress, and a repeat refuses DuplicateSnipeFeeExemption.
modulesModuleShare[], each {factory, bps, config}: any factory PooLauncher will install (isInstallable(factory) is true), that leg's own share of creatorTaxBps in basis points, and any bytes that factory's own create wants.
payoutsPayoutShare[], each {recipient, bps}: a plain address taking a further share of creatorTaxBps — no factory, no create call, just a second or third payout wallet beside payout itself.
payoutWhere the fee that no module or extra payout claims stays. The zero address defaults to your own sending wallet. modules and payouts together carry at most 8 legs, and their bps must not exceed 10,000.
ownerThe splitter's own owner, or the zero address for none — see what an owner can do.
minTokensOutYour own opening buy's slippage floor — read only when you send more than the launch fee (Send it).
saltYours to choose — search for one whose resulting address ends 0x8888; the launch refuses any that doesn't. See Vanity address and economics.
expectedEconomicsA pin against Pons's own live launch terms moving under you between when you read them and when your transaction lands. See Vanity address and economics.
quoteThe pair asset your curve launches against — the zero address for native coin, or an ERC-20 that Pons's own approvedPairTokens(quote) currently answers true for. Anything else refuses QuoteNotApproved before Pons is ever called, and an approved asset with no Uniswap V3 pool deep enough to price the gas refund owed to whoever pays out this token's fees is refused with Unpriced, also before Pons is called; Pons checks its own list again when it actually deploys.
quoteInHow much of quote your opening buy spends — only meaningful on an ERC-20 pair, where leaving it zero skips the opening buy and any other amount is pulled from you once you've approved PooLauncher for it. On a native-coin pair it must stay zero, or it refuses UnexpectedQuoteIn; size that pair's opening buy instead by what you send launch above launchFee() (Send it).

Vanity address and economics

Every POO.MEME token address ends 0x8888 — PooLauncher checks the address your request would land on, both before it ever calls Pons and again on the address Pons actually returns, and refuses NotVanity if either misses. Nothing mines the salt for you: predictLaunch tells you, off chain, exactly what token, curve and splitter address a given salt would produce for your own request, so a search loop varies salt until the token address it predicts ends 0x8888, then sends that same request to launch.

PooLauncher
function predictLaunch(address sender, bytes32 salt, (string name, string symbol, string image, string description, string website, string x, string telegram, uint16 creatorTaxBps, address[] snipeFeeExemptions, (address factory, uint16 bps, bytes config)[] modules, (address recipient, uint16 bps)[] payouts, address payout, address owner, uint256 minTokensOut, bytes32 salt, bytes32 expectedEconomics, address quote, uint256 quoteIn) request) view returns (address token, address curve, address splitter)
function ADDRESS_SUFFIX() view returns (uint16)

request.expectedEconomics pins the launch to Pons's own terms at the moment you read them. Fetch it fresh from Pons's own factory, for your own pair asset — previewLaunchEconomics(0, quote) for POO.MEME's own config, address(0) for the native coin — and carry that same value into every predictLaunch call and into launch itself. The pair asset moves the predicted addresses too, so mine the salt for the quote you will send. A zero value refuses EconomicsNotPinned; a value that no longer matches what Pons currently answers, because its fee policy moved since you read it, refuses EconomicsMismatch(expected, actual). Read the pin again and retry.

IPonsV2LaunchFactory
function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)

Send it

launch(request) is payable. On a native-coin pair, send at least launchFee(); send more and the difference becomes your own opening buy on the fresh curve, in the same transaction, subject to minTokensOut. On an ERC-20 pair, send launchFee() exactly, and size the opening buy with quoteIn instead — approve PooLauncher for it first, and the launch pulls exactly what it received. It returns the new token, its splitter, every leg it created — each modules entry first, in the same order as request.modules, then each payouts recipient — and what your opening buy bought, if you made one.

PooLauncher
function launch((string name, string symbol, string image, string description, string website, string x, string telegram, uint16 creatorTaxBps, address[] snipeFeeExemptions, (address factory, uint16 bps, bytes config)[] modules, (address recipient, uint16 bps)[] payouts, address payout, address owner, uint256 minTokensOut, bytes32 salt, bytes32 expectedEconomics, address quote, uint256 quoteIn) request) payable returns (address token, address splitter, address[] legs, uint256 bought)
function predictSplitter(bytes32 salt) view returns (address)
event Launched(address indexed token, address indexed creator, address indexed splitter, address curve, address[] factories, address[] legs, uint16[] legBps, uint16 creatorTaxBps, address payout, address owner, uint256 bought)
error ZeroAddress()
error LaunchFeeNotPaid(uint256 fee, uint256 value)
error QuoteNotApproved(address quote)
error UnexpectedQuoteIn(uint256 quoteIn)
error TaxTooHigh(uint16 bps)
error TooManyLegs(uint256 count)
error EmptyLeg(uint256 index)
error LegsExceedWhole(uint256 bps)
error NotRegistered(address factory)
error WrongLauncher(address factory, address launcher)
error ModuleNotCreated(uint256 index, address factory)
error SaltUsed(bytes32 salt)
error NotVanity(address token)
error EconomicsNotPinned()
error EconomicsMismatch(bytes32 expected, bytes32 actual)

A msg.value that falls short of launchFee() is refused whole with LaunchFeeNotPaid — and on an ERC-20 pair, so is one that runs over it, since that pair takes no native coin beyond the fee itself. A request whose predicted address misses 0x8888, whose expectedEconomics is unset or stale, whose quote isn't the zero address or one of Pons's own approved pair assets, or that names a module factory isInstallable answers false for (NotRegistered, WrongLauncher), is refused the same way — before Pons is ever called — so an unmined or stale request costs you nothing beyond gas. A salt you've already landed a launch on is refused with SaltUsed, whatever the rest of the request says. Any of msg.value your opening buy does not use, and any native coin the call already held beyond msg.value, comes back to you in the same transaction; on an ERC-20 pair, any of quoteIn your opening buy does not use comes back the same way.

With viem, mining a vanity salt, pinning Pons's economics, and sending a launch on the native coin with no modules and no opening buy:

launch-token.ts
import { createPublicClient, createWalletClient, http, keccak256, parseAbi, toBytes, type Address, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";

const launcherAbi = parseAbi([
  "function pons() view returns (address)",
  "function predictLaunch(address sender, bytes32 salt, (string name, string symbol, string image, string description, string website, string x, string telegram, uint16 creatorTaxBps, address[] snipeFeeExemptions, (address factory, uint16 bps, bytes config)[] modules, (address recipient, uint16 bps)[] payouts, address payout, address owner, uint256 minTokensOut, bytes32 salt, bytes32 expectedEconomics, address quote, uint256 quoteIn) request) view returns (address token, address curve, address splitter)",
  "function launch((string name, string symbol, string image, string description, string website, string x, string telegram, uint16 creatorTaxBps, address[] snipeFeeExemptions, (address factory, uint16 bps, bytes config)[] modules, (address recipient, uint16 bps)[] payouts, address payout, address owner, uint256 minTokensOut, bytes32 salt, bytes32 expectedEconomics, address quote, uint256 quoteIn) request) payable returns (address token, address splitter, address[] legs, uint256 bought)",
]);
const ponsAbi = parseAbi([
  "function launchFee() view returns (uint256)",
  "function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)",
]);

const launcher = process.env.LAUNCHER as Address;
const account = privateKeyToAccount(process.env.BOT_PRIVATE_KEY as Hex);
const client = createPublicClient({ chain: robinhood, transport: http(process.env.RPC_URL) });
const wallet = createWalletClient({ account, chain: robinhood, transport: http(process.env.RPC_URL) });

const pons = await client.readContract({ address: launcher, abi: launcherAbi, functionName: "pons" });
const fee = await client.readContract({ address: pons, abi: ponsAbi, functionName: "launchFee" });
const expectedEconomics = await client.readContract({
  address: pons,
  abi: ponsAbi,
  functionName: "previewLaunchEconomics",
  args: [0n, "0x0000000000000000000000000000000000000000"],
});

const request = {
  name: "Example",
  symbol: "EX",
  image: "https://example.com/logo.png",
  description: "An example launch.",
  website: "https://example.com",
  x: "",
  telegram: "",
  creatorTaxBps: 500,
  snipeFeeExemptions: [],
  modules: [],
  payouts: [],
  payout: account.address,
  owner: "0x0000000000000000000000000000000000000000" as Address,
  minTokensOut: 0n,
  salt: keccak256(toBytes("poo.meme example")),
  expectedEconomics,
  quote: "0x0000000000000000000000000000000000000000" as Address,
  quoteIn: 0n,
};

for (;;) {
  const [token] = await client.readContract({
    address: launcher,
    abi: launcherAbi,
    functionName: "predictLaunch",
    args: [account.address, request.salt, request],
  });
  if ((BigInt(token) & 0xffffn) === 0x8888n) break;
  request.salt = keccak256(request.salt);
}

const { request: call, result } = await client.simulateContract({
  account,
  address: launcher,
  abi: launcherAbi,
  functionName: "launch",
  args: [request],
  value: fee,
});
await wallet.writeContract(call);
console.log("token", result[0], "splitter", result[1]);

Sweep and graduate

getLaunchedToken(token).phase is the one read — see Where a launch stands for the full mapping. curve is the address to read next; graduationThreshold is the real quote reserve the curve graduates at — 4.2 ETH on every launch this platform's own ponsConfigId opens, read here rather than assumed. sweptQuote, sweptTokens and sweptAt are zero until the sweep, and record what the curve held at that moment from then on — its own reserves are drained, not kept current, once swept.

IPonsFactory
function getLaunchedToken(address token) view returns ((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))

While NotGraduated, the curve's own reserves are live:

IPonsCurve
function getReserves() view returns (uint256 quoteReserve, uint256 tokenReserve)
function graduated() view returns (bool)
function feeBps() view returns (uint256)
function creatorTaxBps() view returns (uint256)

quoteReserve over graduationThreshold is a launch's progress. graduated() is the same fact phase already carries, read straight off the curve rather than through Pons's factory — the two never disagree while the curve exists. feeBps and creatorTaxBps are the curve's own live copies of the rates a trade pays now (What a trade pays); read them here rather than assume the figures another token quoted.

Once phase reads Swept, buy and sell on both the curve and PooTradeRouter refuse — the token is not tradable on either side until its pool exists. Pons's own factory offers the one step that creates it: createGraduatedPool(address token) returns (uint256 positionId), permissionless and retryable, callable by anyone until it succeeds. It seeds the new pool from exactly what the curve already swept, so it takes no value and no slippage argument. This call is Pons's own concrete factory's, not part of the narrow interface POO.MEME's own contracts declare, so no fence on this page carries it — verify it directly against Pons's own deployed source before you send it.

Once the pool exists, phase reads PoolCreated and trading resumes — through PooTradeRouter, the same call whichever side a token is on (Buy, sell and quote a trade).

With cast, a launch's progress toward its sweep:

LAUNCH=$(cast call "$PONS_FACTORY" "getLaunchedToken(address)((address,address,address,address,address,uint256,uint24,int24,uint16,bool,uint8,uint256,uint256,uint256,bool))" "$TOKEN" --rpc-url "$RPC_URL")
cast call "$CURVE" "getReserves()(uint256,uint256)" --rpc-url "$RPC_URL"

With viem, the same read, decoded:

read-phase.ts
import { createPublicClient, http, parseAbi, type Address } from "viem";
import { robinhood } from "viem/chains";

const ponsFactoryAbi = parseAbi([
  "function getLaunchedToken(address token) view returns ((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))",
]);
const curveAbi = parseAbi(["function getReserves() view returns (uint256 quoteReserve, uint256 tokenReserve)"]);

const factory = process.env.PONS_FACTORY as Address;
const token = process.env.TOKEN as Address;
const client = createPublicClient({ chain: robinhood, transport: http(process.env.RPC_URL) });

const launch = await client.readContract({ address: factory, abi: ponsFactoryAbi, functionName: "getLaunchedToken", args: [token] });
const PHASE = ["Live", "Graduating", "Launched", "Rescued"] as const;
console.log("phase", PHASE[launch.phase]);

if (launch.phase === 0) {
  const [quoteReserve] = await client.readContract({ address: launch.curve, abi: curveAbi, functionName: "getReserves" });
  console.log("progress", Number((quoteReserve * 10_000n) / launch.graduationThreshold) / 100, "%");
}

Buy, sell and quote a trade

PooTradeRouter resolves which side to hit on its own, so a bot never needs a separate code path for either phase. Every amount on the quote side is in the token's own pair asset — the native coin or the ERC-20 its launch chose (Build the request); read which with getLaunchedToken(token).pairToken on Pons's own factory, the zero address meaning the native coin. buy(token, quoteIn, minOut) is payable: on a native-coin pair, send quoteIn of the native coin as the call's value; on an ERC-20 pair, send no value and approve the router for quoteIn of the pair asset first — any other value is refused with ValueMismatch. sell(token, amount, minOut) takes tokens you have already approved the router for and pays out in the pair asset. Both work identically before and after graduation — the router reads the token's own phase and trades the curve or the pool as it stands, and refuses with NotTradable while the token sits in the brief Swept gap (Sweep and graduate).

quoteBuy and quoteSell are not view — the pool-side quote needs a real, reverted swap to price (PooTradeRouter's own unlock callback), so a plain eth_call is how every quote is read, exactly the way a wallet simulates a write it has not sent yet. In viem, that is simulateContract, never readContract; with cast, a plain cast call already issues an eth_call regardless of the function's own mutability, so it works unchanged. Both net out every fee already — Pons's own share, the creator's fee, POO.MEME's fixed 0.1%, and, on a brand-new curve, the first few seconds' own decaying fee (The first few seconds) — so the value returned is exactly what buy/sell would deliver if sent in the same block.

PooTradeRouter
function buy(address token, uint256 quoteIn, uint256 minOut) payable returns (uint256 tokensOut)
function sell(address token, uint256 amount, uint256 minOut) returns (uint256 quoteOut)
function quoteBuy(address token, uint256 quoteIn) returns (uint256 tokensOut)
function quoteSell(address token, uint256 tokensIn) returns (uint256 quoteOut)
event Bought(address indexed token, address indexed buyer, bool pool, uint256 quoteIn, uint256 tokensOut)
event Sold(address indexed token, address indexed seller, bool pool, uint256 tokensIn, uint256 quoteOut)
error NotTradable(uint8 phase)
error UnknownLaunch(address token)
error ValueMismatch(uint256 expected, uint256 value)
error InsufficientOutput(uint256 out, uint256 minimum)
error ZeroAmount()

pool on both events is false while the trade filled against the curve, true once it filled against the graduated pool — the same sent-and-received amounts either way, so a tracker can build one candle series across the whole life of a token (Trades).

With viem, a buy on a native-coin pair that accepts one percent less than the live quote — on an ERC-20 pair, approve the router for amountIn of the pair asset first and send no value:

buy-on-router.ts
import { createPublicClient, createWalletClient, http, parseAbi, parseEther, type Address, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";

const routerAbi = parseAbi([
  "function quoteBuy(address token, uint256 quoteIn) returns (uint256 tokensOut)",
  "function buy(address token, uint256 quoteIn, uint256 minOut) payable returns (uint256 tokensOut)",
]);

const router = process.env.ROUTER as Address;
const token = process.env.TOKEN as Address;
const account = privateKeyToAccount(process.env.BOT_PRIVATE_KEY as Hex);
const client = createPublicClient({ chain: robinhood, transport: http(process.env.RPC_URL) });
const wallet = createWalletClient({ account, chain: robinhood, transport: http(process.env.RPC_URL) });

const amountIn = parseEther("0.1");
const { result: quoted } = await client.simulateContract({
  address: router,
  abi: routerAbi,
  functionName: "quoteBuy",
  args: [token, amountIn],
});
const minOut = (quoted * 9_900n) / 10_000n;

const { request } = await client.simulateContract({
  account,
  address: router,
  abi: routerAbi,
  functionName: "buy",
  args: [token, amountIn, minOut],
  value: amountIn,
});
await wallet.writeContract(request);

With cast, a sell — approve the router first, quote, then send:

cast send "$TOKEN" "approve(address,uint256)" "$ROUTER" "$AMOUNT" --rpc-url "$RPC_URL" --account bot
QUOTE=$(cast call "$ROUTER" "quoteSell(address,uint256)(uint256)" "$TOKEN" "$AMOUNT" --rpc-url "$RPC_URL")
cast send "$ROUTER" "sell(address,uint256,uint256)" "$TOKEN" "$AMOUNT" "$MIN_OUT" --rpc-url "$RPC_URL" --account bot

Collecting a launch's fees

Every launch's own splitter holds what Pons and POO.MEME's cuts leave behind, in the launch's own pair asset (quote(), the zero address for the native coin). collect() is permissionless — anyone may call it, on any token, at any time — and pays the caller's own gas back out of what it collects, capped at 2%, before it pays POO.MEME's share, each module's leg and the creator's own payout. On a native-coin pair the refund is paid in the coin. On an ERC-20 pair the splitter prices the gas through its launch's own refund route and the caller receives ETH from the swap, or the pair asset itself if the swap fails; when the route cannot price it the caller is refunded nothing and the whole amount is shared, and the refund can never make a collect() revert. A leg an ERC-20 pair asset refuses to pay — a paused or blocklisted recipient — is set aside in deferredOf(recipient) while every other leg is paid, and anyone may release(recipient) it to that recipient alone once it can be paid.

PooSplitter
function collect() returns (uint256 amount)
function collectable() view returns (uint256)
function legs() view returns ((address module, uint16 bps)[])
function quote() view returns (address)
function deferredOf(address recipient) view returns (uint256 amount)
function release(address recipient) returns (uint256 amount)
event Collected(address indexed caller, uint256 amount, uint256 platform, uint256 refund, uint256[] legs, uint256 payout)
event Deferred(address indexed recipient, uint256 amount)
event Released(address indexed recipient, uint256 amount)
error NothingToCollect()

collectable() previews what a collect() right now would find, without spending gas; a splitter with nothing to collect refuses with NothingToCollect rather than emitting an empty event. Find a token's own splitter with PooLauncher.splitterOf(token) (Addresses and roles).