# 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](https://poo.meme/docs/developers/contracts#addresses)).

## 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`

```solidity
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:

| Field | What it is |
| --- | --- |
| `name`, `symbol`, `image`, `description`, `website`, `x`, `telegram` | The token's own profile — `image` is a URL, not raw bytes. |
| `creatorTaxBps` | Your 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. |
| `snipeFeeExemptions` | Up 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](https://poo.meme/docs/buyers/fees#snipe)). More than 32 refuses `TooManySnipeFeeExemptions`, the zero address refuses `ZeroAddress`, and a repeat refuses `DuplicateSnipeFeeExemption`. |
| `modules` | `ModuleShare[]`, 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. |
| `payouts` | `PayoutShare[]`, 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. |
| `payout` | Where 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. |
| `owner` | The splitter's own owner, or the zero address for none — see [what an owner can do](https://poo.meme/docs/creators/run#handoff). |
| `minTokensOut` | Your own opening buy's slippage floor — read only when you send more than the launch fee ([Send it](https://poo.meme/docs/developers/actions#send)). |
| `salt` | Yours to choose — search for one whose resulting address ends `0x8888`; the launch refuses any that doesn't. See [Vanity address and economics](https://poo.meme/docs/developers/actions#vanity). |
| `expectedEconomics` | A 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](https://poo.meme/docs/developers/actions#vanity). |
| `quote` | The 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. |
| `quoteIn` | How 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](https://poo.meme/docs/developers/actions#send)). |

## 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`

```solidity
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`

```solidity
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`

```solidity
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`

```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](https://poo.meme/docs/developers/contracts#status) 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`

```solidity
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`

```solidity
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](https://poo.meme/docs/buyers/fees#table)); 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](https://poo.meme/docs/developers/actions#trade)).

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

```sh
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`

```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](https://poo.meme/docs/developers/actions#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 `approve`d 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](https://poo.meme/docs/developers/actions#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](https://poo.meme/docs/buyers/fees#snipe)) — so the value returned is exactly what `buy`/`sell` would deliver if sent in the same block.

`PooTradeRouter`

```solidity
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](https://poo.meme/docs/developers/contracts#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`

```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:

```sh
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`

```solidity
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](https://poo.meme/docs/developers/contracts#addresses)).
