bow.fun documentation
A fair-launch token platform on Robinhood Chain. Launch an ERC-20 into a Uniswap-V3 pool with liquidity locked forever, earn a share of trading fees, and let tokens graduate at 3.7 ETH. Every token address ends in b03.
Introduction
- Non-custodial — every action is signed by the user's own wallet.
- Immutable tokens — no owner, no mint, no tax, no blacklist.
- LP locked by contract — there is no withdraw /
decreaseLiquiditypath.
Network
| Field | Value |
|---|---|
| Chain | Robinhood Chain |
| Chain ID | 4663 (0x1237) |
| RPC | https://rpc.mainnet.chain.robinhood.com |
| Explorer | https://robinhoodchain.blockscout.com |
| Gas token | ETH |
block.number opcode advances ~every 14 s, even though RPC blocks come ~10/sec. The trading gate (launchDelay) counts opcode blocks — so launchDelay = 1 ≈ 14 seconds. Use launchDelay = 0 for instant trading.Deployed contracts
| Contract | Address | Role |
|---|---|---|
| LaunchFactory | 0xC70E510E14710Ea535CAB7b2414860aF63FEab79 | Deploys tokens, creates + locks the pool |
| Locker | 0x904dCCB96d877E6db365282251Fa3dD156476660 | Owns every position NFT forever; splits fees |
| BowZap | 0xCCA95E5442BbF175d8a1Ad136Be317fA6D55CC38 | 1-tx buy / sell helper |
| WETH | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 | Quote asset |
| SwapRouter02 | 0xCaf681a66D020601342297493863E78C959E5cb2 | Uniswap V3 router |
| PositionManager | 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3 | V3 positions |
| UniswapV3Factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA | V3 pools |
factory.locker(). New launches use the factory above.How it works
Launch (one tx): the factory deploys the token via CREATE2 (full 1B supply minted to the factory), creates & prices the V3 pool at your target market cap, mints a single-sided position (only the token), transfers the position NFT to the Locker forever, writes on-chain socials/IPFS metadata, and optionally performs a dev buy in the same transaction.
Anti-snipe: a per-wallet 2% cap is enforced for limitWindow blocks after launch, then lifts. The launch gate blocks public trading for launchDelay opcode-blocks (deployer & launchpad exempt, so the dev buy always lands first).
Developer guide — Setup
All examples use ethers v6 (npm i ethers).
import { ethers } from "ethers";
const RPC = "https://rpc.mainnet.chain.robinhood.com";
const provider = new ethers.JsonRpcProvider(RPC, 4663);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const A = {
factory: "0xC70E510E14710Ea535CAB7b2414860aF63FEab79",
locker: "0x904dCCB96d877E6db365282251Fa3dD156476660",
zap: "0xCCA95E5442BbF175d8a1Ad136Be317fA6D55CC38",
weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
router: "0xCaf681a66D020601342297493863E78C959E5cb2",
};
const POOL_FEE = 10000; // 1% tier — used everywhere
Launch a token
LaunchParams is a tuple — field order matters. Mine a salt so the CREATE2 address ends in checksummed b03.
const FACTORY_ABI = [
"function launchFee() view returns (uint256)",
"function locker() view returns (address)",
"function tokenInitCodeHash((string name,string symbol,uint256 totalSupply,uint256 launchDelay,uint256 maxWallet,uint256 limitWindow,uint256 targetFdvWeth,bytes32 salt,string description,string website,string telegram,string twitter,string logoURI,string tokenURI,uint256 devBuyMinTokens) p, address creator) pure returns (bytes32)",
"function predictToken(bytes32 salt, bytes32 initCodeHash) view returns (address)",
"function launch((string name,string symbol,uint256 totalSupply,uint256 launchDelay,uint256 maxWallet,uint256 limitWindow,uint256 targetFdvWeth,bytes32 salt,string description,string website,string telegram,string twitter,string logoURI,string tokenURI,uint256 devBuyMinTokens) p) payable returns (address token, uint256 positionId)",
"event Launched(address indexed token, address indexed deployer, address pool, uint256 positionId, uint256 launchId)",
];
const factory = new ethers.Contract(A.factory, FACTORY_ABI, wallet);
// 1. build params as an ORDERED array (the tuple)
const supply = 1_000_000_000n * 10n ** 18n; // 1B tokens
const p = [
"My Token", // name
"mytoken", // symbol (any case — lowercase allowed)
supply, // totalSupply
0n, // launchDelay (opcode-blocks; 0 = instant)
(supply * 200n) / 10000n, // maxWallet = 2%
10n, // limitWindow (blocks the 2% cap is active)
ethers.parseEther("1.5"), // targetFdvWeth = start market cap in WETH
ethers.ZeroHash, // salt (set after mining, index 7)
"before pepe, there was...", // description
"https://mytoken.xyz", // website (optional "")
"https://t.me/mytoken", // telegram (optional "")
"https://x.com/mytoken", // twitter (optional "")
"ipfs://<logoCid>", // logoURI (optional — pin your own)
"ipfs://<metaCid>", // tokenURI (optional metadata json)
0n, // devBuyMinTokens (slippage floor; 0 = any)
];
// 2. mine a salt -> checksummed address ends in "b03"
const initHash = await factory.tokenInitCodeHash(p, wallet.address);
let salt;
for (let i = 0; ; i++) {
const s = ethers.zeroPadValue(ethers.toBeHex(i), 32);
const raw = "0x" + ethers.keccak256(ethers.concat(["0xff", A.factory, s, initHash])).slice(-40);
if (raw.endsWith("b03") && ethers.getAddress(raw).endsWith("b03")) { salt = s; break; }
}
p[7] = salt;
// 3. launch. Send extra ETH beyond launchFee to DEV BUY in the same tx
const fee = await factory.launchFee(); // currently 0
const devBuyEth = ethers.parseEther("0.1"); // optional — 0n for none
const tx = await factory.launch(p, { value: fee + devBuyEth });
const rc = await tx.wait();
// 4. address is deterministic; positionId is in the Launched event
const token = await factory.predictToken(salt, initHash);
const ev = rc.logs.map(l => { try { return factory.interface.parseLog(l); } catch { return null; } })
.find(e => e && e.name === "Launched");
console.log("token:", token, "positionId:", ev.args.positionId.toString());
msg.value − launchFee. IPFS is optional — pass empty strings for no image.Buy — BowZap.buy
Native ETH → token, in one transaction. The zap wraps your ETH and swaps to the token, sent straight to you.
const ZAP_ABI = ["function buy(address token, uint24 fee, uint256 minOut) payable returns (uint256)"];
const zap = new ethers.Contract(A.zap, ZAP_ABI, wallet);
const ethIn = ethers.parseEther("0.05");
const minOut = 0n; // slippage floor in tokens (see Quoting)
const tx = await zap.buy(token, POOL_FEE, minOut, { value: ethIn });
await tx.wait(); // tokens sent to wallet.address
MaxWalletExceeded — that's the intended anti-snipe.Sell — via the router
Don't sell through the Zap during the opening window — the zap would hold your tokens mid-swap and trip the 2% cap. Sell directly through the router so the token flows you → pool (the pool is cap-exempt), then unwrap WETH → native ETH back to you, in one tx.
const ERC20_ABI = [
"function approve(address,uint256) returns (bool)",
"function allowance(address,address) view returns (uint256)",
];
const ROUTER_ABI = [
"function exactInputSingle((address tokenIn,address tokenOut,uint24 fee,address recipient,uint256 amountIn,uint256 amountOutMinimum,uint160 sqrtPriceLimitX96) params) payable returns (uint256)",
"function unwrapWETH9(uint256 amountMinimum, address recipient) payable",
"function multicall(bytes[] data) payable returns (bytes[])",
];
const tokenC = new ethers.Contract(token, ERC20_ABI, wallet);
const router = new ethers.Contract(A.router, ROUTER_ABI, wallet);
const amountIn = ethers.parseEther("1000000"); // tokens to sell (18 decimals)
// 1. one-time approval of the router
if ((await tokenC.allowance(wallet.address, A.router)) < amountIn) {
await (await tokenC.approve(A.router, ethers.MaxUint256)).wait();
}
// 2. swap token -> WETH (kept in router), then unwrap to native ETH — atomic
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002"; // "router keeps output"
const minOut = 0n; // slippage floor in WETH
const call1 = router.interface.encodeFunctionData("exactInputSingle", [{
tokenIn: token, tokenOut: A.weth, fee: POOL_FEE, recipient: ADDRESS_THIS,
amountIn, amountOutMinimum: minOut, sqrtPriceLimitX96: 0n,
}]);
const call2 = router.interface.encodeFunctionData("unwrapWETH9", [minOut, wallet.address]);
await (await router.multicall([call1, call2])).wait(); // native ETH to your wallet
exactInputSingle with recipient: wallet.address and skip the multicall/unwrap.Quoting & slippage
The single-sided pool behaves like a constant-product curve early on. Estimate, or staticCall for an exact quote and apply your slippage %.
// buy: tokens out ≈ supply * ethIn*(1-fee) / (targetFdvWeth + ethIn*(1-fee))
const est = await zap.buy.staticCall(token, POOL_FEE, 0n, { value: ethIn });
const minOut = est * 95n / 100n; // 5% slippage
Collect fees
Permissionless — anyone can trigger it; fees route to the configured receivers (creator gets the 35% WETH share) regardless of caller.
const LOCKER_ABI = ["function collect(uint256 tokenId)"];
const locker = new ethers.Contract(await factory.locker(), LOCKER_ABI, wallet);
await (await locker.collect(positionId)).wait();
Preview pending fees (no spend)
const NPM = "0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3";
const NPM_ABI = [
"function ownerOf(uint256) view returns (address)",
"function collect((uint256 tokenId,address recipient,uint128 amount0Max,uint128 amount1Max) params) payable returns (uint256 amount0, uint256 amount1)",
];
const npm = new ethers.Contract(NPM, NPM_ABI, provider);
const owner = await npm.ownerOf(positionId); // the locker
const MAX = (1n << 128n) - 1n;
const [a0, a1] = await npm.collect.staticCall(
{ tokenId: positionId, recipient: owner, amount0Max: MAX, amount1Max: MAX },
{ from: owner }
);
const wethTotal = A.weth.toLowerCase() < token.toLowerCase() ? a0 : a1;
const creatorWeth = wethTotal * 3500n / 10000n; // creator's 35% share
Read state
const TOKEN_ABI = [
"function symbol() view returns (string)",
"function pool() view returns (address)",
"function migrated() view returns (bool)", // graduated?
"function checkMigration() returns (bool)", // permissionless latch
"function logoURI() view returns (string)",
"function GRADUATION_WETH() view returns (uint256)", // 3.7e18
];
const t = new ethers.Contract(token, TOKEN_ABI, provider);
console.log("graduated:", await t.migrated());
// list all launches on a factory
const LIST_ABI = [
"function launchCount() view returns (uint256)",
"function launches(uint256) view returns (address token, address pool, uint256 positionId, address deployer)",
];
const f = new ethers.Contract(A.factory, LIST_ABI, provider);
const n = Number(await f.launchCount());
for (let i = n - 1; i >= 0; i--) console.log(await f.launches(i));
// stream new launches over WebSocket
const TOPIC = ethers.id("Launched(address,address,address,uint256,uint256)");
// wsProvider.on({ address: A.factory, topics: [TOPIC] }, log => { ... })
Gotchas
- Symbols keep their case —
dogeorDOGE, both survive on-chain. b03is checksummed lowercase — mine againstethers.getAddress(raw).endsWith("b03"), not just the raw bits, or you may get…B03.- Trading gate uses opcode blocks (~14s each).
launchDelay = 0→ instant. - Slippage reverts surface as
Too little received— raiseminOut. - Immutable token — no owner, mint, tax, pause, or blacklist.
setPool/setMetadataare one-time launchpad-only calls in the launch tx.
