bow.fun docs
Open app →

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 / decreaseLiquidity path.

Network

FieldValue
ChainRobinhood Chain
Chain ID4663 (0x1237)
RPChttps://rpc.mainnet.chain.robinhood.com
Explorerhttps://robinhoodchain.blockscout.com
Gas tokenETH
Block-time gotcha. The 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

ContractAddressRole
LaunchFactory0xC70E510E14710Ea535CAB7b2414860aF63FEab79Deploys tokens, creates + locks the pool
Locker0x904dCCB96d877E6db365282251Fa3dD156476660Owns every position NFT forever; splits fees
BowZap0xCCA95E5442BbF175d8a1Ad136Be317fA6D55CC381-tx buy / sell helper
WETH0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73Quote asset
SwapRouter020xCaf681a66D020601342297493863E78C959E5cb2Uniswap V3 router
PositionManager0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3V3 positions
UniswapV3Factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfAV3 pools
The Locker is per-factory — fetch the current one with 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());
Dev buy goes to you, is exempt from the 2% cap, and lands before any public buyer. It's simply 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
During the opening 2% window, a buy larger than 2% of supply reverts with 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
Want WETH instead of native ETH? Call 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 casedoge or DOGE, both survive on-chain.
  • b03 is checksummed lowercase — mine against ethers.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 — raise minOut.
  • Immutable token — no owner, mint, tax, pause, or blacklist. setPool/setMetadata are one-time launchpad-only calls in the launch tx.