bow.fun documentation
A fair-launch token platform on Robinhood Chain. Launch an ERC-20 two ways — Standard (Uniswap V3 · WETH) or RWA (Uniswap V4 · paired with a tokenized stock) — 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. - One entry point — both modes launch through the FactoryHub contract.
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.Launch modes
Every launch goes through the FactoryHub — a single launch(slotId, params) entry point where the slot picks the mode:
| Mode | Slot | Version | Pool | Access | Notes |
|---|---|---|---|---|---|
| Standard | 1 | 3 | Uniswap V3 · WETH | Public | Same mechanics as before — single-sided pool, graduates at 3.7 ETH |
| RWA | 3 | 5 | Uniswap V4 · stock quote | Public | Paired with 1 of 25 tokenized stocks (AAPL, NVDA, SPY…) |
The version in the Launched event tells you what kind of token you're looking at: 3 = V3 (the pool address is set, poolId = 0); 4+ = V4 (the poolId is set, pool = 0 — V4 pools live inside the PoolManager singleton and have no address of their own).
Deployed contracts
| Contract | Address | Role |
|---|---|---|
| FactoryHub | 0x229Faa919ABf14279E2461Dba53F039c5B4C7E29 | Launch entry point — all 3 modes (UUPS proxy) |
| LaunchFactory legacy | 0xC70E510E14710Ea535CAB7b2414860aF63FEab79 | Old V3 factory — existing tokens only, no new launches |
| Legacy Locker | 0x904dCCB96d877E6db365282251Fa3dD156476660 | Positions of legacy-factory tokens |
| BowZap | 0xCCA95E5442BbF175d8a1Ad136Be317fA6D55CC38 | 1-tx buy / sell helper — V3 tokens only |
| WETH | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 | Quote asset (Standard) |
| USDG | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 | RWA routing intermediary — 6 decimals |
| SwapRouter02 | 0xCaf681a66D020601342297493863E78C959E5cb2 | Uniswap V3 router (Standard tokens) |
| PositionManager (V3) | 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3 | V3 positions |
| UniswapV3Factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA | V3 pools |
| PoolManager (V4) | 0x8366a39CC670B4001A1121B8F6A443A643e40951 | V4 singleton — every RWA pool lives inside it |
| PositionManager (V4) | 0x58daec3116aae6D93017bAAea7749052E8a04fA7 | V4 positions |
| Universal Router | 0x8876789976dEcBfCbBbe364623C63652db8C0904 | V4 swaps — a fork, see Trade RWA tokens |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 | Token approvals for V4 sells |
| V4 Quoter | 0xe8393F168D54B622218Adc36037008f254404C40 | Exact V4 quotes — view-only, never holds funds |
locker() getter. Read it per slot from hub.getSlotInfo(slotId), or use the table in Collect fees.How it works
Standard (slot 1), one tx: the hub deploys the token via CREATE2 (full 1B supply), creates & prices the V3 WETH pool at your target market cap, mints a single-sided position (only the token), locks the position NFT in the slot's locker forever, writes on-chain socials/IPFS metadata, and optionally performs a dev buy in the same transaction. Graduates when the pool holds 3.7 ETH.
RWA (slot 3): one V4 pool pairing the token against a tokenized stock (the quote token). Price and graduation are denominated in the quote; ETH buys/sells route WETH ↔ USDG ↔ quote ↔ token through three pools (see Trade RWA tokens).
Anti-snipe (all modes): 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 = {
hub: "0x229Faa919ABf14279E2461Dba53F039c5B4C7E29", // FactoryHub — all launches
weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
usdg: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
zap: "0xCCA95E5442BbF175d8a1Ad136Be317fA6D55CC38", // V3 tokens only
router: "0xCaf681a66D020601342297493863E78C959E5cb2", // V3 SwapRouter02
ur: "0x8876789976dEcBfCbBbe364623C63652db8C0904", // Universal Router (V4)
permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3",
};
const POOL_FEE = 10000; // 1% tier — every launch pool, V3 and V4 (tickSpacing 200 on V4)
Launch a token
All modes share one call: hub.launch(slotId, params). LaunchParams is a 21-field tuple — field order matters. Fields that don't apply to your mode are zero / empty.
const HUB_ABI = [
"function getSlotInfo(uint8 slotId) view returns (uint8 version, bool active, bool launchEnabled, address npm, address weth, address router, address locker, address treasury, uint256 launchFee)",
"function launch(uint8 slotId, (string name,string symbol,uint256 totalSupply,uint256 launchDelay,uint256 maxWallet,uint256 limitWindow,bytes32 salt,string description,string website,string telegram,string twitter,string logoURI,string tokenURI,uint256 targetFdvWeth,uint256 devBuyMinTokens,uint64 vestingCliff,uint64 vestingDuration,uint256 seedEth,uint256 buyEth,address quoteToken,uint24 wethQuoteFee) p) payable returns (address token, uint256 positionId)",
"event Launched(address indexed token, address indexed deployer, uint8 indexed version, uint8 slotId, address pool, bytes32 poolId, uint256 positionId, uint256 launchId)",
];
const hub = new ethers.Contract(A.hub, HUB_ABI, wallet);
// 1. build params as an ORDERED array (Standard / slot 1 shown)
const supply = 1_000_000_000n * 10n ** 18n; // 1B tokens
const buyEth = ethers.parseEther("0.1"); // dev buy — now an explicit field
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.hexlify(ethers.randomBytes(32)), // salt (raw CREATE2 salt — see b03 note)
"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)
ethers.parseEther("1.5"), // targetFdvWeth = start market cap in WETH
0n, // devBuyMinTokens (slippage floor; 0 = any)
0n, // vestingCliff — reserved, leave 0
0n, // vestingDuration — reserved, leave 0
0n, // seedEth — reserved, leave 0
buyEth, // buyEth = the dev buy
ethers.ZeroAddress, // quoteToken — RWA only
0, // wethQuoteFee — RWA only
];
// 2. launch. msg.value must equal launchFee + buyEth EXACTLY (EthMismatch otherwise)
const info = await hub.getSlotInfo(1); // [8] = launchFee (currently 0)
const est = await hub.launch.estimateGas(1, p, { value: info[8] + buyEth });
const tx = await hub.launch(1, p, { value: info[8] + buyEth, gasLimit: est * 125n / 100n });
const rc = await tx.wait();
// 3. token address + positionId from the Launched event in the receipt
const ev = rc.logs.map(l => { try { return hub.interface.parseLog(l); } catch { return null; } })
.find(e => e && e.name === "Launched");
console.log("token:", ev.args.token, "positionId:", ev.args.positionId.toString());
estimateGas and pad ~25% (the site uses max(4.5M, est × 1.25)).RWA launches (slot 3)
Set quoteToken to one of the 25 supported stock tokens and wethQuoteFee to that stock's USDG-pool fee tier (e.g. NVDA 0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC → 10000). Your buyEth is routed WETH → USDG → stock → token inside the launch tx.
The b03 address suffix
The hub exposes no predictToken/tokenInitCodeHash helpers, but it CREATE2-deploys with your raw salt, and the token creation code is readable from the on-chain TokenCodeProvider (0xF53C1Be2088dD9345CCC74582e81e0c19F462F1b, via hub.codeProvider()) — so vanity mining still works, and the bow.fun frontend mines …b03 client-side for both modes. If you integrate directly and don't want to reimplement that, just use a random salt and read the token address from the Launched event.
buyEth field now, not leftover msg.value. IPFS is optional — pass empty strings for no image.Buy (Standard) — BowZap.buy
V3 tokens only (Standard / legacy) — RWA tokens trade through the Universal Router, see Trade RWA tokens.
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 (Standard) — 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.Trade RWA tokens
RWA tokens live in Uniswap V4 pools and trade through the Universal Router: execute(commands, inputs, deadline), where each command byte has one ABI-encoded input.
uint256[] minHopPriceX36 field (uint256 in the single-hop struct) after amountOutMinimum — pass [] / 0 to disable the per-hop check. Stock Uniswap SDK encoding abi-decodes garbage here and reverts.Route. RWA tokens pair a stock quote, so an ETH buy is three hops: WETH →(200/4)→ USDG →(stock's usdgFee/usdgTs)→ stock →(10000/200)→ token (reverse to sell). Each PathKey is (intermediateCurrency, fee, tickSpacing, hooks, hookData) describing the pool into that currency. All launch pools are fee 10000 / tickSpacing 200, no hooks.
Buy (native ETH in): commands 0x0b10 = WRAP_ETH then V4_SWAP. Wrap to the router (ADDRESS_THIS = 0x…02), then V4 actions SWAP_EXACT_IN → SETTLE(weth, 0 = open delta, payerIsUser: false) → TAKE_ALL(token, minOut). SETTLE_ALL would try to pull WETH from you instead of the router's wrapped balance — that's why plain SETTLE with payerIsUser=false is required.
const UR_ABI = ["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"];
const ur = new ethers.Contract(A.ur, UR_ABI, wallet);
const abi = ethers.AbiCoder.defaultAbiCoder();
const ROUTER_THIS = "0x0000000000000000000000000000000000000002";
// 3-hop path — NVDA-paired token shown (NVDA's USDG pool is 10000/200; each stock differs)
const stock = "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC";
const path = [
[A.usdg, 200, 4, ethers.ZeroAddress, "0x"],
[stock, 10000, 200, ethers.ZeroAddress, "0x"],
[token, 10000, 200, ethers.ZeroAddress, "0x"],
];
// NOTE the empty [] = minHopPriceX36 — fork field!
const swap = abi.encode(
["tuple(address,tuple(address,uint24,int24,address,bytes)[],uint256[],uint128,uint128)"],
[[A.weth, path, [], ethIn, minOut]]
);
const params = [
swap,
abi.encode(["address","uint256","bool"], [A.weth, 0n, false]), // SETTLE: router pays its wrapped WETH
abi.encode(["address","uint256"], [token, minOut]), // TAKE_ALL: tokens → you, enforces minOut
];
const actions = "0x070b0f"; // SWAP_EXACT_IN, SETTLE, TAKE_ALL
const inputs = [
abi.encode(["address","uint256"], [ROUTER_THIS, ethIn]), // WRAP_ETH — WETH stays in router
abi.encode(["bytes","bytes[]"], [actions, params]),
];
const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);
await (await ur.execute("0x0b10", inputs, deadline, { value: ethIn })).wait();
Sell: two one-time approvals — ERC-20 approve(permit2, max), then Permit2 approve(token, router, 2¹⁶⁰−1, 2⁴⁸−1). Commands 0x100c = V4_SWAP then UNWRAP_WETH; actions 0x070c0e: SWAP_EXACT_IN (reversed path) → SETTLE_ALL(token, amountIn) pulls your tokens via Permit2 → TAKE(weth, ADDRESS_THIS, 0) leaves WETH in the router, and UNWRAP_WETH(MSG_SENDER = 0x…01, minOut) pays you native ETH.
Quoting & slippage
V3 (Standard): 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
V4 (RWA): use the on-chain V4 Quoter at 0xe8393F168D54B622218Adc36037008f254404C40 — exact, includes every hop's fees, and never touches funds.
const Q_ABI = [
"function quoteExactInput((address exactCurrency,tuple(address,uint24,int24,address,bytes)[] path,uint128 exactAmount) p) returns (uint256 amountOut, uint256 gasEstimate)",
];
const quoter = new ethers.Contract("0xe8393F168D54B622218Adc36037008f254404C40", Q_ABI, provider);
const [out] = await quoter.quoteExactInput.staticCall([A.weth, path, ethIn]); // same path as the buy
const minOut = out * 95n / 100n;
V4TooLittleReceived — raise slippage or shrink the trade.Collect fees
Permissionless — anyone can trigger it; fees route to the configured receivers regardless of caller. Each slot has its own locker (the hub has no locker() getter — it's per slot):
| Tokens | Locker | Creator share |
|---|---|---|
| Legacy factory | 0x904dCCB96d877E6db365282251Fa3dD156476660 | 35% of the WETH side |
| Standard (slot 1) | 0x7fABf1aE9a26A1Ac15836b892f8bC2de96E0b8F5 | 35% of the WETH side |
| RWA (slot 3) | 0x90B667f32a9f61CeaD06F194fd0165cf620efA24 | 35% of the quote (stock) side |
const LOCKER_ABI = ["function collect(uint256 tokenId)"];
const locker = new ethers.Contract("<locker for this token's slot>", LOCKER_ABI, wallet);
await (await locker.collect(positionId)).wait();
Preview pending fees (V3 tokens, 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
V4 positions have no equivalent staticCall preview — pending fees must be computed from PoolManager storage (feeGrowthInside deltas via extsload). The bow.fun dev-profile API does this for you and shows exact claimable amounts.
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 (V3; RWA: graduationQuote() in quote units)
];
const t = new ethers.Contract(token, TOKEN_ABI, provider);
console.log("graduated:", await t.migrated()); // works on every token version — one-way latch
// list all hub launches (all 3 modes)
const LIST_ABI = [
"function launchCount() view returns (uint256)",
"function getLaunch(uint256 launchId) view returns (address token, address pool, bytes32 poolId, uint256 positionId, address deployer, uint8 slotId, uint8 version)",
"function deployerOf(address token) view returns (address)",
];
const f = new ethers.Contract(A.hub, LIST_ABI, provider);
const n = Number(await f.launchCount());
for (let i = n - 1; i >= 0; i--) console.log(await f.getLaunch(i));
// stream new launches over WebSocket (topic0 of the hub's Launched event)
const TOPIC = "0x65f174315961cf8b1c0d0763569c6c8746f20dc81df9151164bec284e6ed9f01";
// Launched(address idx token, address idx deployer, uint8 idx version, uint8 slotId,
// address pool, bytes32 poolId, uint256 positionId, uint256 launchId)
// wsProvider.on({ address: A.hub, topics: [TOPIC] }, log => { ... })
V4 pool state (price/tick) is read from the PoolManager singleton with extsload: the pool's state root is keccak256(abi.encode(poolId, 6)); word 0 packs sqrtPriceX96 (bits 0–159) and tick (bits 160–183). V4 Swap events are emitted by the PoolManager itself, keyed by bytes32 poolId — filter on your token's poolId from the Launched event.
Gotchas
- The Universal Router is a fork — its V4 swap structs carry an extra
minHopPriceX36field. Hand-encode as shown in Trade V4 tokens; the stock Uniswap SDK reverts. msg.valuemust match exactly —launchFee + buyEth, or the launch reverts withEthMismatch. Do the math inBigInt, never floats.- The hub has no
predictToken— read the token address from theLaunchedevent in your receipt (the frontend still mines…b03via the TokenCodeProvider bytecode). - V4
Swapamounts are swapper deltas — negative means the swapper paid that currency into the pool (the opposite of the V3 pool-view convention). - USDG has 6 decimals — and RWA stock tokens vary; always read
decimals(), never assume 18. - 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(V3) orV4TooLittleReceived(V4) — raiseminOut. - Immutable token — no owner, mint, tax, pause, or blacklist.
setPool/setMetadataare one-time launchpad-only calls in the launch tx.
