Build on Liquidmint
Everything a bot, terminal or aggregator needs to recognise our tokens and trade them. All of it is also available as one JSON document at /api/info, and the contract definitions at /api/abi/launcher, /token and /hook.
Contracts on Robinhood Chain (chain 4663)
This site is not connected to a launcher yet. Our own addresses are published here at launch; Uniswap's are listed already.
How to recognise a Liquidmint token
- Listen to TokenCreated(token, creator, name, symbol) on the launcher
- or: PoolManager Initialize events whose hooks field equals the Liquidmint hook
- confirm with launcher.launchOf(token): the creator is not the zero address
- a pool for the same token with a different hook is NOT ours
Pools
Every token has its own Uniswap V4 pool, created in the launch transaction: currency0 is native ETH (the zero address), currency1 is the token, pool fee static: the creator's choice at launch (0, 3000, 5000 or 10000: read it from the pool key), tick spacing 200, hooks = the launch hook. Pool id = keccak256(abi.encode(poolKey)). Supply is 1,000,000,000 tokens. The liquidity is locked: the launcher has no function that removes it, and the hook rejects any removal. Price in ETH per token = 1 / (sqrtPriceX96 / 2^96)^2, read with StateView.getSlot0(poolId).
Fees, and the launch tax your bot will meet
- Every swap pays a 1% protocol fee, taken by the hook itself on the INPUT currency (buys pay it in ETH, sells in tokens) and split per token between the creator and the platform (default 85% creator, fixed at launch). Outside liquidity providers cannot dilute it.
- On top, the pool has a pool fee the creator chose at launch (none by default, or 0.3%, 0.5% or 1%), which goes to liquidity providers. Total cost to a trader = protocol fee + pool fee.
- Launch tax: buys pay a fee falling in a straight line from 99% to 1% over 30 seconds, counted on the chain's clock from the launch block. Sells are never taxed, and only the launcher's own first buy (in the launch transaction) is exempt: there is no whitelist. The tax is burned.
- The fee right now:
hook.currentFee(sender, poolKey, zeroForOne). The tax held by the hook until the launcher sweeps and burns it:hook.taxEth(poolId). - Reading trades: the PoolManager's
Swapevent reports the pool-level amounts, net of the hook's fee. What a trader actually paid is the pool-level input plus thefeein the hook'sFeeTakenevent for the same swap (below). An exact-input swap the pool cannot fill in full reverts (PartialFill), and the hook takes its ETH fee from the PoolManager's own balance while the buyer's ETH is not yet paid in. - Uniswap's V4Quoter runs the real swap, so quotes already include the hook's fee. A quote is computed at the latest block's time, so just after a launch it can be worse than a transaction mined a moment later (the tax only falls).
- Fees paid in tokens (by sellers): the platform's share is always burned; the creator can pledge to burn a share of theirs:
launcher.burnBpsOf(token). Tokens burned from fees so far:launcher.feeBurned(token).
Trading through the Universal Router
Plain ETH in, tokens out, one transaction; no wrapping. The code below is what our own site sends (it is tested against a copy of the real chain).
import { encodeAbiParameters } from "viem";
const poolKey = [{ name: "currency0", type: "address" }, { name: "currency1", type: "address" }, { name: "fee", type: "uint24" },
{ name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" }];
// Robinhood's Universal Router has ONE extra field (minHopPriceX36, always 0) between amountOutMinimum and hookData.
const swap = { type: "tuple", components: [
{ name: "poolKey", type: "tuple", components: poolKey }, { name: "zeroForOne", type: "bool" }, { name: "amountIn", type: "uint128" },
{ name: "amountOutMinimum", type: "uint128" }, { name: "minHopPriceX36", type: "uint256" }, { name: "hookData", type: "bytes" } ] };
function plan(key, zeroForOne, amountIn, minOut) {
const s = encodeAbiParameters([swap], [{ poolKey: key, zeroForOne, amountIn, amountOutMinimum: minOut, minHopPriceX36: 0n, hookData: "0x" }]);
const settle = encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [zeroForOne ? key.currency0 : key.currency1, amountIn]);
const take = encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [zeroForOne ? key.currency1 : key.currency0, minOut]);
return encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], ["0x060c0f", [s, settle, take]]); // SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL
}
// BUY with plain ETH: router.execute("0x10", [plan(key, true, ethIn, minTokensOut)], deadline, { value: ethIn })
// SELL for plain ETH: router.execute("0x10", [plan(key, false, tokensIn, minEthOut)], deadline)
// (command 0x10 = V4_SWAP; our tokens start with an unlimited allowance for Permit2, so a sell needs one Permit2.approve(token, router, amount, expiry),
// or a signed PermitSingle sent first with command 0x0a: use commands "0x0a10" and inputs [permitInput, plan(...)])
// Quote first with the V4Quoter: quoteExactInputSingle({ poolKey, zeroForOne, exactAmount, hookData: "0x" })