TypeScript
Launch and trade a seeded pool
The other launcher, end to end: create a coin that opens as a Uniswap v3 pool, buy it, sell it, and collect what the pool has earned — all with the same viem client.
A seeded launch has no curve and no graduation — it is a Uniswap v3 pool from the block it is created in. That makes the integration shorter than the curve's: one call to our factory to create the coin, and after that it is an ordinary v3 token that Uniswap's own router buys and sells.
Everything here builds on Setup, which the samples import as
size.ts. The seeded half of that file is at the bottom of it: the factory,
the locker, Uniswap's SwapRouter02 and QuoterV2, and the ABIs.
Launching#
One call. It deploys the token at a CREATE2 address, creates the pool at the config's opening price, mints the whole supply into a one-sided position above it, hands the position to a locker with no withdrawal function, and — if you send more than the launch fee — buys the coin for you before anyone else can.
import { parseEther, parseEventLogs, toHex } from 'viem'
import {
publicClient, walletClient, account,
seeded, seededFactoryAbi, SEEDED_CONFIG_ID,
} from './size.js'
const launchFee = await publicClient.readContract({
address: seeded.factory,
abi: seededFactoryAbi,
functionName: 'launchFee',
})
// Anything above the launch fee is spent buying the coin for `feeWallet` in the
// same transaction. It is the only buy the token's first block allows, and it
// is available only on an OKB-quoted launch — the factory rejects value on any
// other quote asset rather than stranding it. Set it to 0n to launch without.
const devBuy = parseEther('0.5')
// CREATE2, so a repeated salt is a repeated address and the launch reverts.
const salt = toHex(crypto.getRandomValues(new Uint8Array(32)))
const params = {
name: 'Seeded Doge',
symbol: 'SDOGE',
logo: 'https://size.fun/api/img/…',
description: 'Straight into a v3 pool.',
socials: {
twitter: 'https://x.com/…', telegram: '', discord: '',
website: 'https://…', farcaster: '',
},
// Where the creator's 70% is paid, and who receives the dev buy above.
feeWallet: account.address,
} as const
const { request, result: token } = await publicClient.simulateContract({
address: seeded.factory,
abi: seededFactoryAbi,
functionName: 'launchToken',
// dexId 0 is the canonical Uniswap v3 deployment; it is the only one today.
args: [params, SEEDED_CONFIG_ID, 0n, salt],
value: launchFee + devBuy,
account,
})
const hash = await walletClient.writeContract(request)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
// The pool address is in the event, and it is the thing worth keeping: it is
// what DexScreener, an aggregator or your own indexer will key the coin on.
const [launched] = parseEventLogs({
abi: seededFactoryAbi,
eventName: 'TokenLaunched',
logs: receipt.logs,
})
console.log({ token, pool: launched.args.pool, positionId: launched.args.positionId })
The first block is closed
The token refuses every pool buy in its own launch block except the one the factory makes inside this transaction, and caps a wallet at 5% of supply for two blocks after it. A bot that lands in the launch block reverts; you do not have to outrun it.
Which config id#
One config per quote asset, positional, and 0 is
OKB. Read them rather than assuming the order — a disabled config reverts a launch, and
the list can grow:
const count = await publicClient.readContract({
address: seeded.factory, abi: seededFactoryAbi, functionName: 'launchConfigCount',
})
for (let id = 0n; id < count; id++) {
const config = await publicClient.readContract({
address: seeded.factory, abi: seededFactoryAbi, functionName: 'getLaunchConfig', args: [id],
})
// initialTick is the opening price: 1.0001 ** tick, per token.
const openingFdv = 1.0001 ** config.initialTick * (Number(config.supply) / 1e18)
console.log({ id, pairToken: config.pairToken, openingFdv, enabled: config.enabled })
}
Knowing the address first#
predictTokenAddress answers with the same CREATE2 address the launch will
deploy, for the same parameters and salt. Useful for mining a salt that sorts a
particular way, or just for having the address before the transaction lands.
Buying#
A plain exactInputSingle on Uniswap's SwapRouter02, on the 1% tier. The
value goes as native OKB and the router wraps it — the buyer never touches WOKB.
import { parseEther } from 'viem'
import type { Address } from 'viem'
import {
publicClient, walletClient, account,
seeded, quoterAbi, routerAbi, POOL_FEE,
} from './size.js'
/** What a buy would fill at, price impact included. The quoter runs the swap
* and reverts, which is the only exact answer — a price read off the pool
* ignores the trade's own impact, and on a young pool that is most of it. */
export async function quoteBuy(token: Address, okb: string) {
const { result } = await publicClient.simulateContract({
address: seeded.quoter,
abi: quoterAbi,
functionName: 'quoteExactInputSingle',
args: [{
tokenIn: seeded.wokb,
tokenOut: token,
amountIn: parseEther(okb),
fee: POOL_FEE,
sqrtPriceLimitX96: 0n,
}],
})
return result[0]
}
export async function buySeeded(token: Address, okb: string, slippageBps = 100n) {
const amountIn = parseEther(okb)
const quoted = await quoteBuy(token, okb)
const amountOutMinimum = (quoted * (10_000n - slippageBps)) / 10_000n
const { request } = await publicClient.simulateContract({
address: seeded.router,
abi: routerAbi,
functionName: 'exactInputSingle',
args: [{
// WOKB in, but the value below is native OKB: the router wraps its own
// balance, so the buyer never holds the wrapped token.
tokenIn: seeded.wokb,
tokenOut: token,
fee: POOL_FEE,
recipient: account.address,
amountIn,
amountOutMinimum,
sqrtPriceLimitX96: 0n,
}],
value: amountIn,
account,
})
const hash = await walletClient.writeContract(request)
return publicClient.waitForTransactionReceipt({ hash })
}
await buySeeded('0x…', '0.25')
Selling#
Two calls in one transaction: the swap pays the router, and unwrapWETH9
turns that into OKB for the seller. Sending the swap straight to the wallet is the common
mistake — it works, and leaves them holding WOKB.
import { encodeFunctionData, erc20Abi, parseEther } from 'viem'
import type { Address } from 'viem'
import {
publicClient, walletClient, account,
seeded, quoterAbi, routerAbi, POOL_FEE, ADDRESS_THIS,
} from './size.js'
export async function sellSeeded(token: Address, amount: string, slippageBps = 100n) {
const amountIn = parseEther(amount)
// Selling moves tokens into the pool through the router, so it needs an
// allowance. Buying does not — that leg is the transaction's own value.
const allowance = await publicClient.readContract({
address: token, abi: erc20Abi, functionName: 'allowance',
args: [account.address, seeded.router],
})
if (allowance < amountIn) {
const { request } = await publicClient.simulateContract({
address: token, abi: erc20Abi, functionName: 'approve',
args: [seeded.router, amountIn], account,
})
await publicClient.waitForTransactionReceipt({ hash: await walletClient.writeContract(request) })
}
const { result } = await publicClient.simulateContract({
address: seeded.quoter,
abi: quoterAbi,
functionName: 'quoteExactInputSingle',
args: [{
tokenIn: token, tokenOut: seeded.wokb, amountIn, fee: POOL_FEE, sqrtPriceLimitX96: 0n,
}],
})
const floor = (result[0] * (10_000n - slippageBps)) / 10_000n
// Two calls, one transaction: the swap pays the router, then the unwrap turns
// that into OKB for the seller. Sending the swap straight to the wallet would
// leave them holding WOKB and needing a second transaction to spend it.
const calls = [
encodeFunctionData({
abi: routerAbi,
functionName: 'exactInputSingle',
args: [{
tokenIn: token,
tokenOut: seeded.wokb,
fee: POOL_FEE,
recipient: ADDRESS_THIS,
amountIn,
amountOutMinimum: floor,
sqrtPriceLimitX96: 0n,
}],
}),
encodeFunctionData({
abi: routerAbi, functionName: 'unwrapWETH9', args: [floor, account.address],
}),
]
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200)
const { request } = await publicClient.simulateContract({
address: seeded.router,
abi: routerAbi,
functionName: 'multicall',
args: [deadline, calls],
account,
})
const hash = await walletClient.writeContract(request)
return publicClient.waitForTransactionReceipt({ hash })
}
await sellSeeded('0x…', '1000000')
Collecting what the pool earned#
The pool charges 1% in whichever token went in, and it accrues inside the locked position. Collecting splits it 30% protocol / 70% creator and credits both halves to the same escrow a curve launch pays into, so one claim collects everything.
import { formatEther } from 'viem'
import type { Address } from 'viem'
import {
publicClient, walletClient, account,
seeded, seededLockerAbi, seededFactoryAbi, escrowAbi, size,
} from './size.js'
/** What the pool has earned and not yet distributed. A v3 position accrues
* fees with no view function to read them off, so this simulates the
* collection — the return values are the answer, and simulating costs
* nothing. */
export async function pendingFees(token: Address) {
const [{ result }, launched] = await Promise.all([
publicClient.simulateContract({
address: seeded.locker, abi: seededLockerAbi, functionName: 'collectFees', args: [token],
}),
publicClient.readContract({
address: seeded.factory, abi: seededFactoryAbi, functionName: 'getLaunchedToken', args: [token],
}),
])
// Pool ordering is by address, so which amount is the quote side depends on
// which side the launch token sorted onto.
const [amount0, amount1] = result
return { quote: launched.isToken0 ? amount1 : amount0, token: launched.isToken0 ? amount0 : amount1 }
}
/** Moves them into the escrow, split 30% protocol / 70% creator. Anyone may
* call this: it can only pay the two addresses the launch already fixed. */
export async function collect(token: Address) {
const { request } = await publicClient.simulateContract({
address: seeded.locker, abi: seededLockerAbi, functionName: 'collectFees', args: [token], account,
})
const hash = await walletClient.writeContract(request)
await publicClient.waitForTransactionReceipt({ hash })
// From here it is the same claim a curve launch's fees go through — the
// escrow pays whoever calls it, so there is no recipient argument.
const claimable = await publicClient.readContract({
address: size.feeEscrow, abi: escrowAbi, functionName: 'balanceOf', args: [account.address],
})
console.log(`claimable: ${formatEther(claimable)} OKB`)
}
Reading and indexing one#
There is no curve to read state off. Price is the pool's slot0, depth is
what the pool holds, and every trade is the pool's own Swap event — the
standard Uniswap v3 one, so an existing v3 indexer needs nothing new:
const launched = await publicClient.readContract({
address: seeded.factory, abi: seededFactoryAbi, functionName: 'getLaunchedToken',
args: [token],
})
// Which side of the pool the coin sorted onto decides what slot0's price means
// and which leg of a Swap is the quote asset.
const { pairedPrincipal, threshold } = await publicClient.readContract({
address: seeded.factory, abi: seededFactoryAbi, functionName: 'graduationStatus',
args: [token],
})
console.log({ pool: launched.positionId, isToken0: launched.isToken0, pairedPrincipal, threshold })
graduationStatus reports how much quote has accumulated in the position
against a milestone. Nothing acts on it — a seeded launch never migrates anywhere — it is
there so a UI can show progress against the same figure a curve launch graduates at.
What is different from a curve launch#
| Seeded | Curve | |
|---|---|---|
| Launch call | launchToken(params, configId, dexId, salt) on the V3 factory |
launchToken(params, configId, pairToken) on the curve factory |
| Buy / sell | Uniswap SwapRouter02 | the coin's own curve, then the v4 pool after graduation |
| Quote | QuoterV2 | simulate buy/sell at a floor of zero |
| Fees out | collectFees(token) on the locker |
sweepFees(minBuybackTokensOut) on the curve |
| Claim | the same claim() on the same escrow | |
| Trades | the pool's Swap |
CurveBuy / CurveSell, then v4 Swap |