# How baselaunch works, from launch to locked liquidity

baselaunch launches meme coins on Base: creating one costs 0.0005 ETH plus gas, and its whole supply of 1,000,000,000 tokens starts on a bonding curve. When 714,285,714 tokens have been bought, the coin graduates to a Uniswap pool whose liquidity is locked for good. Coin and token mean the same thing here: an ERC-20 on Base.

This page is the whole manual: how a coin is created, priced, traded, graduated and paid out, and exactly what its creator and the protocol owner can and cannot change. Part I explains the protocol in plain language. Part II gives builders the contracts, functions, events and code to work with it directly, and the public API, its live feed and its SDK. Part III points to the contract reference, a page per launcher contract with every function and event. Short answers to common questions are on the [FAQ](https://baselaunch.app/faq).

Numbers checked on chain on 20 October 2026. Last updated 2026-10-21.

Part I

Protocol

What happens to a coin from launch to graduation and after, who is paid what, and who can change what. No code.

## 1. Overview

baselaunch is a launchpad for meme coins on Base. Anyone can create a coin in a single transaction for 0.0005 ETH plus network gas. Each coin is a plain ERC-20 with a fixed supply of 1,000,000,000 tokens, minted once at creation and placed entirely in the coin's own bonding curve. There is no presale, no team allocation and no way to mint more later.

A coin trades in two places over its life. It starts on its bonding curve, a contract that always quotes a price and fills every buy and sell itself. When the curve has sold its last sellable token, the coin graduates: the money the curve collected and a fixed share of the supply open a Uniswap v4 pool, and the pool's liquidity is locked for good. Your balance is the same token before and after; only the place you trade it changes.

baselaunch never holds your funds. Your wallet signs every transaction, and baselaunch.app is one interface to public contracts on Base that anyone can call directly. Fees are paid in the coin's pair asset and split on chain between the protocol, the creator and, when there is one, the app the coin was launched through and the app that sent the trade.

Coin and token mean the same thing here: an ERC-20 on Base.

- Network: Base (chain ID 8453)
- Launch fee: 0.0005 ETH plus gas, paid in ETH whatever the pair
- Supply: 1,000,000,000 tokens per coin, minted once
- Trading fee: 1% on the curve, 1% in the pool, plus a creator fee of 0 to 10%
- Graduation: When 714,285,714 tokens are sold (4.2 ETH in the curve on an ETH pair)
- Liquidity after graduation: Locked permanently; there is no function to withdraw it
- Launch tax: Buys only, up to 98% in the launch second with no creator fee (99% of the buy including the 1% fee), falling to zero within 3 seconds

This book has two parts. Part I, sections 1 to 13, explains the protocol in plain language for traders and creators. Part II repeats each idea for builders, with exact function names, contract addresses and code. Section numbers and anchors stay stable, so a link such as [#fees](#fees) keeps pointing at the same place.

## 2. Launch lifecycle

Every coin goes through the same four steps, in this order. None of them needs permission from baselaunch or from the creator once the coin exists.

1. **Create** The creator picks a name, ticker, image, pair and creator fee, pays the 0.0005 ETH launch fee, and can make an opening buy in the same transaction. The full supply is minted straight into the coin's bonding curve.
2. **Trade the curve** Anyone buys from and sells back to the curve at the price it quotes. The price rises as tokens are bought and falls as they are sold. Buys in the first 3 seconds pay a launch tax.
3. **Graduate** The buy that takes the last of the 714,285,714 sellable tokens closes the curve. Its collected funds and the reserved tokens open a Uniswap v4 pool at the curve's final price.
4. **Trade the pool** From then on the coin trades in that pool, through baselaunch or any app that routes Uniswap v4. The pool's liquidity is locked forever, and the same fee rules keep applying.

Graduation runs inside the buy that finishes the curve. If it cannot complete there, for example because that transaction ran short of gas, the coin waits in a finished state and anyone can complete it later with a public function. baselaunch also runs a service that does this automatically.

## 3. Bonding curve

Think of the curve as a vending machine that sets its own price. It holds every token that has not been bought yet, and its price depends only on how many it has sold. Buy, and you take tokens out of the machine at a slightly higher price than the buyer before you. Sell, and you put tokens back and receive the pair asset at the current price. Because the contract itself is the other side of every trade, there is always a price to buy at and always a price to sell at while the curve is open.

The price never starts at zero. The curve behaves as if it already held 1.68 ETH (on an ETH pair) before anyone bought. This virtual reserve sets the opening price at 0.00000000168 ETH per token; it is never paid out and cannot be withdrawn. By the time the curve closes, the price has risen 12.25 times, to 0.00000002058 ETH.

### Price impact

Every buy moves the price for the next one, so a large order pays more per token on average than a small one. That is not a fee; it is how any market with limited depth behaves. What the first buyer of a fresh ETH-pair coin receives, after the launch tax window, with the 1% trading fee and no creator fee:

| First buy | Tokens received | Share of supply | Price afterwards |
| --- | --- | --- | --- |
| 0.01 ETH | 5,858,335 | 0.59% | 0.0000000017 ETH |
| 0.1 ETH | 55,649,241 | 5.56% | 0.000000001884 ETH |
| 0.5 ETH | 227,586,207 | 22.76% | 0.000000002816 ETH |
| 1 ETH | 370,786,517 | 37.08% | 0.000000004243 ETH |
| 2 ETH | 540,983,607 | 54.1% | 0.000000007974 ETH |

### The formula behind it

The curve is a constant product with a virtual reserve. In words: the virtual reserve plus the real money in the curve, multiplied by the tokens still in the curve, always equals the virtual reserve multiplied by the full supply.

Curve `(V + R) × T = V × S`

Price of one token `(V + R) ÷ T`

- V: The virtual reserve: 1.68 ETH on an ETH pair.
- R: The real pair asset buyers have paid in, after fees.
- T: The tokens still held by the curve.
- S: The supply, 1,000,000,000.

### Fees on the curve

Fees are always charged in the pair asset, never in the coin. On a buy, the trading fee, any creator fee and any launch tax come off what you pay before the curve prices the rest. On a sell, the curve prices your tokens first and the fees come off what you receive. Section [7](#fees) shows where each part goes.

### When selling to the curve stops

Once the last sellable token is bought, the curve is holding exactly what the pool will open with, so it stops accepting sells. Normally the pool opens in that same transaction and selling continues there; between the two, a sell is refused rather than filled at a price that would change the pool.

## 4. Graduation

From the moment a coin is created, its curve holds back a fixed number of tokens: 285,714,286 of the 1,000,000,000. It only ever sells the other 714,285,714. The reserved amount follows from the virtual reserve and the graduation target, so it is the same for every coin launched on the same settings. Every such coin therefore graduates into a pool of the same size, at the same price, however its curve happened to be bought out: in one buy or in ten thousand.

On an ETH pair the curve holds 4.2 ETH when the last sellable token is bought. That last buy is filled only up to what is left. You pay for the tokens you receive and the rest of your payment comes back in the same transaction, so nobody can make the finishing buy fail by leaving an awkward remainder. Your price limit still holds: a clamped buy may receive fewer tokens than asked, never a worse price per token.

### What opens the pool

The curve priced tokens as if it held 1.68 ETH more than it really did. The pool can only be funded with real money, so it receives just enough of the reserved tokens to open at exactly the curve's final price. The tokens left over are locked permanently and never circulate. Someone who bought the last curve token pays the same price the pool quotes a moment later.

| At graduation, ETH pair | Amount |
| --- | --- |
| Pair asset into the pool | 4.2 ETH |
| Tokens into the pool | 204,081,633 |
| Tokens locked permanently | 81,632,653 |
| Pool opening price | 0.00000002058 ETH |

### Where the supply ends up

| Holder | Tokens | Share of supply |
| --- | --- | --- |
| Buyers' wallets (sold by the curve) | 714,285,714 | 71.4% |
| Uniswap v4 pool, locked liquidity | 204,081,633 | 20.4% |
| Locked for good, never circulating | 81,632,653 | 8.2% |

### Two steps, both public

Graduation has two steps: the curve's reserves are moved out of the curve, then the pool is created and seeded with them. Both normally happen inside the finishing buy. If seeding fails, the coin stays between the steps and anyone can retry the seeding at any time; it does not need the creator or baselaunch. The rare case where a seed can never succeed is covered in section [12](#owner-powers).

Graduation is a step, not a verdict

It means the curve sold out. It says nothing about a coin's quality and promises no price or volume afterwards, and many coins never reach it.

## 5. Pairs

Every coin is priced in one pair asset, chosen at launch and fixed for life: ETH, USDC, cbBTC or one of 8 tokenized stocks. Buyers pay in the pair, sellers receive the pair, the graduation target is counted in the pair, and after graduation the pool holds the coin against the same pair. Nothing is converted behind your back.

| Pair | Virtual reserve | Graduation target |
| --- | --- | --- |
| ETH | 1.68 ETH | 4.2 ETH |
| USDC | 3,236 USDC | 8,090 USDC |
| cbBTC | 0.052739 cbBTC | 0.13184752 cbBTC |
| MSTRc (tokenized stock) | 23.81828194 MSTRc | 59.54570485 MSTRc |
| GOOGLc (tokenized stock) | 9.3892218 GOOGLc | 23.4730545 GOOGLc |
| NVDAc (tokenized stock) | 14.89346641 NVDAc | 37.23366602 NVDAc |
| SPCXc (tokenized stock) | 21.91988291 SPCXc | 54.79970727 SPCXc |
| TSLAc (tokenized stock) | 8.84851215 TSLAc | 22.12128037 TSLAc |
| AMZNc (tokenized stock) | 12.28879466 AMZNc | 30.72198666 AMZNc |
| METAc (tokenized stock) | 4.57730765 METAc | 11.44326913 METAc |
| AAPLc (tokenized stock) | 9.40980242 AAPLc | 23.52450605 AAPLc |

Every pair has the same curve shape: the target is 2.5 times the virtual reserve. Pairs other than ETH were sized to roughly the value of the ETH curve on the day they were approved, so their dollar size drifts as prices move.

### What changes for a buyer

- On an ERC-20 pair (USDC, cbBTC, a stock token) your wallet first approves the pair asset, one extra signature, then buys.
- The price you see is in the pair asset. A coin can rise against its pair while falling in dollars, if the pair asset falls faster.
- The launch fee is always 0.0005 ETH, whatever the pair.

### What changes for a creator

Creator earnings arrive in the pair asset: a USDC coin pays its creator in USDC, a cbBTC coin in cbBTC. The cost of reaching graduation moves with the pair asset too; if the asset doubles in price, so does the real cost of filling the curve.

### Which assets can be pairs

Only assets the protocol owner has approved can be chosen. Approval says the asset works with the contracts, not that baselaunch endorses it. Removing an approval only stops new coins on that pair; coins already launched keep trading against it.

Pair risk stacks on top of coin risk

The tokenized stocks are issued by a third party that tracks a listed share; baselaunch does not issue, back or redeem them. They are not offered in some regions, including the United States and the United Kingdom, and the site hides them where your region cannot be determined. Whatever happens to the pair asset happens to every coin priced in it.

## 6. Launch protection

Bots try to buy a new coin in the same instant it appears, before any person can react. To take the profit out of that, every buy in a coin's first 3 seconds pays a launch tax on top of the normal fees. It is up to 98% in the launch second with no creator fee (99% of the buy including the 1% fee), and halves again and again until it reaches zero.

The tax falls in fourteen halving steps spread evenly over the window. Base records time in whole seconds, so a buy on a coin with no creator fee pays one of these rates:

| Seconds after launch | Launch tax on the buy |
| --- | --- |
| 0 | 98% |
| 1 | 6.18% |
| 2 | 0.19% |
| 3 | 0% |

The tax is capped so that after the 1% trading fee, the creator fee and the tax, a buy always keeps at least 1% of what it spends. On a coin with a 10% creator fee the launch-second tax is therefore at most 88%.

### Who pays it

- Buys only. Sells and wallet-to-wallet transfers are never taxed.
- The creator's own wallet and the creator's fee wallet are exempt, so the opening buy made inside the launch transaction pays none.
- A launch can name up to 32 further wallets as exempt, for example a team's own buying wallets. The list is fixed in the launch transaction and cannot grow later.
- The tax is computed for the wallet that receives the tokens, not the one that sends the transaction.

### Where the tax goes

The tax is not burned. It is split between the protocol and the creator side exactly like the base trading fee, without a referrer part, and on a coin with buyback turned on, the creator part of it funds the buyback too. Each coin copies the tax start and window from the protocol settings at creation, so a later settings change never reaches a coin already trading.

A speed bump, not a guarantee

After 3 seconds anyone can buy at the normal price, bots included. The tax makes the first seconds expensive; it does not decide who buys.

## 7. Fees

There are only two kinds of charge. Creating a coin costs a one-time launch fee of 0.0005 ETH, plus Base gas, which goes to the network and not to baselaunch. Every trade then pays a base trading fee of 1% on the curve and 1% in the pool after graduation, plus the coin's creator fee, if it has one.

The creator fee is chosen at launch, from 0 to 10% of each trade, and fixed for the life of the coin. All of it goes to the creator side. The base fee and the creator fee together can never exceed 20%.

The Uniswap pool a coin graduates into charges no fee of its own. The 1% is taken by the protocol's pool hook instead, so it can be split by the same rules as on the curve, and you never pay a second pool fee on top.

### Who gets the base fee

1. The protocol share comes first: 30% of the base fee. When the trade came with a trade referrer, the referrer is paid 0.2% of the trade out of this part.
2. The other 70% is the creator side, together with the whole creator fee.
3. When the coin was launched through an app that registered a launch interface, that app receives 20% of the creator side. A coin created on baselaunch.app has no interface, so its creator keeps that part.
4. When buyback is on, part of the creator side buys the coin back first (section [9](#buyback)). The rest is the creator's.

What each party receives from $100 of trades at the 1% base fee, with no creator fee and buyback off, for a coin without and with a launch interface and a trade without and with a referrer. A coin created on baselaunch.app has no launch interface.

| Recipient | Neither | Referrer only | Interface only | Both |
| --- | --- | --- | --- | --- |
| Protocol | $0.30 | $0.10 | $0.30 | $0.10 |
| Trade referrer | $0.00 | $0.20 | $0.00 | $0.20 |
| Creator | $0.70 | $0.70 | $0.56 | $0.56 |
| Launch interface | $0.00 | $0.00 | $0.14 | $0.14 |
| Total | $1.00 | $1.00 | $1.00 | $1.00 |

The same rates apply on the curve and in the pool. A creator fee is added to the creator side in full; on a coin with a launch interface, the interface takes its 20% of that too. Larger worked examples are on [fees](https://baselaunch.app/fees).

### Fixed in the code, set by policy, frozen per coin

- Fixed in the code: The launch interface share (20% of the creator side) and the referrer share (0.2% of volume). No one can change them.
- Set by the owner: The protocol share (between 20% and 50%), the buyback share, the curve and pool fees, the launch tax and the highest creator fee allowed, each within limits written in the contracts. They apply to new launches only.
- Frozen per coin: Every coin copies its fee terms when it is created, so its rates never change while it trades.

The owner can move the address the protocol share is paid to, and can redirect a coin's creator fee recipient after a 3-day public notice. Both are listed in section [12](#owner-powers).

## 8. Payouts and claiming

Fees are never pushed into wallets during a trade. Each recipient's share is credited to a balance in a fee escrow contract, and the recipient claims it whenever they like. This pull model means one recipient that cannot accept a payment, such as a contract without a way to receive ETH, can never block anyone else's trade or payout.

### Getting paid

- Balances are kept per asset. ETH and every ERC-20 pair asset have separate balances, and each is claimed on its own.
- Each launcher version has its own escrow. A coin launched on an earlier set keeps crediting that set's escrow, so a creator with coins on both claims from both.
- The escrow pays a balance only to the address it was credited to. It has no owner and no rescue function.
- Creators are paid in the pair asset, never converted to ETH. A coin in holders mode is paid to its distributor instead (section [10](#holders)).

### When fees reach the escrow

A trade referrer is credited in the trade itself. The other shares first collect on the curve or, after graduation, in the pool hook, and reach the escrow when the coin's fees are swept. A sweep is run by the protocol's fee sweep operator, or by the creator's fee recipient when it needs no swap.

In the pool, part of the fee can be taken in the coin itself. A sweep sells that part for the pair asset before paying it out, within a price impact cap of 3% by default. When the sale would move the price more than that, it is skipped and retried on a later sweep, so a thin pool is never dumped on to pay fees.

### Changing the fee wallet

The creator can point future payouts at a new wallet at any time, and the change is immediate. Balances already credited stay with the address they were credited to, which can still claim them.

## 9. Buyback and vesting

A creator can turn on buyback and lock. 50% of the creator part of the 1% base fee, and of the launch tax on the curve, buys the coin back and locks it. The coins unlock over 5 years, the creator receiving 70% of each release and the protocol 30%. On a coin created on baselaunch.app, which has no launch interface, the creator fee is still paid in full to the fee wallet.

The bought coins are not burned. They go to a buyback vault that holds them and releases them in a straight line over 5 years. Each release is split on the coin's own protocol share, so with today's settings the creator receives 70% of every release and the protocol 30%. Either side can trigger a release, and a release always pays both.

### How the vesting clock works

A coin can add to its lock on every sweep. Rather than track each deposit separately, the vault keeps one vesting start per coin and moves it forward in proportion to each new deposit's size. A small top-up barely moves a large lock's clock, and a new deposit never inherits the progress of older ones.

### What buyback costs the creator

- The buyback is funded from the creator part only: 50% of it by default. The creator fee is never spent on it.
- Because the vault returns only the creator's share of each release, turning buyback on moves some value from the creator to the protocol, compared with taking the fees directly.
- A buyback that cannot fill within the price impact cap returns the unspent part to the creator's payout.
- Only the creator's fee recipient can turn buyback on. The owner can only turn it off. Either change applies to fees charged from then on.

## 10. Holders mode

Instead of a wallet, a creator can send the creator side to the coin's holders. At launch, a holder rewards distributor is created for the coin and set as its fee recipient. It is a copy of one fixed contract with no owner, no sweep and no way to change who it pays, so the money in it can only leave as rewards.

- Collecting: The coin's creator side is credited to the distributor in the fee escrow, in the pair asset. Anyone can pull it into the distributor.
- Epochs: A publisher key posts reward epochs: a list of holders and amounts, weighted by balance and holding time, committed on chain as a Merkle root.
- Claiming: Each epoch can be claimed for 90 days. Anyone may submit a claim, and it always pays the account the epoch names.
- Expiry: What is unclaimed after the window returns to the distributor and goes into later epochs.

The choice is made at launch and the creator cannot change it later. The protocol owner can redirect a coin's fee recipient after a 3-day notice; see [Security](https://baselaunch.app/security). A holders coin has no buyback: only the fee recipient can turn buyback on, and the distributor has no way to do so.

The publisher decides the split

The publisher key chooses how the distributor's unallocated balance is divided. A dishonest publisher could allocate it to any account. It can never allocate more than the unallocated balance, and it cannot touch an epoch already published and still claimable.

## 11. Creator controls

Once a coin exists, its creator can change exactly two things, both through the address that receives the coin's creator fees.

- Where fees go: The fee recipient can hand the role to a new address at any time, effective immediately. It moves future payouts and the buyback vest with it; balances already credited stay where they are.
- Whether to buy back: The fee recipient can turn buyback and lock on or off. The owner can only turn it off, never on.

### Fixed for the life of the coin

The name, ticker, image, description and links, the pair, the creator fee rate, the holders choice and the launch tax exemptions are all set in the launch transaction and cannot be edited afterwards, by the creator or by anyone else.

### What nobody can do

- Mint more coins. The 1,000,000,000 supply is minted once, to the curve, and the coin contract has no mint function and no owner.
- Freeze or blacklist a wallet, or pause transfers. The coin is a plain ERC-20 with no transfer rules.
- Add a tax after launch or raise the creator fee.
- Take the pool's liquidity after graduation.

## 12. Owner powers and safety

The factory, the pool hook, the buyback vault, the liquidity locker and the holder rewards factory each have one owner address. Ownership moves only in two steps, and it cannot be renounced. None of the launcher contracts is a proxy or has an upgrade function, so the code at each address is the code that runs. The launch router, the fee escrow and the launch interface registry have no owner at all.

Most owner settings are copied into a coin when it is created, so they only shape future launches. The full list below is every owner-only function and every key the owner appoints, with its limit as written in the contracts.

`addLaunchConfig` BaseLaunchFactory.sol

- Who: Owner
- What it does: Adds a launch configuration: supply, curve trading fee, virtual reserve, graduation target and pool tick spacing.
- Limit: Curve fee at most 10%, and the curve must quote a small buy. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`updateLaunchConfig` BaseLaunchFactory.sol

- Who: Owner
- What it does: Replaces or disables an existing launch configuration.
- Limit: Same checks as adding one. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setLaunchFee` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets the fixed fee paid in ETH to create a coin.
- Limit: No cap in the contract. Paid once at creation, so it never touches a coin already launched.

`setLaunchEnabled` BaseLaunchFactory.sol

- Who: Owner
- What it does: Opens or closes coin creation to the public.
- Limit: Gates creating new coins only. Buying, selling, graduation and fee claims keep working.

`setWhitelistedLauncher` BaseLaunchFactory.sol

- Who: Owner
- What it does: Lets a named address create coins while public creation is closed.
- Limit: Gates creating new coins only.

`setPairTokenEconomics` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets the virtual reserve and graduation target for an ERC-20 pair asset (for example USDC).
- Limit: The asset must have at least 6 decimals and the curve must quote a small buy. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setPairTokenApproved` BaseLaunchFactory.sol

- Who: Owner
- What it does: Approves or removes an ERC-20 pair asset for new coins.
- Limit: Approval needs a deployed token with economics set and matching decimals. Removing one only stops new coins on that pair.

`setMaxCreatorTaxBps` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets the highest creator fee a creator may choose.
- Limit: Hard ceiling of 10% in the contract. Each coin keeps the creator fee it launched with.

`setSnipeTaxStartBps` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets the launch tax that buys pay at the start of the launch tax window.
- Limit: Zero (off), or above 20% and at most 99%. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setSnipeTaxSeconds` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets how long the launch tax takes to decay to zero.
- Limit: Between 1 and 60 seconds. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setGraduationExecutor` BaseLaunchFactory.sol

- Who: Owner
- What it does: Wires the helper that seeds graduated pools.
- Limit: Can be set once; reverts once set.

`setLaunchDeployer` BaseLaunchFactory.sol

- Who: Owner
- What it does: Wires the helper that deploys each coin and its curve.
- Limit: Can be set once; reverts once set.

`setLaunchForwarder` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sets the router trusted to name the account a launch is made for (creator record, default creator fee recipient, launch tax exemption).
- Limit: Rotatable at any time. Affects coins created through it from then on, not coins already launched.

`setBuybackEnabled` BaseLaunchFactory.sol

- Who: Owner (disable only) or the coin's creator fee recipient
- What it does: Turns a coin's buyback-and-lock off. Only the creator fee recipient can turn it on.
- Limit: Applies to fees charged from then on; fees already charged keep their split.

`setCreatorFeeRecipient` BaseLaunchFactory.sol

- Who: Owner
- What it does: Proposes a new creator fee recipient for any coin, also redirecting its buyback vest. Meant for creators who lose their wallet, but not limited to that case.
- Limit: Executable only after a 3-day timelock, by anyone, within the following 3 days. A transfer by the creator during the wait does not cancel it. Announced on chain when proposed.

`cancelCreatorFeeRecipientChange` BaseLaunchFactory.sol

- Who: Owner
- What it does: Cancels a pending creator fee recipient override.
- Limit: Only cancels; moves nothing.

`forceSweptGraduation` BaseLaunchFactory.sol

- Who: Owner
- What it does: Moves a finished curve's reserves into the factory when the pool seed check refuses them, so the delayed rescue can return them.
- Limit: Only for a curve that has reached its target and whose pool seed is not viable; reverts while anyone could graduate it normally.

`rescueCurveFees` BaseLaunchFactory.sol

- Who: Owner
- What it does: Pays a curve's pending fees straight to the protocol recipient, the launch interface and the creator recipient when the fee escrow path stops working.
- Limit: Pays the normal split to the normal recipients; nothing goes to the owner, and the interface share is kept.

`rescueSweptGraduation` BaseLaunchFactory.sol

- Who: Owner
- What it does: Sends a swept launch's reserves (pair asset and coins) to an address the owner names, for off-chain distribution.
- Limit: Only for a launch stuck between curve and pool: 7 days after the sweep if the pool seed is refused, 30 days if it still looks seedable. Seeding stays open to anyone whenever the seed is valid.

`setFactory` BaseLaunchHook.sol

- Who: Owner
- What it does: Wires the hook to the factory.
- Limit: Can be set once; reverts once set.

`setBuybackVault` BaseLaunchHook.sol

- Who: Owner
- What it does: Wires the hook to the buyback vault.
- Limit: Can be set once; reverts once set.

`setProtocolFeeShareBps` BaseLaunchHook.sol

- Who: Owner
- What it does: Sets the protocol share of the base fee (30% by default).
- Limit: Between 20% and 50%. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setBuybackBurnBps` BaseLaunchHook.sol

- Who: Owner
- What it does: Sets how much of the creator part of the base fee goes to buyback-and-lock when a coin has it on (50% by default).
- Limit: At most 100%. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setHookFeeBps` BaseLaunchHook.sol

- Who: Owner
- What it does: Sets the trading fee of the Uniswap pool a coin graduates into (1% by default).
- Limit: At most 10%, and at most 20% together with the creator fee. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setMaxInternalPriceImpactBps` BaseLaunchHook.sol

- Who: Owner
- What it does: Sets the price impact cap for the fee conversion and buyback swaps (3% by default).
- Limit: Above 0% and below 100%. New launches only: every launch snapshots it at creation, so coins already launched keep their terms.

`setProtocolFeeRecipient` BaseLaunchHook.sol

- Who: Owner
- What it does: Moves the address the protocol share is paid to, for every coin, existing and future.
- Limit: Moves only the protocol share; creator, interface and referrer shares are unaffected.

`setFeeSweepOperator` BaseLaunchHook.sol

- Who: Owner
- What it does: Appoints the operator key that runs fee conversions and buybacks, with minimum outputs, for every coin.
- Limit: Each swap is capped by the coin's own price impact limit. The operator cannot change any share or recipient.

`sweepPoolFees` BaseLaunchHook.sol

- Who: Fee sweep operator, or the coin's creator fee recipient when no swap is needed
- What it does: Converts and pays out a graduated pool's pending fees, choosing the minimum outputs and running the buyback.
- Limit: Pays the coin's fixed split to its recipients; swaps are capped by the coin's price impact limit.

`rescuePoolFees` BaseLaunchHook.sol

- Who: Owner
- What it does: Pays a graduated pool's pending fees straight to the protocol recipient, the launch interface and the creator recipient when the normal sweep cannot deliver.
- Limit: Pays the normal split to the normal recipients; nothing goes to the owner, and the interface share is kept.

`sweepFees` BaseLaunchBondingCurve.sol

- Who: Fee sweep operator, or the coin's creator fee recipient when no swap is needed
- What it does: Pays out a curve's pending fees and runs the buyback.
- Limit: Pays the coin's fixed split to its recipients; the buyback is capped by the coin's price impact limit.

`setFactory` BaseLaunchBuybackVault.sol

- Who: Owner
- What it does: Wires the buyback vault to the factory.
- Limit: Can be set once; reverts once set.

`setFactory` BaseLaunchLocker.sol

- Who: Owner
- What it does: Wires the liquidity locker to the factory.
- Limit: Can be set once; reverts once set.

`setPublisher` BaseLaunchHoldersDistributorFactory.sol

- Who: Owner
- What it does: Appoints the publisher key for every holder rewards distributor.
- Limit: Rotatable at any time. Ownership cannot be renounced.

`publish` BaseLaunchHoldersDistributor.sol

- Who: Publisher key
- What it does: Posts a holder reward epoch: decides which accounts get how much of the distributor's unallocated balance. A dishonest publisher could allocate it to any account.
- Limit: Cannot exceed the unallocated balance and cannot touch an epoch already published and still claimable (90 days).

`setRouter` BaseLaunchInterfaceRegistry.sol

- Who: Registry deployer
- What it does: Binds the router allowed to record each coin's launch interface.
- Limit: Can be set once; the registry has no owner.

### Locked liquidity

A graduated pool's liquidity is a full-range Uniswap v4 position held by the liquidity locker, together with the supply locked at graduation. The locker has no withdraw function. This is not a promise to leave the liquidity alone: the function to take it out does not exist, so no key, including the owner's, can call it.

### A stuck graduation

If a finished curve's reserves have been moved out but the pool can never be seeded, the money would otherwise sit still forever. For that case only, the owner can send the reserves to an address the owner names, to be returned off chain: 7 days after the sweep when the pool seed is refused, or 30 days when it still looks seedable. While a seed is possible, anyone can still seed the pool. A coin rescued this way is marked as rescued on chain for good.

### The fee recipient override

The owner can redirect any coin's creator fee recipient, and with it the buyback vest. It is meant for creators who lose their wallet, but the contract does not limit it to that case. It is announced on chain, waits 3 days, and can then be executed by anyone within the following 3 days before it expires. A transfer by the creator during the wait does not cancel it, so a stolen wallet cannot block a recovery.

### What cannot be recovered

- Locked pool liquidity and the supply locked at graduation.
- Tokens or pair assets sent to a curve, the locker or a coin contract by mistake: they are deliberately left where they land.
- Any transaction once it is on Base.

The same list, with each contract's address and Basescan link, is on [security](https://baselaunch.app/security).

## 13. Risk disclosures

Meme coins are highly speculative and most lose their value. Nothing here is financial advice. Before you buy or launch, weigh these:

- The contracts have not been independently audited yet. An audit is part of our plan.
- Anyone can launch any name and image. Names and tickers are not unique or verified, and a logo can copy a real project. The contract address is the only identity a coin has; check it.
- Every trade pays the 1% trading fee and the coin's creator fee of up to 10%, so buying and selling straight away loses money even if the price does not move. Read the creator fee before you buy.
- Buys in the first 3 seconds pay a launch tax of up to 98% in the launch second with no creator fee (99% of the buy including the 1% fee).
- Graduation is not a quality signal, and many coins never reach it.
- A creator can hold a large share from the opening buy and sell it at any time.
- On a pair other than ETH, the pair asset's own risk adds to the coin's.
- The owner keeps the powers listed in section [12](#owner-powers), including redirecting a coin's creator fees after a 3-day notice.
- Prices, market caps and charts on the site are estimates read from the chain and can lag behind it.
- Transactions on Base are final. Nobody, including baselaunch and the creator, can reverse a trade or return a loss.

Only spend what you can afford to lose

Smart contracts can have bugs, and meme coins can go to zero in minutes.

Part II

Build on baselaunch

Everything a coin does is readable and callable on Base. This part names the contracts, functions and events, with code you can copy.

## 14. Network

Everything in this part reads and writes the contracts directly. No baselaunch server sits in the path of a launch, a trade or a claim, so any Base RPC and any EVM library will do; the samples use viem.

| Field | Value |
| --- | --- |
| Network | Base mainnet |
| Chain ID | 8453 |
| Native asset | ETH (18 decimals) |
| Public RPC | `https://mainnet.base.org` (rate limited; index through your own provider) |
| Explorer | [basescan.org](https://basescan.org) |
| Coin supply | 1,000,000,000 per coin, 18 decimals, minted once to the coin's curve (launch config 0) |
| Launch fee | Read `launchFee()` on the factory; paid in ETH as part of the transaction value |
| Trading fee | 1% base fee on the curve and in the pool, plus the coin's creator fee; read `feeBps()` and `creatorTaxBps()` on the curve |
| Pair assets | ETH, USDC, cbBTC and tokenized stocks; check `approvedPairTokens(pair)` on the factory |
| Launcher versions | 2.1.0 for every new coin; 2.0 for coins launched before the cutover (see [Contracts](#contracts)) |

## 15. Contracts

A launcher is a set of contracts deployed together and bound to each other for life. Every coin is created by one factory and keeps that factory's curve code, hook, escrow, vault and locker forever. Base has two sets: 2.1.0, where every new coin is launched, and 2.0, which keeps serving the coins launched before the cutover. Only the Uniswap v4 contracts and Permit2 are shared.

- Launch factory: Creates each coin and its curve, keeps the launch record (getLaunchedToken), runs both graduation steps and holds the owner settings for new launches. One per set.
- Launch router: Launches a coin and lands the creator's opening buy in the same transaction, optionally in holders mode, and records the launch interface. No owner, and it holds nothing between transactions.
- Interface registry: Launcher 2.1.0 only. Records, per coin, the interface that earns the interface share. Only that interface can hand its entry on; there is no owner.
- Bonding curve: One per coin. Holds the whole supply at launch and buys and sells against the pair asset until the sellable part is bought out.
- Launch token: One per coin. A fixed-supply, burnable ERC-20 minted once to its curve, with its logo, description and links stored on chain. No owner.
- Pool hook: One per set, attached to every graduated pool of that set. Takes the pool fee and the creator fee on each swap, pays trade referrers and holds pending pool fees. Also the fee policy every curve of the set reads.
- Fee escrow: One per set. The pull-payment ledger every payout is credited to, in ETH or the ERC-20 earned; each recipient claims its own balance. No owner.
- Buyback vault: One per set. Holds bought-back coins and releases them linearly over five years, split between the creator recipient and the protocol.
- Liquidity locker: Holds every graduated pool's position NFT and the reserved tokens the pool did not need. It has no function that releases either.
- Launch deployer: Deploys each curve and coin with CREATE2 for the factory; predictLaunchAddresses returns both addresses before the launch.
- Graduation executor: Mints the full-range Uniswap v4 position straight into the locker when a coin graduates.
- Graduation guard: A stateless check that a graduation seed is one Uniswap v4 will mint. The factory deploys it in its constructor; read graduationGuard() on the factory for its address.
- Holders distributor factory: Creates one holders distributor per holders-mode coin, as a clone of a single implementation whose code cannot change, and names the publisher key.
- Holders distributor: The creator fee recipient of a holders-mode coin. Harvests its escrow balance and pays holders through published Merkle epochs, each claimable for 90 days.

### Launcher 2.1.0 (current)

Deployed from block 52,885,079; start indexing this set there. Each coin of the set also has its own curve and token, created at launch.

Launch factory`0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697`[](https://basescan.org/address/0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697)

Launch router`0x32E6c9C592c9082C6F6A205fc3B38FECdC1be5d5`[](https://basescan.org/address/0x32E6c9C592c9082C6F6A205fc3B38FECdC1be5d5)

Interface registry`0x295F904Eb48862F50086b650b9a56D3B0330F800`[](https://basescan.org/address/0x295F904Eb48862F50086b650b9a56D3B0330F800)

Pool hook`0x9eBcb19b5075E9e87d62141a360bC39c3F92a044`[](https://basescan.org/address/0x9eBcb19b5075E9e87d62141a360bC39c3F92a044)

Fee escrow`0x03132BD55D1c189B4e5AC0895628cAB0Beb72bfB`[](https://basescan.org/address/0x03132BD55D1c189B4e5AC0895628cAB0Beb72bfB)

Buyback vault`0x6d2D3263733329e2ED65D21DCC082a46ab754E92`[](https://basescan.org/address/0x6d2D3263733329e2ED65D21DCC082a46ab754E92)

Liquidity locker`0xE6dBD6f38A883AF15698a771d2772a2728945E70`[](https://basescan.org/address/0xE6dBD6f38A883AF15698a771d2772a2728945E70)

Launch deployer`0xd7b14F37F68e2E468a9aACEad9e16Ab6698DAF2B`[](https://basescan.org/address/0xd7b14F37F68e2E468a9aACEad9e16Ab6698DAF2B)

Graduation executor`0x56eB01751007e982Cc01861829dDDd151E129852`[](https://basescan.org/address/0x56eB01751007e982Cc01861829dDDd151E129852)

Holders distributor factory`0xB7464E790e2A3083FeF8a842d730a49242846ACA`[](https://basescan.org/address/0xB7464E790e2A3083FeF8a842d730a49242846ACA)

Holders distributor implementation`0x7cC41c9bb4c3831CfdE3E72067d1A848A5535068`[](https://basescan.org/address/0x7cC41c9bb4c3831CfdE3E72067d1A848A5535068)

### Launcher 2.0 (coins launched before the cutover)

Deployed from block 52,862,330; start indexing this set there. Each coin of the set also has its own curve and token, created at launch.

Launch factory`0x775199d61734EFde2537443eF4614717Afb6CfC2`[](https://basescan.org/address/0x775199d61734EFde2537443eF4614717Afb6CfC2)

Launch router`0xfa27309d5493B287235a709dE70306969a41291A`[](https://basescan.org/address/0xfa27309d5493B287235a709dE70306969a41291A)

Pool hook`0x14C3c3d571b2E4383a2a67377312764c60Fba044`[](https://basescan.org/address/0x14C3c3d571b2E4383a2a67377312764c60Fba044)

Fee escrow`0xad62A3881aF9291CD94622Ef4BbF4BC41f782CfF`[](https://basescan.org/address/0xad62A3881aF9291CD94622Ef4BbF4BC41f782CfF)

Buyback vault`0x8Cd01789e9DE3C88daf8524497EbD7CA06a6495c`[](https://basescan.org/address/0x8Cd01789e9DE3C88daf8524497EbD7CA06a6495c)

Liquidity locker`0x0E7842CEeD8434ef250fd3014718E33C249a5791`[](https://basescan.org/address/0x0E7842CEeD8434ef250fd3014718E33C249a5791)

Launch deployer`0xD02d9A7C4B755FD33E8dc11d427acD9D0e6738C5`[](https://basescan.org/address/0xD02d9A7C4B755FD33E8dc11d427acD9D0e6738C5)

Graduation executor`0x4a8443A8773d0aFF04edC60347b60DE75788FBd1`[](https://basescan.org/address/0x4a8443A8773d0aFF04edC60347b60DE75788FBd1)

Holders distributor factory`0xe161531B9296029De5f274892a4Dc4D945649224`[](https://basescan.org/address/0xe161531B9296029De5f274892a4Dc4D945649224)

Holders distributor implementation`0xf8049DEcBE32bACD3E239Ba5Ff625FC32366a287`[](https://basescan.org/address/0xf8049DEcBE32bACD3E239Ba5Ff625FC32366a287)

### Uniswap v4 and Permit2 on Base (both sets)

Uniswap v4 PoolManager`0x498581fF718922c3f8e6A244956aF099B2652b2b`[](https://basescan.org/address/0x498581fF718922c3f8e6A244956aF099B2652b2b)

Uniswap v4 PositionManager`0x7C5f5A4bBd8fD63184577525326123B519429bDc`[](https://basescan.org/address/0x7C5f5A4bBd8fD63184577525326123B519429bDc)

Universal Router`0x6fF5693b99212Da76ad316178A184AB56D299b43`[](https://basescan.org/address/0x6fF5693b99212Da76ad316178A184AB56D299b43)

Uniswap v4 Quoter`0x0d5e0F971ED27FBfF6c2837bf31316121532048D`[](https://basescan.org/address/0x0d5e0F971ED27FBfF6c2837bf31316121532048D)

Uniswap v4 StateView`0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71`[](https://basescan.org/address/0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71)

Permit2`0x000000000022D473030F116dDEE9F6B43aC78BA3`[](https://basescan.org/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)

Resolve a coin from the factory that launched it

Never assume a coin lives on the current set. Read `launchFactory()` on the coin, match it to a factory above, and use that set's hook, escrow and vault for everything about the coin. The wrong set does not revert: `getLaunchedToken` returns a record with `exists` false, the wrong escrow reports a zero balance and the wrong hook builds a pool id that does not exist.

## 16. Launching a coin

Launch through the 2.1.0 router. One call creates the coin and its curve, records your interface, and lands the creator's opening buy as the first trade, so nothing can trade between the launch and that buy. The factory's own `launchToken` also works, but it takes no opening buy and records no interface.

Before offering a launch, check `canLaunch(account)` on the factory: it is true while public launches are open, and for whitelisted accounts while they are closed.

### Configs and the economics pin

A launch config fixes the supply, the curve fee, the virtual reserve, the graduation target, the pool fee and the tick spacing. Configs are append-only, but the owner can edit or disable one, so list the enabled ones rather than hard-coding an id. A coin on an ERC-20 pair takes its virtual reserve and target from `pairTokenEconomics(pair)` instead, in that asset's own decimals.

`previewLaunchEconomics(configId, pairToken)` returns a hash of every owner-set term the coin will lock in: virtual reserve, target, supply, curve fee, pool fee, tick spacing and the four fee policy figures. Pass it as `expectedEconomics`; if the owner changes any of them before your launch lands, it reverts with `LaunchEconomicsMismatch` instead of launching on new terms. Zero waives the check.

Reading launch configs, the fee and the pints

```
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

const FACTORY = "0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697"; // launcher 2.1.0

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)",
  "function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)",
  "function launchFee() view returns (uint256)",
  "function canLaunch(address launcher) view returns (bool)",
  "function maxCreatorTaxBps() view returns (uint256)",
  "function approvedPairTokens(address pairToken) view returns (bool)",
  "function pairTokenEconomics(address pairToken) view returns (uint256 phantomQuote, uint256 graduationThreshold, uint8 decimals)",
]);

// Configs are append-only; the owner can edit or disable one, so list the enabled ones each time.
const count = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchConfigCount" });
const enabled = [];
for (let id = 0n; id < count; id++) {
  const config = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchConfig", args: [id] });
  if (config.enabled) enabled.push({ id, ...config });
}

// ETH is the zero address. An ERC-20 pair must be approved and brings its own reserve and target, in its own decimals.
const pairToken = zeroAddress;
const [launchFee, expectedEconomics, maxCreatorTaxBps] = await Promise.all([
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" }),
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "previewLaunchEconomics", args: [enabled[0].id, pairToken] }),
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "maxCreatorTaxBps" }),
]);
```

### The router call

- `params`: Name, symbol, logo, description and links; the creator fee recipient (zero means the launching account); `creatorTaxBps`, the creator fee, at most `maxCreatorTaxBps()` (10% ceiling); `buybackEnabled`; `expectedEconomics`; and a 32-byte `salt`.
- `launchConfigId`: An enabled config id.
- `pairToken`: The zero address for ETH, or an approved ERC-20. Fixed for the coin's life.
- `snipeTaxExemptions`: Up to 32 wallets that pay no launch tax, for a team buying across several wallets. The launching account and the creator fee recipient are exempt already. More than 32 reverts with `ExemptionListTooLong`.
- `buyAmount`: The opening buy, in the pair asset's units. Zero for none.
- `minTokensOut`: The floor for the opening buy. The buy is the first trade on a fresh curve and pays no launch tax, so a simulation gives the exact fill.
- `holders`: Holders mode: a new holders distributor becomes the creator fee recipient for good. Requires a zero recipient and buyback off, or it reverts with `HoldersModeConflict`.
- `interface_`: Your payout wallet. It is registered for this coin only, in this transaction, and earns 20% of the creator side of the coin's fees from then on. Zero for none.
- `referrer`: Paid its share of the opening buy's base fee. Zero for none; recorded in LaunchedWithBuy only when there is an opening buy.

The value is `launchFee() + buyAmount` on an ETH pair. On an ERC-20 pair it is `launchFee()` alone, and the router pulls `buyAmount` of the pair asset, so approve the router for it first. Anything the opening buy does not spend comes back to the caller in the same transaction.

Launching with an opening buy, an interface and a referrerts

```
import { createPublicClient, createWalletClient, custom, http, parseAbi, parseEther, toHex, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();

const FACTORY = "0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697";
const ROUTER = "0x32E6c9C592c9082C6F6A205fc3B38FECdC1be5d5";
const INTERFACE = "0x..."; // your front end's payout wallet: 20% of the creator side of this coin's fees, for good
const REFERRER = "0x...";  // paid 20% of the base fee of the opening buy; zeroAddress for none

const factoryAbi = parseAbi([
  "function launchFee() view returns (uint256)",
  "function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)",
]);
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 launch(TokenParams params, uint256 launchConfigId, address pairToken, address[] snipeTaxExemptions, uint256 buyAmount, uint256 minTokensOut, bool holders, address interface_, address referrer) payable returns (address token, address curve, uint256 tokensOut, address distributor)",
]);

const launchConfigId = 0n;
const pairToken = zeroAddress; // ETH
const buyAmount = parseEther("0.05");
const [launchFee, expectedEconomics] = await Promise.all([
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "launchFee" }),
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "previewLaunchEconomics", args: [launchConfigId, pairToken] }),
]);

const params = {
  name: "Example", symbol: "EXAMPLE", logo: "ipfs://...", description: "One line about the coin.",
  socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
  creatorFeeRecipient: zeroAddress, // zero: the launching account
  creatorTaxBps: 200,               // the creator fee, at most maxCreatorTaxBps()
  buybackEnabled: false,
  expectedEconomics,                // reverts LaunchEconomicsMismatch if the owner moved the terms since the preview
  salt: toHex(crypto.getRandomValues(new Uint8Array(32))),
};
const exemptions = [];              // up to 32 wallets free of the launch tax; the launching account is exempt already
const holders = false;
const value = launchFee + buyAmount; // an ERC-20 pair: launchFee only, after approve(ROUTER, buyAmount) on the pair asset

// The opening buy is the first trade on a fresh curve and the launching account pays no launch tax,
// so a simulation returns the exact fill. Use it as the floor.
const { result } = await client.simulateContract({
  account, address: ROUTER, abi: routerAbi, functionName: "launch", value,
  args: [params, launchConfigId, pairToken, exemptions, buyAmount, 0n, holders, INTERFACE, REFERRER],
});
const hash = await wallet.writeContract({
  account, address: ROUTER, abi: routerAbi, functionName: "launch", value,
  args: [params, launchConfigId, pairToken, exemptions, buyAmount, result[2], holders, INTERFACE, REFERRER],
});
```

### Deterministic addresses

The curve and the coin are deployed with CREATE2. The salt is namespaced by the launching account (keccak256 of the account and your `salt`), so nobody else can take your addresses, and a salt only has to be unused among your own launches. Reusing one on identical terms reverts. Mine salts offline for a vanity address, and confirm the result with `predictLaunchAddresses` on the deployer, which takes every term the constructors take. In holders mode the fee recipient is a distributor created inside the launch, so the addresses are only known once it exists.

Metadata is stored on the coin and capped in UTF-8 bytes: name 64, symbol 16, logo 512, description 2048 and each link 256. One byte over reverts with `MetadataTooLong`.

Predicting the coin and curve addressests

```
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

const FACTORY = "0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697";
const DEPLOYER = "0xd7b14F37F68e2E468a9aACEad9e16Ab6698DAF2B";
const HOOK = "0x9eBcb19b5075E9e87d62141a360bC39c3F92a044";
const ESCROW = "0x03132BD55D1c189B4e5AC0895628cAB0Beb72bfB";
const VAULT = "0x6d2D3263733329e2ED65D21DCC082a46ab754E92";

const factoryAbi = parseAbi([
  "struct LaunchConfig { uint256 supply; uint256 curveFeeBps; uint256 phantomQuote; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; bool enabled; }",
  "function getLaunchConfig(uint256 id) view returns (LaunchConfig)",
]);
const hookAbi = parseAbi([
  "struct FeePolicySnapshot { uint16 protocolFeeShareBps; uint16 buybackBurnBps; uint16 hookFeeBps; uint16 maxInternalPriceImpactBps; }",
  "function currentFeePolicy() view returns (FeePolicySnapshot)",
]);
const deployerAbi = parseAbi([
  "struct FeePolicySnapshot { uint16 protocolFeeShareBps; uint16 buybackBurnBps; uint16 hookFeeBps; uint16 maxInternalPriceImpactBps; }",
  "struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
  "struct LaunchDeployment { address pairToken; address creatorFeeRecipient; address originalDeployer; address feePolicy; FeePolicySnapshot policy; address feeEscrow; address buybackVault; uint256 phantomQuote; uint256 curveFeeBps; uint256 creatorTaxBps; bool buybackEnabled; uint256 graduationThreshold; uint256 supply; bytes32 salt; string name; string symbol; string logo; string description; Socials socials; }",
  "function predictLaunchAddresses(LaunchDeployment params) view returns (address token, address curve)",
]);

const launcher = "0x...";           // the account that sends the launch (the router passes its caller through)
const salt = "0x...";               // the 32-byte salt you will send in TokenParams
const [config, policy] = await Promise.all([
  client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchConfig", args: [0n] }),
  client.readContract({ address: HOOK, abi: hookAbi, functionName: "currentFeePolicy" }),
]);

// ETH pair: reserve and target come from the config; an ERC-20 pair takes them from pairTokenEconomics(pair).
const [token, curve] = await client.readContract({
  address: DEPLOYER, abi: deployerAbi, functionName: "predictLaunchAddresses",
  args: [{
    pairToken: zeroAddress, creatorFeeRecipient: launcher, originalDeployer: launcher,
    feePolicy: HOOK, policy, feeEscrow: ESCROW, buybackVault: VAULT,
    phantomQuote: config.phantomQuote, curveFeeBps: config.curveFeeBps, creatorTaxBps: 200n, buybackEnabled: false,
    graduationThreshold: config.graduationThreshold, supply: config.supply, salt,
    name: "Example", symbol: "EXAMPLE", logo: "ipfs://...", description: "One line about the coin.",
    socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
  }],
});
```

The interface is set once, by the launch

Only the router can register an interface, and only for the coin it just launched. A coin launched without one never gets one, and a coin launched on 2.0 has no registry at all.

## 17. Buying and selling on the curve

While a coin's phase is 0 it trades on its own curve, at the address in `getLaunchedToken(token).curve`. Both directions settle in the coin's pair asset.

- `buy(quoteIn, minTokensOut, recipient)` and `sell(tokensIn, minQuoteOut, recipient)` trade without a referrer and exist on both sets.
- `buy(quoteIn, minTokensOut, recipient, referrer)` and `sell(tokensIn, minQuoteOut, recipient, referrer)` pay the referrer in the same trade. They exist only on 2.1.0 curves; a 2.0 curve has no such function and the call reverts.

On an ETH coin, send `value` equal to `quoteIn`. On an ERC-20 coin, send no value and approve the curve for `quoteIn` first; the curve credits what actually arrived. Selling always needs an approval of the coin to the curve and never takes value. Coins go to `recipient`; a refund goes back to the caller.

Buying and selling on the curvets

```
import { createPublicClient, createWalletClient, custom, http, parseAbi, parseEther, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();

const FACTORY = "0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697"; // the coin's own factory: read token.launchFactory()
const TOKEN = "0x...";
const REFERRER = "0x...";          // or use the 3-argument overloads for no referrer

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 curveAbi = parseAbi([
  "function buy(uint256 quoteIn, uint256 minTokensOut, address recipient) payable returns (uint256 tokensOut)",
  "function buy(uint256 quoteIn, uint256 minTokensOut, address recipient, address referrer) payable returns (uint256 tokensOut)",
  "function sell(uint256 tokensIn, uint256 minQuoteOut, address recipient) returns (uint256 quoteOut)",
  "function sell(uint256 tokensIn, uint256 minQuoteOut, address recipient, address referrer) returns (uint256 quoteOut)",
]);
const erc20Abi = parseAbi(["function approve(address spender, uint256 value) returns (bool)"]);

const { curve, pairToken, phase } = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [TOKEN] });
if (phase !== 0) throw new Error("not on the curve any more: trade the Uniswap v4 pool");
const native = pairToken === zeroAddress;

// Buy. The amount is in the pair asset's own units (ETH: wei, USDC: 6 decimals).
const quoteIn = parseEther("0.05");
if (!native) {
  const hash = await wallet.writeContract({ account, address: pairToken, abi: erc20Abi, functionName: "approve", args: [curve, quoteIn] });
  await client.waitForTransactionReceipt({ hash });
}
const buy = await client.simulateContract({
  account, address: curve, abi: curveAbi, functionName: "buy",
  args: [quoteIn, 0n, account, REFERRER], value: native ? quoteIn : 0n,
});
const minTokensOut = buy.result - buy.result / 100n; // 1% slippage, or quoteBuy from "Getting a quote"
await wallet.writeContract({
  account, address: curve, abi: curveAbi, functionName: "buy",
  args: [quoteIn, minTokensOut, account, REFERRER], value: native ? quoteIn : 0n,
});

// Sell. Approve the curve for the coin; proceeds arrive in the pair asset. Never send value.
const tokensIn = 1_000_000n * 10n ** 18n;
const approval = await wallet.writeContract({ account, address: TOKEN, abi: erc20Abi, functionName: "approve", args: [curve, tokensIn] });
await client.waitForTransactionReceipt({ hash: approval });
const sell = await client.simulateContract({ account, address: curve, abi: curveAbi, functionName: "sell", args: [tokensIn, 0n, account, REFERRER] });
await wallet.writeContract({
  account, address: curve, abi: curveAbi, functionName: "sell",
  args: [tokensIn, sell.result - sell.result / 100n, account, REFERRER],
});
```

### The finishing buy is clamped, not rejected

A buy larger than what is left on the curve is filled up to the last sellable coin, charged only for that, and the rest of the input is refunded in the same transaction, announced by `CurveBuyRefunded`. Read the real fill from the return value or from `CurveBuy`, not from your input. On a clamped buy, `minTokensOut` bounds the price you pay rather than the quantity, so a smaller fill at your price or better still goes through.

The buy that sells out the curve also graduates it in the same transaction. Give it extra gas: if graduation runs out, the buy still succeeds and the curve emits `AutoGraduationFailed`. Anyone can then call `graduate(token)` and `createGraduatedPool(token)` on the factory; both are permissionless and can be retried.

Sells close at sell-out, before the pool exists

Once `readyToGraduate()` is true (no sellable coins left), the curve refuses sells as well as buys with `CurveGraduated`, even before `graduated()` flips. Gate buys on `sellableTokens() == 0` and sells on `readyToGraduate()`, then move to the pool when the phase reaches 2.

## 18. Getting a quote

The curve has no quote function. Reproduce its math client side, in the same integer order, and the quote matches settlement to the wei. The sample below is the curve's own arithmetic ported to BigInt; run as written, it reproduces trades settled by the 2.1.0 curves on a Base fork exactly, including a finishing buy with its clamp and refund. A simulated call (`simulateContract`) gives the same answer without the math, at the cost of a round trip.

Quoting a curve tradets

```
import { createPublicClient, http, parseAbi, parseEther } from "viem";
import { base } from "viem/chains";

const BPS = 10_000n;

// LaunchCurveMath.getAmountOut with a zero fee: the curve charges its fees on the quote leg itself.
function amountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
  if (amountIn === 0n) throw new Error("InsufficientInputAmount");
  const amountInWithFee = amountIn * BPS;
  const out = (amountInWithFee * reserveOut) / (reserveIn * BPS + amountInWithFee);
  if (out === 0n) throw new Error("InsufficientOutputAmount");
  return out;
}

// LaunchCurveMath.getAmountIn with a zero fee.
function amountIn(amountOut: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
  return (amountOut * reserveIn * BPS) / ((reserveOut - amountOut) * BPS) + 1n;
}

const ceilDiv = (a: bigint, b: bigint) => (a + b - 1n) / b;

interface CurveState {
  quoteReserve: bigint;   // getReserves()[0]: virtual plus real quote, unswept fees excluded
  tokenReserve: bigint;   // getReserves()[1]
  sellableTokens: bigint; // sellableTokens(): what is left to buy before graduation
  feeBps: bigint;         // feeBps()
  creatorTaxBps: bigint;  // creatorTaxBps()
}

// Buy: base fee, creator fee and launch tax come off the input, then the rest is priced.
// snipeTaxBps is currentSnipeTaxBps(recipient), keyed on who receives the coins.
function quoteBuy(s: CurveState, quoteIn: bigint, snipeTaxBps = 0n) {
  let snipe = snipeTaxBps;
  if (snipe !== 0n) {
    // The launch tax is capped so the buyer always nets at least 1% of the spend.
    const maxSnipe = BPS - s.feeBps - s.creatorTaxBps - 100n;
    if (snipe > maxSnipe) snipe = maxSnipe;
  }
  let spent = quoteIn;
  let fee = (spent * s.feeBps) / BPS;
  let tax = (spent * s.creatorTaxBps) / BPS;
  let snipeTax = (spent * snipe) / BPS;
  let tokensOut = amountOut(spent - fee - tax - snipeTax, s.quoteReserve, s.tokenReserve);
  if (s.sellableTokens === 0n) throw new Error("CurveGraduated");
  if (tokensOut > s.sellableTokens) {
    // The finishing buy: filled to what is left, charged only for that, the rest refunded.
    tokensOut = s.sellableTokens;
    const net = amountIn(s.sellableTokens, s.quoteReserve, s.tokenReserve);
    const gross = ceilDiv(net * BPS, BPS - s.feeBps - s.creatorTaxBps - snipe);
    spent = gross < quoteIn ? gross : quoteIn;
    fee = (spent * s.feeBps) / BPS;
    tax = (spent * s.creatorTaxBps) / BPS;
    snipeTax = (spent * snipe) / BPS;
  }
  // fee and tax match the CurveBuy event: fee is the base fee plus any launch tax.
  return { tokensOut, spent, refund: quoteIn - spent, fee: fee + snipeTax, tax, graduates: tokensOut === s.sellableTokens };
}

// Sell: priced first, then both fees come off the output. Sells pay no launch tax and close at sell-out.
function quoteSell(s: CurveState, tokensIn: bigint) {
  if (s.sellableTokens === 0n) throw new Error("CurveGraduated");
  const gross = amountOut(tokensIn, s.tokenReserve, s.quoteReserve);
  const fee = (gross * s.feeBps) / BPS;
  const tax = (gross * s.creatorTaxBps) / BPS;
  return { quoteOut: gross - fee - tax, fee, tax };
}

// Reading the inputs from the curve
const client = createPublicClient({ chain: base, transport: http() });
const CURVE = "0x...";     // getLaunchedToken(token).curve
const RECIPIENT = "0x..."; // the address that will receive the coins

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 read = (functionName, args = []) => client.readContract({ address: CURVE, abi: curveAbi, functionName, args });
const [[quoteReserve, tokenReserve], sellableTokens, feeBps, creatorTaxBps, snipeTaxBps] = await Promise.all([
  read("getReserves"), read("sellableTokens"), read("feeBps"), read("creatorTaxBps"), read("currentSnipeTaxBps", [RECIPIENT]),
]);
const state = { quoteReserve, tokenReserve, sellableTokens, feeBps, creatorTaxBps };

const buy = quoteBuy(state, parseEther("0.05"), snipeTaxBps);
const minTokensOut = buy.tokensOut - buy.tokensOut / 100n; // 1% slippage
// buy.graduates: this buy finishes the curve and graduates in the same transaction; give it more gas.
```

### Which reserves to price against

Use `getReserves()`. Its quote reserve is the virtual reserve plus the real one, with fees that have not been swept left out; its token reserve is the coins the curve holds for trading. Do not use the curve's token or ETH balance, which include unswept fees and anything sent to it by mistake, and do not price against `realQuoteReserve()`, which leaves out the virtual part. The spot price `quoteReserve / tokenReserve` is for display only; every trade moves it.

### Buys and sells are not mirror images

A buy takes the base fee, the creator fee and any launch tax off the input and prices the rest. A sell prices the whole input and takes the two fees off the output. Sells pay no launch tax. A round trip pays both fees on the way in and again on the way out.

The launch tax is keyed on the recipient of the coins, not on the sender: read `currentSnipeTaxBps(recipient)`, which returns 0 for exempt wallets and after the window. It is capped per buy at `10000 - feeBps - creatorTaxBps - 100` basis points, so a buyer always nets at least 1% of the spend.

### When to stop quoting

When `sellableTokens()` is 0 the curve takes no more trades in either direction. Quote the pool once the phase is 2 (see [Uniswap v4 pools](#uniswap-v4)). A buy whose quote returns `graduates: true` sells out the curve and graduates it in the same transaction.

## 19. Reading state

The factory keeps one record per coin, `getLaunchedToken(token)`: its curve, creator, creator fee recipient, pair asset, graduation target, pool fee and tick spacing, creator fee, buyback setting and graduation phase. Read it on the coin's own factory, which the coin names in `launchFactory()`.

Reading a coin's record, reserves, price and progressts

```
import { createPublicClient, http, parseAbi } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

const TOKEN = "0x...";

const tokenAbi = parseAbi(["function launchFactory() view returns (address)"]);
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; }",
  "struct FeePolicySnapshot { uint16 protocolFeeShareBps; uint16 buybackBurnBps; uint16 hookFeeBps; uint16 maxInternalPriceImpactBps; }",
  "function getLaunchedToken(address token) view returns (LaunchedToken)",
  "function getLaunchFeePolicy(address token) view returns (FeePolicySnapshot)",
  "function pendingCreatorFeeRecipient(address token) view returns (address newRecipient, uint256 effectiveAt, uint256 expiresAt)",
]);
const curveAbi = parseAbi([
  "function getReserves() view returns (uint256 quoteReserve_, uint256 tokenReserve_)",
  "function realQuoteReserve() view returns (uint256)",
  "function sellableTokens() view returns (uint256)",
  "function reservedTokens() view returns (uint256)",
  "function launchSupply() view returns (uint256)",
  "function phantomQuote() view returns (uint256)",
  "function graduationThreshold() view returns (uint256)",
  "function readyToGraduate() view returns (bool)",
  "function feeBps() view returns (uint256)",
  "function creatorTaxBps() view returns (uint256)",
  "function buybackEnabled() view returns (bool)",
  "function protocolFeeBalance() view returns (uint256)",
  "function creatorFeeBalance() view returns (uint256)",
]);

// The coin names the factory that launched it; that factory's set is the coin's set for life.
const factory = await client.readContract({ address: TOKEN, abi: tokenAbi, functionName: "launchFactory" });
const launch = await client.readContract({ address: factory, abi: factoryAbi, functionName: "getLaunchedToken", args: [TOKEN] });
// launch.phase: 0 NotGraduated (curve), 1 Swept (moving to the pool), 2 PoolCreated (pool), 3 Rescued

const curve = (functionName) => client.readContract({ address: launch.curve, abi: curveAbi, functionName });
const [[quoteReserve, tokenReserve], realQuote, sellable, threshold] = await Promise.all([
  curve("getReserves"), curve("realQuoteReserve"), curve("sellableTokens"), curve("graduationThreshold"),
]);

// Price in pair-asset units per coin unit (both 18-decimal coins; scale by the pair's decimals for display).
const price = Number(quoteReserve) / Number(tokenReserve);
// Progress to graduation: real reserve against the target. Graduation itself happens when sellable reaches 0.
const progress = sellable === 0n ? 1 : Math.min(1, Number(realQuote) / Number(threshold));

const [policy, pending] = await Promise.all([
  client.readContract({ address: factory, abi: factoryAbi, functionName: "getLaunchFeePolicy", args: [TOKEN] }),
  client.readContract({ address: factory, abi: factoryAbi, functionName: "pendingCreatorFeeRecipient", args: [TOKEN] }),
]);
// pending.newRecipient is zero when no override is waiting.
```

### Phases

- 0 NotGraduated: Trading on the curve.
- 1 Swept: The curve sold out and handed its reserves to the factory; trading is paused until `createGraduatedPool(token)` runs, which anyone may call.
- 2 PoolCreated: The Uniswap v4 pool exists with its liquidity locked. Route every trade there.
- 3 Rescued: The pool could never be seeded and the owner released the reserves after the delay. Show it explicitly; it has no pool.

The phase is the authority. Do not infer it from balances or from the last event you saw.

### Curve reserves

- `getReserves()`: Quote reserve (virtual plus real, unswept fees excluded) and token reserve: what trades price against.
- `realQuoteReserve()`: Pair asset actually collected from trading, without the virtual part or unswept fees.
- `phantomQuote()`: The virtual reserve the curve started with. It sets the opening price and can never be withdrawn.
- `sellableTokens()`: Coins left to buy before graduation. Zero means sold out.
- `reservedTokens()`: Coins held back for the pool, fixed at launch.
- `launchSupply()`: The supply the coin was launched with. The coin's own totalSupply() falls as holders burn.
- `graduationThreshold()`: The real reserve the curve reaches at sell-out.

Reserved for the pool `supply × phantomQuote ÷ (phantomQuote + graduationThreshold)`

Spot price `quoteReserve ÷ tokenReserve`

Graduation progress `realQuoteReserve ÷ graduationThreshold`

Because the reserved amount is derived from the target, the curve reaches the target exactly when its sellable coins run out, so every coin on the same terms graduates into the same pool size at the same price. Progress is complete when `sellableTokens()` is 0.

### Fee rates and policy

`feeBps()` and `creatorTaxBps()` on the curve are fixed for the coin's life; their sum is what a curve trade costs. `getLaunchFeePolicy(token)` on the factory returns the policy frozen at launch: the protocol share of the base fee, the buyback share, the pool fee in basis points and the price impact cap for internal swaps. Only the protocol's payout address is read live, from `protocolFeeRecipient()` on the hook.

### Metadata and the buyback vest

The coin stores its logo, description and five links on chain; `getTokenInfo()` returns them with the creator in one call. Bought-back coins sit in the set's buyback vault and vest linearly over `VESTING_DURATION()`; `releasable(token)` is what a release would pay now. New buybacks move a weighted vesting start rather than restarting the clock, so read `vestedAmount` instead of recomputing it from `vestingStart`. `release(token)` may be sent by the creator recipient or the protocol recipient and pays both, into the escrow.

### A pending fee recipient change

`pendingCreatorFeeRecipient(token)` returns the address the owner proposed, when it becomes executable and when the proposal expires; all zero when nothing is pending. It takes effect only after a 3-day notice, anyone may execute it during the following 3 days, and a transfer by the creator in the meantime does not cancel it. Show it to holders while it waits.

## 20. Integrator shares

Two shares pay the front ends and apps that bring launches and trades. Neither changes what a trader pays.

- **Launch interface:** 20% of the creator side of a coin's fees (the creator part of the base fee plus the creator fee), for the coin's whole life, curve and pool. Set once by the router launch, in the interface registry. Paid at each fee sweep, in the pair asset.
- **Trade referrer:** 0.2% of the volume of each trade that names it (20% of the 1% base fee), paid out of the protocol part, never the creator part. Credited in the same trade. Never paid from the launch tax or the creator fee.

The referrer rate is the lower of 20% and the coin's frozen protocol share, and the owner cannot set the protocol share for new launches below 20%, so the published rate holds. Both shares exist only on launcher 2.1.0.

### One $100 trade, 2% creator fee, with both

| Part | Amount | Paid to |
| --- | --- | --- |
| Base fee | $1.00 | 1% of the trade |
| Trade referrer | $0.20 | Referrer, credited in the trade |
| Protocol | $0.10 | Protocol recipient, at sweep |
| Creator part of the base fee | $0.70 | Creator side |
| Creator fee | $2.00 | Creator side |
| Launch interface | $0.54 | 20% of the $2.70 creator side, at sweep |
| Creator recipient | $2.16 | The rest of the creator side, at sweep (less any buyback) |

The amounts are integer basis points of the trade, rounded down, exactly as below. On the curve they are in the pair asset. In the pool the hook takes its fee from the side of the swap you did not fix; on an exact-input buy that is the coin, so a pool referrer can be paid in the coin. The interface share is always paid in the pair asset, after the sweep converts coin-denominated fees.

Integrator shares: amounts, the registry and hookDatats

```
import { createPublicClient, encodeAbiParameters, http, parseAbi } from "viem";
import { base } from "viem/chains";

const BPS = 10_000n;
const REFERRER_BPS = 2_000n;  // LaunchFeeShares.REFERRER_BPS: of the base fee, capped at the protocol share
const INTERFACE_BPS = 2_000n; // LaunchFeeShares.INTERFACE_BPS: of the creator side, at sweep

// One trade's base fee. The referrer is paid out of the protocol part; the creator part never changes.
function splitBaseFee(baseFee: bigint, protocolFeeShareBps: bigint, hasReferrer: boolean) {
  const creator = baseFee - (baseFee * protocolFeeShareBps) / BPS;
  const protocolPart = baseFee - creator;
  let referrer = 0n;
  if (hasReferrer) {
    const bps = protocolFeeShareBps < REFERRER_BPS ? protocolFeeShareBps : REFERRER_BPS;
    referrer = (baseFee * bps) / BPS;
    if (referrer > protocolPart) referrer = protocolPart;
  }
  return { referrer, protocol: protocolPart - referrer, creator };
}

// At each sweep the interface takes 20% of the creator side: the creator part of the base fee plus the creator fee.
function splitCreatorSide(creatorSide: bigint, hasInterface: boolean) {
  const toInterface = hasInterface ? (creatorSide * INTERFACE_BPS) / BPS : 0n;
  return { toInterface, toCreator: creatorSide - toInterface };
}

// Reading and moving an interface
const client = createPublicClient({ chain: base, transport: http() });
const REGISTRY = "0x295F904Eb48862F50086b650b9a56D3B0330F800"; // launcher 2.1.0 only; coins launched on 2.0 have no interface
const TOKEN = "0x...";
const REFERRER = "0x...";

const registryAbi = parseAbi([
  "function interfaceOf(address token) view returns (address)",
  "function transferInterface(address token, address newRecipient)",
  "event InterfaceSet(address indexed token, address indexed previous, address indexed current)",
]);
const current = await client.readContract({ address: REGISTRY, abi: registryAbi, functionName: "interfaceOf", args: [TOKEN] });
// Only the current interface may hand the entry on: wallet.writeContract({ ..., functionName: "transferInterface", args: [TOKEN, next] })

// Curve trades: pass the referrer as the 4th argument of buy or sell.
// Pool swaps: put it in the swap's hookData as exactly one 32-byte word.
const hookData = encodeAbiParameters([{ type: "address" }], [REFERRER]);
```

### Naming a referrer

On the curve, call the 4-argument `buy` or `sell` with the referrer last. In the pool, put `encodeAbiParameters([{ type: "address" }], [referrer])` in the swap's `hookData`: exactly one 32-byte word. Any other length, a word with bits above the address, the zero address, the hook or the pool manager means no referrer, and the swap still goes through. The Uniswap `Swap` event does not show the referrer; `HookFeeCollected` in the same transaction does.

A contract recipient must be able to claim

Shares are credited to the fee escrow and the recipient pulls them with `claim()` or `claimToken(token)`. A contract used as interface or referrer must be able to make those calls, and to receive ETH: `claim()` sends ETH to the caller, so a contract without a `receive()` function can never collect its ETH, and nobody can move it for you.

On an ERC-20 pair, retry a referred pool swap once without hookData

The hook credits the referrer inside the swap, and it requires the escrow to receive exactly the amount sent. If a pair asset delivers short (a fee on transfer, or a transfer the asset refuses), the hook reverts with `InexactQuoteTransfer` and the whole swap fails. Retry once with empty `hookData`: without a referrer nothing is paid out during the swap.

## 21. Claiming fees

Every payout goes to a fee escrow as a balance the recipient pulls. A recipient that cannot receive never blocks a trade, a sweep or a graduation; its credit waits. Each set has its own escrow, and each escrow keeps one ETH balance and one balance per ERC-20 for every recipient. A creator with coins on both sets, or an interface paid in several pair assets, has several balances to claim.

- `balanceOf(recipient)` and `claim()` for ETH.
- `balanceOfToken(recipient, token)` and `claimToken(token)` for each ERC-20: pair assets, and coins paid by buyback releases or pool referrer credits.

The escrow has no owner and no rescue path: a balance can only leave to the address it was credited to. Changing a coin's creator fee recipient moves future payouts, not balances already credited.

Claiming from both escrowsts

```
import { createPublicClient, createWalletClient, custom, http, parseAbi, parseEther, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();

const ESCROWS = [
  "0x03132BD55D1c189B4e5AC0895628cAB0Beb72bfB", // launcher 2.1.0
  "0xad62A3881aF9291CD94622Ef4BbF4BC41f782CfF", // launcher 2.0
];
const ASSETS = ["0x..."]; // every ERC-20 you may be owed: pair assets, and coins (buyback releases, pool referrer payouts)

const escrowAbi = parseAbi([
  "function balanceOf(address recipient) view returns (uint256)",
  "function balanceOfToken(address recipient, address token) view returns (uint256)",
  "function claim() returns (uint256)",
  "function claimToken(address token) returns (uint256)",
  "event Credited(address indexed recipient, address indexed token, uint256 amount)",
]);

for (const escrow of ESCROWS) {
  // ETH: one native balance per escrow.
  const eth = await client.readContract({ address: escrow, abi: escrowAbi, functionName: "balanceOf", args: [account] });
  if (eth > 0n) await wallet.writeContract({ account, address: escrow, abi: escrowAbi, functionName: "claim" });

  // ERC-20: one balance per asset per escrow.
  for (const asset of ASSETS) {
    const owed = await client.readContract({ address: escrow, abi: escrowAbi, functionName: "balanceOfToken", args: [account, asset] });
    if (owed > 0n) await wallet.writeContract({ account, address: escrow, abi: escrowAbi, functionName: "claimToken", args: [asset] });
  }
}

// To find every asset ever credited to you, read Credited logs with your address as the recipient topic.
const credited = await client.getLogs({
  address: ESCROWS, event: escrowAbi[4], args: { recipient: account }, fromBlock: 52862330n,
});
```

### Fees reach the escrow at a sweep

A trade referrer is credited in the trade. Everything else waits on the curve or the hook until a sweep splits it: protocol part to the protocol recipient, then the creator side, of which the launch interface takes 20% before any buyback, and the creator recipient the rest.

- `sweepFees(minBuybackTokensOut)` on the curve. Graduation sweeps the curve too.
- `sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut)` on the hook, which first converts fees that landed in the coin.

The sweep operator may always sweep. The creator recipient may sweep when no buyback or conversion is pending; otherwise the call reverts with `InternalSwapRequiresOperator`, because only the operator chooses minimum outputs for swaps. To show what is owed before a sweep, add the pending amounts to the escrow balance.

Reading fees that are not swept yetts

```
import { createPublicClient, http, parseAbi, zeroAddress } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

const HOOK = "0x9eBcb19b5075E9e87d62141a360bC39c3F92a044"; // the hook of the coin's set
const CURVE = "0x...";
const POOL_ID = "0x...";    // keccak256(abi.encode(poolKey)), see "Uniswap v4 pools"
const PAIR = zeroAddress;   // the coin's pair asset (zero for ETH)
const TOKEN = "0x...";

const curveAbi = parseAbi([
  "function protocolFeeBalance() view returns (uint256)",
  "function creatorFeeBalance() view returns (uint256)",
  "function buybackQuoteBalance() view returns (uint256)",
  "function sweepFees(uint256 minBuybackTokensOut)",
]);
const hookAbi = parseAbi([
  "function pendingProtocol(bytes32 poolId, address currency) view returns (uint256)",
  "function pendingFees(bytes32 poolId, address currency) view returns (uint256)",
  "function pendingCreatorTax(bytes32 poolId, address currency) view returns (uint256)",
  "function pendingBuyback(bytes32 poolId, address currency) view returns (uint256)",
  "function sweepPoolFees(bytes32 poolId, uint256 minConversionQuoteOut, uint256 minBuybackTokensOut)",
]);

// On the curve: the creator side (creator part of the base fee plus creator fee) waiting for the next sweep.
const creatorSide = await client.readContract({ address: CURVE, abi: curveAbi, functionName: "creatorFeeBalance" });

// In the pool: per currency, because a fee can land in the pair asset or in the coin (converted at sweep).
const poolCreatorSide = async (currency) => {
  const [fees, tax] = await Promise.all([
    client.readContract({ address: HOOK, abi: hookAbi, functionName: "pendingFees", args: [POOL_ID, currency] }),
    client.readContract({ address: HOOK, abi: hookAbi, functionName: "pendingCreatorTax", args: [POOL_ID, currency] }),
  ]);
  return fees + tax;
};
const [inPair, inCoin] = await Promise.all([poolCreatorSide(PAIR), poolCreatorSide(TOKEN)]);
// A coin with an interface pays it 20% of these amounts at the sweep; the rest goes to the creator recipient.
```

### Holders mode

In holders mode the creator recipient is a distributor contract. Anyone may call `harvest()` to pull its escrow balance; the publisher key then posts reward epochs, each a Merkle root over (account, amount) leaves with a URI to the full table. Holders claim with `claim(epoch, account, amount, proof)`, taking amount and proof from the epoch's table, within 90 days of publication (anyone may submit a claim; it always pays the account in the leaf); after that, anyone may call `expire(epoch)` to return the unclaimed rest to the next epochs.

One escrow per set

A balance credited by a 2.0 coin sits in the 2.0 escrow and a balance from a 2.1.0 coin in the 2.1.0 escrow. Neither moves to the other; claim from each.

## 22. Events to index

Index both factories from their deployment blocks, then follow each coin's curve and, after graduation, its pool id on the set's hook. The signatures below are the 2.1.0 ones, with the topic0 each produces.

Core events with their topic0ts

```
import { parseAbi } from "viem";

export const launcherEvents = parseAbi([
  // BaseLaunchFactory
  // topic0 0x8d4aad4953d0ca700d468f3753aa14432d1b35b43ec6409f051fb6aa43a89607
  "event TokenLaunched(address indexed token, address indexed curve, address indexed deployer, address pairToken, uint256 launchConfigId, uint256 graduationThreshold)",
  // topic0 0xcdb72f157fd3666758a6ce201387ffb52038c7562e4fff352828da1096c4b6b4
  "event LaunchSwept(address indexed token, uint256 quoteOut, uint256 tokenOut)",
  // topic0 0x0a44ef75df69c534f43cd6c1aa3ef8983065fe5fe79ef9e79f6494e6f258c259
  "event PoolGraduated(address indexed token, uint256 positionId, uint256 tokenAmount, uint256 pairTokenAmount)",

  // BaseLaunchRouter
  // topic0 0xebd4e7a7e653be8f420d90a4e85c32c0b35671fb1469006490e1203db211f2c6
  "event LaunchedWithBuy(address indexed token, address indexed curve, address indexed creator, address pairToken, uint256 buyAmount, uint256 tokensOut, address distributor, address interface_, address referrer)",

  // BaseLaunchInterfaceRegistry
  // topic0 0x0e27c1124491cea3950586d93f3b76330f61a9acf13af6f4d33f8407428ad0da
  "event InterfaceSet(address indexed token, address indexed previous, address indexed current)",

  // BaseLaunchBondingCurve (emitted by each coin's curve)
  // topic0 0x9ee9e99e75b0adbf369b55047505ace25678f4f41eed4d2122c350c657395ef1
  "event CurveBuy(address indexed buyer, address indexed recipient, uint256 quoteIn, uint256 tokensOut, uint256 fee, uint256 tax, address indexed referrer, uint256 referrerAmount)",
  // topic0 0xffed2de01e98e02aa5962c3a7fe57426258869ae0f56fde79ddd9e5a1f1f54de
  "event CurveSell(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 quoteOut, uint256 fee, uint256 tax, address indexed referrer, uint256 referrerAmount)",
  // topic0 0xa69e8258ccc7b9bbb70ab953fc2d1062b4ee28b8ca827534097e1732e87b0262
  "event CurveBuyRefunded(address indexed buyer, uint256 refund)",
  // topic0 0xe2cd2f31ebc05ec28640102987f4c8fc5f20e269e1b3aa82577f3f2f0e35c7c6
  "event AutoGraduationFailed(address indexed token, uint256 gasRemaining)",

  // BaseLaunchHook
  // topic0 0xb4e7942917b25be15f017ad4e591b521ad2a5098e6ad869e775763bac1ce8b43
  "event HookFeeCollected(bytes32 indexed poolId, address currency, uint256 feeAmount, uint256 taxAmount, address indexed referrer, uint256 referrerAmount)",

  // BaseLaunchFeeEscrow
  // topic0 0x4e45da441832cf53bdaa69235704fc0575e68210f459ee1562911024b12967d5
  "event Credited(address indexed recipient, address indexed token, uint256 amount)",
  // topic0 0xf7a40077ff7a04c7e61f6f26fb13774259ddf1b6bce9ecf26a8276cdd3992683
  "event Claimed(address indexed recipient, address indexed token, uint256 amount)",
]);
```

In `CurveBuy` and `CurveSell` the referrer is the third indexed topic, and `fee` includes any launch tax, which is also reported on its own by `SnipeTaxCharged`. Payouts to referrers and interfaces arrive as ordinary `Credited` events in the escrow; tie them to their cause through the `referrerAmount` fields and the `interfaceAmount` of the sweep events.

Backfilling launches from both factoriests

```
import { createPublicClient, http, parseAbi } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() });

const SETS = [
  { version: "2.1.0", factory: "0x5C154D821C565eeD0Ee575e5aA9F2fB9Ad736697", fromBlock: 52885079n },
  { version: "2.0", factory: "0x775199d61734EFde2537443eF4614717Afb6CfC2", fromBlock: 52862330n },
];
const CHUNK = 2_000n; // keep each eth_getLogs range small; wide ranges are refused by public endpoints

const factoryEvents = parseAbi([
  "event TokenLaunched(address indexed token, address indexed curve, address indexed deployer, address pairToken, uint256 launchConfigId, uint256 graduationThreshold)",
]);

const head = await client.getBlockNumber();
const coins = [];
for (const set of SETS) {
  for (let from = set.fromBlock; from <= head; from += CHUNK) {
    const to = from + CHUNK - 1n < head ? from + CHUNK - 1n : head;
    const logs = await client.getLogs({ address: set.factory, events: factoryEvents, fromBlock: from, toBlock: to });
    // Store each coin with its set: its curve, hook, escrow and vault all belong to that set.
    for (const log of logs) coins.push({ set: set.version, ...log.args });
  }
}
// Then follow each curve (CurveBuy, CurveSell, ...) and, after PoolGraduated, its pool id on the set's hook.
```

Coins on launcher 2.0 emit the 2.0 shapes

On the 2.0 set, `CurveBuy`, `CurveSell`, `FeesSwept`, `FeesRescued`, `HookFeeCollected`, `PoolFeesSwept`, `PoolFeesRescued` and `LaunchedWithBuy` have no referrer or interface fields, so their topic0 values differ. `TokenLaunched`, `Credited` and `Claimed` are the same on both sets. Decode each coin's logs with the ABI of its set.

| Event | Emitted by | Why it matters |
| --- | --- | --- |
| `TokenLaunched` | `BaseLaunchFactory` | One per coin: coin, curve, creator, pair asset, config and graduation target. Every other address of the coin follows from it. |
| `InterfaceSet` | `BaseLaunchInterfaceRegistry` | An interface registered at launch (previous is zero) or handed on. The latest one is the coin's current interface. |
| `CurveBuy`, `CurveSell` | `BaseLaunchBondingCurve` | Every curve trade. fee includes any launch tax; referrerAmount was already credited to referrer. |
| `CurveBuyRefunded` | `BaseLaunchBondingCurve` | A finishing buy clamped at sell-out: the part of the input sent back. |
| `CurveCompleted` | `BaseLaunchBondingCurve` | The curve's reserves handed to the factory at graduation. |
| `FeesSwept`, `FeesRescued` | `BaseLaunchBondingCurve` | Curve fees paid out to protocol, buyback, creator and interface, by a sweep or the owner's rescue. |
| `LaunchSwept`, `PoolGraduated` | `BaseLaunchFactory` | Graduation in two steps: reserves moved and curve closed, then the pool created with locked liquidity. |
| `PoolRegistered` | `BaseLaunchHook` | A pool registered at graduation: pool id, coin, pair asset and creator recipient. |
| `HookFeeCollected` | `BaseLaunchHook` | Every pool swap's fee and creator fee, with the referrer and its amount. Join to the Uniswap Swap event by transaction. |
| `PoolFeesSwept`, `PoolFeesRescued` | `BaseLaunchHook` | Pool fees paid out, by a sweep or the owner's rescue. |
| `Credited`, `Claimed` | `BaseLaunchFeeEscrow` | Every payout and every claim, per asset (token zero is ETH). Index both escrows and every asset. |

Every other event, contract by contract: [BaseLaunchFactory](https://baselaunch.app/docs/reference/factory), [BaseLaunchRouter](https://baselaunch.app/docs/reference/router), [BaseLaunchInterfaceRegistry](https://baselaunch.app/docs/reference/registry), [BaseLaunchBondingCurve](https://baselaunch.app/docs/reference/curve), [BaseLaunchToken](https://baselaunch.app/docs/reference/token), [BaseLaunchHook](https://baselaunch.app/docs/reference/hook), [BaseLaunchFeeEscrow](https://baselaunch.app/docs/reference/escrow), [BaseLaunchBuybackVault](https://baselaunch.app/docs/reference/vault), [BaseLaunchLocker](https://baselaunch.app/docs/reference/locker), [BaseLaunchDeployer](https://baselaunch.app/docs/reference/deployer), [BaseLaunchGraduationExecutor](https://baselaunch.app/docs/reference/executor), [BaseLaunchGraduationGuard](https://baselaunch.app/docs/reference/guard), [BaseLaunchHoldersDistributorFactory](https://baselaunch.app/docs/reference/distributor-factory), [BaseLaunchHoldersDistributor](https://baselaunch.app/docs/reference/distributor).

## 23. Uniswap v4 pools

A graduated coin trades in an ordinary Uniswap v4 pool. Its liquidity is one full-range position whose NFT sits in the set's locker for good. Any v4 router, aggregator or wallet can swap it without knowing about baselaunch; the pool key is all it needs.

### Rebuilding the pool

The key is the two currencies sorted by address (ETH, the zero address, is always `currency0`), the record's `poolFee` (0) and `tickSpacing`, and the hook of the coin's set. The pool id is `keccak256(abi.encode(key))`. The same coin address with the other set's hook is a different pool that does not exist, so take the hook from the set the coin was launched on.

Rebuilding the pool key and pool idts

```
import { createPublicClient, encodeAbiParameters, http, keccak256, parseAbi, zeroAddress } from "viem";
import { base } from "viem/chains";

// From getLaunchedToken(token): token, pairToken, poolFee (0) and tickSpacing. hook: the hook of the coin's set.
function poolKeyFor(launch: { token: string; pairToken: string; poolFee: number; tickSpacing: number }, hook: string) {
  // v4 sorts currencies by address; ETH (zero) is always currency0.
  const pairFirst = BigInt(launch.pairToken) < BigInt(launch.token);
  return {
    currency0: pairFirst ? launch.pairToken : launch.token,
    currency1: pairFirst ? launch.token : launch.pairToken,
    fee: launch.poolFee,
    tickSpacing: launch.tickSpacing,
    hooks: hook,
  };
}

function poolIdOf(key: ReturnType<typeof poolKeyFor>) {
  return keccak256(encodeAbiParameters(
    [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }],
    [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks],
  ));
}

// Reading the pool and its pending fees
const client = createPublicClient({ chain: base, transport: http() });
const HOOK = "0x9eBcb19b5075E9e87d62141a360bC39c3F92a044";
const STATE_VIEW = "0xA3c0c9b65baD0b08107Aa264b0f3dB444b867A71";
const key = poolKeyFor({ token: "0x...", pairToken: zeroAddress, poolFee: 0, tickSpacing: 200 }, HOOK);
const poolId = poolIdOf(key);

const hookAbi = parseAbi([
  "function launches(bytes32 poolId) view returns (bool registered, bool memecoinIsCurrency0, address memecoin, address quoteToken, address creator, address buybackCreatorRecipient, uint16 creatorTaxBps, uint16 protocolFeeShareBps, uint16 buybackBurnBps, uint16 hookFeeBps, uint16 maxInternalPriceImpactBps, bool buybackEnabled)",
]);
const stateViewAbi = parseAbi([
  "function getSlot0(bytes32 poolId) view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)",
]);
const [registered, , , , , , creatorTaxBps, , , hookFeeBps] = await client.readContract({ address: HOOK, abi: hookAbi, functionName: "launches", args: [poolId] });
const [sqrtPriceX96] = await client.readContract({ address: STATE_VIEW, abi: stateViewAbi, functionName: "getSlot0", args: [poolId] });
```

### What the hook does

- Before initialization: only the factory may create a pool with this hook, so every such pool is a registered baselaunch pool.
- After each swap: it takes the pool fee and the coin's creator fee from the side of the swap you did not fix (on an exact-input swap, the output), books them as pending, and pays a referrer named in `hookData` on the spot. That is why the LP fee in the key is 0: the fee is charged once, by the hook, and split under the coin's frozen policy.
- No other callback is active. The hook never blocks a swap and does not restrict who may trade or move the coin.

Quote pool trades with the v4 Quoter, which runs the swap including the hook's cut, rather than from the pool price alone. `launches(poolId)` on the hook returns the coin, its pair, its creator recipient and its frozen fee terms.

Swapping on the pool with a referrerts

```
import { createPublicClient, createWalletClient, custom, encodeAbiParameters, http, parseAbi, parseEther, zeroAddress } from "viem";
import { base } from "viem/chains";

const POOL_KEY = [
  { name: "currency0", type: "address" }, { name: "currency1", type: "address" },
  { name: "fee", type: "uint24" }, { name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" },
] as const;
// The ExactInputSingleParams layout the Universal Router on Base takes (no minHopPriceX36 field).
const EXACT_IN_SINGLE = [{
  type: "tuple",
  components: [
    { name: "poolKey", type: "tuple", components: POOL_KEY },
    { name: "zeroForOne", type: "bool" },
    { name: "amountIn", type: "uint128" },
    { name: "amountOutMinimum", type: "uint128" },
    { name: "hookData", type: "bytes" },
  ],
}] as const;
const CURRENCY_AMOUNT = [{ type: "address" }, { type: "uint256" }] as const;

// One V4_SWAP command (0x10): SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL (actions 0x06 0x0c 0x0f).
function v4ExactInSingle(a: { key: any; zeroForOne: boolean; amountIn: bigint; minOut: bigint; referrer?: string }) {
  const hookData = a.referrer ? encodeAbiParameters([{ type: "address" }], [a.referrer]) : "0x";
  const swap = encodeAbiParameters(EXACT_IN_SINGLE, [{
    poolKey: a.key, zeroForOne: a.zeroForOne, amountIn: a.amountIn, amountOutMinimum: a.minOut, hookData,
  }]);
  const [currencyIn, currencyOut] = a.zeroForOne ? [a.key.currency0, a.key.currency1] : [a.key.currency1, a.key.currency0];
  const settle = encodeAbiParameters(CURRENCY_AMOUNT, [currencyIn, a.amountIn]);
  const take = encodeAbiParameters(CURRENCY_AMOUNT, [currencyOut, a.minOut]);
  const input = encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], ["0x060c0f", [swap, settle, take]]);
  return { commands: "0x10", inputs: [input] };
}

// Sending the swap
const client = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();
const UNIVERSAL_ROUTER = "0x6fF5693b99212Da76ad316178A184AB56D299b43";
const QUOTER = "0x0d5e0F971ED27FBfF6c2837bf31316121532048D";
const REFERRER = "0x...";
const key = { currency0: zeroAddress, currency1: "0x...", fee: 0, tickSpacing: 200, hooks: "0x9eBcb19b5075E9e87d62141a360bC39c3F92a044" };

const quoterAbi = parseAbi([
  "struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }",
  "struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; }",
  "function quoteExactInputSingle(QuoteExactSingleParams params) returns (uint256 amountOut, uint256 gasEstimate)",
]);
const routerAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]);

// Buy with ETH: currency0 in, the coin out. The hook takes its fee and the creator fee from the output.
const amountIn = parseEther("0.05");
const { result: [quoted] } = await client.simulateContract({
  address: QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle",
  args: [{ poolKey: key, zeroForOne: true, exactAmount: amountIn, hookData: "0x" }],
});
const { commands, inputs } = v4ExactInSingle({ key, zeroForOne: true, amountIn, minOut: quoted - quoted / 100n, referrer: REFERRER });
const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
await wallet.writeContract({ account, address: UNIVERSAL_ROUTER, abi: routerAbi, functionName: "execute", args: [commands, inputs, deadline], value: amountIn });
// Paying in an ERC-20 (a sell, or a buy on an ERC-20 pair): approve Permit2 for the input, then
// Permit2.approve(input, UNIVERSAL_ROUTER, amount, expiration), and send no value.
```

The Universal Router on Base takes the older swap layout

Its `ExactInputSingleParams` has no `minHopPriceX36` field, so encode the swap as in the sample: pool key, direction, amount in, minimum out, hookData. Selling, or buying with an ERC-20 pair, pays through Permit2: approve Permit2 for the input token once, then grant the router an allowance in Permit2.

## 24. Public API

The public API reads what happens on baselaunch and builds the transactions to act on it. It is free, keyless and open to browsers, returns only public on-chain data, and never holds keys or funds: your wallet signs everything. The base URL is `https://api.baselaunch.app/v1`.

- **Reads** from the indexer's database, a few seconds behind the chain: `/coins/new`, `/coins/{address}` with `/trades`, `/holders` and `/candles`, `/stats`, `/wallets/{address}/claimable`.
- **Quotes and builders**: `GET /quote`, and `POST /prepare/launch`, `/buy`, `/sell` and `/claim`, which return unsigned transactions.

Limits per client IP: 120 reads a minute, and 30 builder and quote calls a minute, counted apart. Past a limit the API answers 429 `rate_limited`. The live feed allows 20 subscriptions per client.

A builder answers with:

- `tx`, the unsigned transaction (`to`, `data`, `value`, `chainId`, and `gas` on a buy that fills the curve, so graduation runs in the same transaction).
- `approval`, the ERC-20 allowance to send first, and `permit2`, the extra step a graduated coin needs. Skip an allowance the wallet already has.
- `fallback`, the same action without the referrer: send it if `tx` reverts.
- `payee`, who receives the output; `referrerPaid`, whether your referrer is paid (false for a coin launched on 2.0, which takes none); `launchTax`, the early-buy tax in basis points if the buy landed now.

Building a buy with curlsh

```
curl -X POST https://api.baselaunch.app/v1/prepare/buy   -H "content-type: application/json"   -d '{"coin":"0x...","amountIn":"50000000000000000","slippageBps":100,"recipient":"0x...","sender":"0x...","referrer":"0x..."}'
# amountIn is wei. The answer holds tx, and approval, permit2 and fallback when they apply.
```

Two rules decide whether a call works:

- On a graduated coin the swap pays the wallet that sends it, so send `sender`, equal to `recipient`. The API refuses anything else.
- A claim (`POST /prepare/claim`) must be signed by the `claimant`: the escrow pays whoever sends it their own balance.

A contract referrer or interface needs receive()

A contract used as a referrer or launch interface without a `receive()` function can never claim its ETH credits, and nobody can claim for it.

Every route, parameter, response field and error code is on the [API reference](https://baselaunch.app/docs/api). Referrer and interface shares are in [integrator shares](https://baselaunch.app/docs#integrator-shares).

## 25. WebSocket feed

One WebSocket at `wss://api.baselaunch.app/v1/ws` pushes new coins, graduations, trades and curve progress. Send a JSON text message to subscribe; every message the server sends is `{ type, data }`.

- `{ sub: "newCoin" }` and `{ sub: "graduation" }`: every launch, and every coin that leaves the curve or opens its pool.
- `{ sub: "trade", coins }` and `{ sub: "curve", coins }`: trades, or curve progress, of the listed coins (`trade` also takes `"all"`).
- `{ sub: "wallet", wallets }`: every trade by the listed wallets, on any coin.

Following a coin and a wallet on the feedts

```
const ws = new WebSocket("wss://api.baselaunch.app/v1/ws");

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ sub: "trade", coins: ["0x..."] }));    // every trade of a coin
  ws.send(JSON.stringify({ sub: "wallet", wallets: ["0x..."] })); // every trade by a wallet, on any coin
});

ws.addEventListener("message", (event) => {
  const { type, data } = JSON.parse(event.data);
  if (type === "error") return console.error(data.error); // bad_message, bad_address, too_many_subs
  if (type === "trade" || type === "wallet") console.log(type, data.side, data.coin, data.quoteAmount, data.wallet, data.tx);
});
// The feed never replays history: backfill over REST, and subscribe again after a close.
```

A client holds 20 subscriptions, each listed coin or wallet counting as one (a list has at most 20); a request past the cap is refused whole with `too_many_subs`. Each client IP may open 10 connections, and a message over 4096 bytes closes the connection. The server pings every 30 seconds and drops a client that does not answer; browsers answer on their own. There is no unsubscribe: close the socket. Message fields are on the [API reference](https://baselaunch.app/docs/api#websocket-feed).

## 26. SDK

`@baselaunch/sdk` is a TypeScript client with no runtime dependencies: `npm install @baselaunch/sdk`. It reads (`newCoins`, `coin`, `coinTrades`, `coinHolders`, `stats`), follows the feed (`subscribe`, one socket for all subscriptions) and throws `ApiError` with the status and error code. Set `clientName` to your app's name: it is sent as `x-client-name` so the operator can see who uses the API.

Reading a coin and following its trades with the SDKts

```
import { createClient } from "@baselaunch/sdk";

const api = createClient({ clientName: "my-app/1.0" }); // https://api.baselaunch.app by default

const { coins } = await api.newCoins({ limit: 10 });
const coin = await api.coin(coins[0].address);
console.log(coin.name, coin.phase, coin.graduation.progress);

const feed = api.subscribe({ sub: "trade", coins: [coin.address] }, (m) => m.type === "trade" && console.log(m.data.side, m.data.quoteAmount));
// feed.close() stops it
```

Builders are plain HTTP calls. `sendWithFallback(walletClient, prepared)` sends `prepared.tx` and, if the send reverts before broadcast, `prepared.fallback` once; a rejection is never retried, and a transaction mined and then reverted is not retried, so check its receipt. It passes `tx.gas` through when the builder returns it (a buy that fills the curve), so graduation runs in the same transaction. `encodeReferrerHookData(referrer)` gives the `hookData` a pool swap takes.

Building a buy and sending it with viem and the SDKts

```
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { base } from "viem/chains";
import { sendWithFallback } from "@baselaunch/sdk";

const [account] = await window.ethereum.request({ method: "eth_requestAccounts" });
const wallet = createWalletClient({ account, chain: base, transport: custom(window.ethereum) });
const client = createPublicClient({ chain: base, transport: http() });

const res = await fetch("https://api.baselaunch.app/v1/prepare/buy", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ coin: "0x...", amountIn: "50000000000000000", recipient: account, sender: account, referrer: "0x..." }),
});
if (!res.ok) throw new Error((await res.json()).error);
const prepared = await res.json();

// Allowances first when present, each mined before the next send, then the buy. A revert before broadcast sends prepared.fallback once.
for (const step of [prepared.approval, prepared.permit2]) {
  if (!step) continue;
  const { hash } = await sendWithFallback(wallet, { tx: step.tx });
  await client.waitForTransactionReceipt({ hash });
}
const { hash, usedFallback } = await sendWithFallback(wallet, prepared);
```

## 27. For Telegram bot operators

A bot can show a coin's trades and offer a buy that pays its operator a share of the fee. The bot never holds keys or funds: it builds transactions, and the user's own wallet signs them.

1. Subscribe to `trade` for the coins a chat follows (or `newCoin`) and post each message: `side`, `quoteAmount` in wei, `wallet`, `tx`.
2. On a Buy tap, post to `/prepare/buy` with the user's wallet as `recipient` and `sender` and your payout wallet as `referrer`.
3. Send the answer to a page where the user's wallet signs `approval` and `permit2` if present, then `tx`, then `fallback` if it reverts. Never ask for a key or seed phrase.
4. When `referrerPaid` is true the trade credits your referrer wallet 0.2% of its volume. Collect it with `GET /wallets/{address}/claimable` and `/prepare/claim`, sent from that wallet ([claiming](https://baselaunch.app/docs#claiming)).

A Telegram bot that posts trades and offers a buyts

```
import { createClient } from "@baselaunch/sdk";
import { formatEther } from "viem";

const REFERRER = "0x..."; // your payout wallet; a contract here needs receive()
const api = createClient({ clientName: "my-bot/1.0" });
const send = (method: string, body: object) =>
  fetch(`https://api.telegram.org/bot${process.env.BOT_TOKEN}/${method}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });

// 1. Post each trade of the coins you follow, with a Buy button.
api.subscribe({ sub: "trade", coins: ["0x..."] }, (m) => m.type === "trade" && send("sendMessage", {
  chat_id: process.env.CHAT_ID,
  text: `${m.data.side} ${formatEther(BigInt(m.data.quoteAmount))} ETH`,
  reply_markup: { inline_keyboard: [[{ text: "Buy 0.05 ETH", callback_data: `buy:${m.data.coin}` }]] },
}));

// 2. On a tap, build the buy for the user's wallet with your referrer, and hand it to the page where they sign.
async function offerBuy(coin: string, wallet: string) {
  const res = await fetch("https://api.baselaunch.app/v1/prepare/buy", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ coin, amountIn: "50000000000000000", recipient: wallet, sender: wallet, referrer: REFERRER }),
  });
  return res.json(); // referrerPaid is false for a coin launched on 2.0
}
```

A contract referrer needs `receive()`, or its ETH credits can never be claimed. Every builder call from your server shares its IP's 30 a minute; for a large chat, let the page where the user signs call the builder from their browser.

## 28. Errors

These are the errors an integration is most likely to show a user. Each one is a custom error of the 2.1.0 contracts, so decode reverts with the ABIs of the coin's set.

- `SlippageExceeded`: The trade would fill worse than the minimum you passed, so quote again against fresh reserves and send a new floor.
- `CurveGraduated`: The curve has sold out and takes no more buys or sells, so send the trade to the coin's Uniswap v4 pool once its phase reaches 2.
- `WrongGraduationPhase`: A graduation step was called out of order, so read the coin's phase first and call createGraduatedPool only while it is Swept.
- `LaunchFeeNotPaid`, `InvalidValue`: The ETH sent with the launch is not the launch fee (plus the opening buy on an ETH pair), so read launchFee() right before sending.
- `LaunchEconomicsMismatch`: The owner changed the launch terms after your preview, so read previewLaunchEconomics again, show the creator the new terms and resend.
- `CreatorTaxTooHigh`: The creator fee is above the current cap, so keep creatorTaxBps at or below maxCreatorTaxBps().
- `PairTokenNotApproved`: That pair asset is not open for new coins, so check approvedPairTokens(pair) or launch on ETH.
- `NativeValueMismatch`, `UnexpectedNativeValue`: The ETH attached does not fit the coin's pair, so send exactly quoteIn on an ETH coin and no ETH at all on an ERC-20 coin.
- `NotWhitelisted`: Public launches are closed and this account is not on the launch list, so check canLaunch(account) before offering a launch.
- `ExemptionListTooLong`: More than 32 wallets were named as free of the launch tax, so send 32 or fewer.
- `TimelockNotElapsed`, `TimelockExpired`: A fee recipient override was executed too early or too late, so run it between the effectiveAt and expiresAt that pendingCreatorFeeRecipient returns.
- `InexactQuoteTransfer`: An ERC-20 payout reached the escrow short and the swap reverted, so retry a swap that names a referrer once with empty hookData.
- `ProtocolShareBelowReferrer`: Only the owner can hit this one: it stops the protocol share for new launches from going below the 20% referrer share you integrate against.

## 29. Security and support

The contracts have not been independently audited yet. An audit is part of our plan. The main launcher contracts on Base are verified on Basescan, and none of the launcher contracts is a proxy or has an upgrade function, so the code at each address above is the code that runs. Every owner power and its limits are listed in [Owner powers and safety](#owner-powers) and on [the security page](https://baselaunch.app/security).

Before you integrate

Treat the contracts as unaudited, test against a fork of Base first, and check `canLaunch` before offering launches. Nothing here is a guarantee of any coin.

### Reporting a vulnerability

Message baselaunch on [X](https://x.com/Baselaunchapp) or [Telegram](https://t.me/BaseLaunchapp) with a short note and we will move the details to a private chat. Please do not post exploit details publicly. The contact and policy are also published at [/.well-known/security.txt](https://baselaunch.app/.well-known/security.txt).

### Integration help

For questions about launching through the router, interface and referrer payouts, or indexing, use the same channels. We never DM you first and never ask for a seed phrase.

Part III

Contract reference

Every launcher contract has its own reference page with every function and event it has.

## 30. Contract reference

Every launcher contract has its own reference page listing every function and event, who may call each function, and the contract's addresses on both Base launcher sets. Start at the [contract reference](https://baselaunch.app/docs/reference), or open a contract:

- [BaseLaunchFactory](https://baselaunch.app/docs/reference/factory)
- [BaseLaunchRouter](https://baselaunch.app/docs/reference/router)
- [BaseLaunchInterfaceRegistry](https://baselaunch.app/docs/reference/registry)
- [BaseLaunchBondingCurve](https://baselaunch.app/docs/reference/curve)
- [BaseLaunchToken](https://baselaunch.app/docs/reference/token)
- [BaseLaunchHook](https://baselaunch.app/docs/reference/hook)
- [BaseLaunchFeeEscrow](https://baselaunch.app/docs/reference/escrow)
- [BaseLaunchBuybackVault](https://baselaunch.app/docs/reference/vault)
- [BaseLaunchLocker](https://baselaunch.app/docs/reference/locker)
- [BaseLaunchDeployer](https://baselaunch.app/docs/reference/deployer)
- [BaseLaunchGraduationExecutor](https://baselaunch.app/docs/reference/executor)
- [BaseLaunchGraduationGuard](https://baselaunch.app/docs/reference/guard)
- [BaseLaunchHoldersDistributorFactory](https://baselaunch.app/docs/reference/distributor-factory)
- [BaseLaunchHoldersDistributor](https://baselaunch.app/docs/reference/distributor)

[Back to top](#top)

Canonical page: https://baselaunch.app/docs
