Build on B420.
Every launch starts on a bonding curve and graduates into permanently locked Uniswap v4 liquidity. Everything on this page is callable without an account: the contracts take a signed transaction from any wallet, the REST API needs no key, and every launch is indexed onto the site automatically. Base mainnet, chain id 8453.
Launch on a curve, graduate into locked liquidity.
B420 is a launch protocol on Base (chain id 8453). A creator deploys a token, the public buys it off a bonding curve, and when the curve completes the launch automatically graduates into a permanently locked Uniswap v4 pool. The protocol is non-custodial: every transaction is signed by your own wallet, and no B420 contract ever holds a balance it can redirect.
The entire supply sits on the curve from the moment of creation. There is no presale, no team allocation, and nothing that exists before the curve opens, which removes launch-day sniping at the root; what remains of the opening race is priced by a fading 99% snipe tax (section 06). Launches can be quoted in ETH, USDC, a Coinbase wrapped asset or a tokenized stock (section 05), and the chosen quote asset is the currency for buying, selling and creator payouts alike. The token itself is a plain fixed-supply ERC-20 and is identical before and after graduation; balances never change at graduation.
Four stages, no alternative paths.
Create. The creator sets a name, symbol, image, description and socials (all written immutably onto the token contract), picks the quote asset, optionally adds a creator tax, and pays the launch fee. Supply is fixed at 1,000,000,000 tokens (18 decimals), minted in full to the launch's own curve contract; the creator receives nothing except what they buy.
Trade the curve. Anyone buys or sells against the curve at a deterministic price. Selling is always possible while the curve is open: the curve is the counterparty, so no other trader is needed.
Graduate. The instant a buy takes the last of the sellable allocation, the curve closes and graduation executes inside that same transaction. If that automatic step ever fails (for example the buyer starved it of gas), graduation is permissionless: anyone can complete it, and the platform runs a keeper that retries it within seconds. Neither the creator nor B420 is required.
Pool. The raised quote and the reserved token share seed a full-range Uniswap v4 position at exactly the curve's final price. The position is transferred to a locker contract with no withdrawal function of any kind (section 11).
A vending machine that buys back.
The curve is a constant-product market (x · y = k) holding the whole supply from creation, always willing to sell tokens for the quote asset and buy them back. Price does not start at zero: each launch opens at a set starting price, implemented as a virtual quote reserve (the phantom reserve) equal to 0.4× the graduation threshold. Holding that ratio fixed gives every launch the same shape: 71.43% of supply is sellable on the curve, 28.57% is reserved for the graduated pool from the first block, and the price rises about 12.25× between the first buy and graduation.
Fees are always charged in the quote asset, never in the launch token: buys pay on the amount spent, sells on the amount received (section 07). Large orders get worse average execution than the quoted spot, exactly like any constant-product pool.
minTokensOut) is enforced as a rate, not a quantity, so a clamped fill that honors your price-per-token still succeeds.The reserved share cannot be sold through by anyone; its exhaustion IS graduation. Because the reserved amount and threshold are fixed at creation, every launch on the same settings graduates into the same-size pool at the same price, regardless of how many wallets bought or in what sizes.
The curve ends where the pool begins.
Graduation triggers when the sellable allocation is bought out, which by construction is the same moment the curve holds its graduation threshold in real quote (section 05 lists the thresholds). It proceeds in two steps so a failed pool seed can never strand funds: first the curve's reserves are swept into the factory (automatic, inside the crossing buy), then the v4 pool is created and its full-range position minted straight into the locker. The second step is permissionless and retryable; the platform keeper usually completes it within one block of the sweep.
The pool opens at the curve's exact final price: no gap, no repricing, nothing to arbitrage at listing. Between the sweep and the pool creation, trading is briefly unavailable in both venues; the token page shows this as a graduation interlude.
Thirteen quotes, one dollar target.
A launch is denominated in one protocol-approved quote asset: it is the trading currency, the graduation-target denomination and the creator payout currency, with no conversion anywhere. Only the protocol can approve a quote asset, which prevents fake pricing against worthless tokens; approval is not an endorsement. Every quote targets the same dollar raise (currently $8,090): the launch form computes the threshold from the quote's live price at the moment you launch, shows it, and writes it into the launch you sign; the contract derives the 0.4x phantom from it and checks it against a wide per-quote sanity band (pairTokenEconomics(quote)), so a broken price source can never produce a degenerate launch. Each launch's threshold is frozen for its life.
| Quote | Address | Decimals | Family | Fee split (creator / protocol / treasury) |
|---|---|---|---|---|
| ETH (WETH) | 0x4200000000000000000000000000000000000006 | 18 | canonical | 50 / 50 |
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | stablecoin | 50 / 50 |
| cbBTC | 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf | 8 | Coinbase wrapped | 50 / 50 |
| cbXRP | 0xcb585250f852C6c6bf90434AB21A00f02833a4af | 6 | Coinbase wrapped | 50 / 50 |
| cbDOGE | 0xcbD06E5A2B0C65597161de254AA074E489dEb510 | 8 | Coinbase wrapped | 50 / 50 |
| cbADA | 0xcbADA732173e39521CDBE8bf59a6Dc85A9fc7b8c | 6 | Coinbase wrapped | 50 / 50 |
| cbLTC | 0xcb17C9Db87B595717C857a08468793f5bAb6445F | 8 | Coinbase wrapped | 50 / 50 |
| cbZEC | 0xb2000000000000000000008501b13360000cb2ec | 8 | B20 wrap | 50 / 50 |
| cbHYPE | 0xb200000000000000000000451d033a5000cb479e | 18 | B20 wrap | 50 / 50 |
| NVDAc | 0xb20000000000000000000078ee7ce2fE4908108C | 8 | tokenized stock | 50 / 30 / 20 |
| AAPLc | 0xb200000000000000000000C2e324d24d7eEcd1fb | 8 | tokenized stock | 50 / 30 / 20 |
| GOOGLc | 0xb2000000000000000000002D0BA3164cc74f58B7 | 8 | tokenized stock | 50 / 30 / 20 |
| METAc | 0xb2000000000000000000008bC8786B856E61707C | 8 | tokenized stock | 50 / 30 / 20 |
At current prices that is about 4.2 ETH, 8,090 USDC or 0.13 cbBTC; the exact figure for your launch is on the form. B420 itself is deliberately not a quote asset. The tokenized-stock set can grow through the onchain registry; a newly listed stock becomes a quote after protocol approval.
Sniping funds the launch, not the sniper.
Buys landing in the opening seconds of a launch pay a tax that starts at 99% of the quote leg and decays exponentially to zero across the 3-second launch window: about 6.2% after one second, 0.2% after two, zero from three. It applies to buys only, sells are never taxed, and it cannot be disabled. The proceeds join the launch's ordinary fee split (section 07), so a sniper's spend flows to the creator and the protocol instead of buying tokens. A taxed buy always nets the buyer at least 1% of their spend. Terms are frozen per launch at creation; retuning the window never touches an open launch.
Exemptions. The launching wallet and the creator fee recipient are exempt automatically, and the creator can declare up to 32 wallets in total at launch for team buys bundled across addresses. The atomic first buy through the router (section 14) settles inside the launch transaction itself and is exempt, so a creator's opening position never pays the tax.
currentSnipeTaxBps(recipient) and fold it into buy quotes during the window (section 16); raw curve math overstates output while the window is open. It returns 0 afterwards.Every rate, stated plainly.
| Fee | Rate | When | Goes to |
|---|---|---|---|
| Launch fee | 0.0005 ETH | once, at creation | protocol |
| Base trade fee | 1% of the quote leg | every curve trade and every pool swap | split: creator 50% / protocol 50% (stock quotes: creator 50% / protocol 30% / treasury 20%) |
| Creator tax | 0 to 10%, creator-chosen at launch | same trades as the base fee | creator, in full |
| Snipe tax | 99% fading to 0 over 3s | buys in the launch window | joins the base-fee split |
| Pool fee tier | 0 | graduated pool | the v4 pool itself charges nothing; the hook charges the 1% instead |
The creator tax is layered on top of the base fee, paid entirely to the creator, and immutable from the moment of launch: it can never be raised, lowered or removed by anyone, the protocol included. The combined base fee plus tax is hard-capped at 20% onchain (in practice 1% + at most 10%). Fee terms are snapshotted per launch, so a protocol policy change never touches a token already launched; a creator can pin the exact terms they were quoted with expectedEconomics (section 14).
On the curve, fees are always charged in the quote asset. On the graduated pool a swap's fee can land in either currency, and delivery follows the launch's routing: a creator is paid in kind, in both currencies, exactly as the legacy launchpad delivered fees; a launch whose fees stream to its holder distributor converts the token side to the quote automatically inside each swap, so the holders' stream is a single clean currency. The protocol's share feeds the same staking flywheel as the rest of the platform (section 22), and the treasury leg on stock quotes goes to the B420 treasury.
Fees accrue, you pull.
Nothing is pushed to wallets. Trade fees accrue on the curve (and, after graduation, on the hook), are swept into a shared escrow, and sit there as a claimable balance until the recipient withdraws. A creator is paid in kind: the quote asset for every curve trade, and both currencies for pool swaps, with no conversion anywhere. One escrow serves every launch, so a creator with five tokens claims each currency once, not per token.
A sweep in the default mode is a plain distribution with no market operation in it, so the creator can always run it themselves, straight from the contract, with no dependency on B420 infrastructure of any kind. The protocol and treasury legs are paid directly at sweep time; only the creator leg goes through the escrow. Claiming is claimToken(asset) from the recipient's own wallet (section 18).
A five-year commitment signal.
A creator can opt their launch into buyback-and-lock, funded entirely from the creator's own share of the base fee, never from traders or the protocol. While enabled, half of the creator's base-fee slice market-buys the token (on the curve before graduation, on the pool after) and locks it in a shared vault that releases linearly over five years, the released amount split between the creator and the protocol at the launch's frozen fee split. Bought-back tokens are not burned.
The five-year clock is weighted: each new buyback shifts the vesting start forward in proportion to its size, so a large fresh buyback cannot ride an old tranche's progress to instant liquidity. Either party can trigger a release; one call pays both sides. When a buyback cannot execute sensibly (thin curve, more than 3% price impact), it is skipped and that money reaches the creator as ordinary fee income instead; skipping is reported onchain.
Two levers, nothing else.
After launch, a creator can do exactly two things:
1. Redirect fees. transferCreatorFeeRecipient(token, newRecipient) moves future earnings (and the buyback vest beneficiary with them) to a new wallet, effective immediately. Only the current recipient can call it. One planned exception is permanent by design: once a launch's recipient is its registered holder fee distributor (the no-staking dividend system, shipping separately), the routing can never be moved again by anyone, the protocol owner included; holders can rely on the stream for the life of the token.
2. Toggle buybacks. Only the creator can turn buybacks on (it spends their money); the protocol can turn them off but never on. Toggling affects future fees only.
Everything else is immutable or absent: supply, pricing, quote asset, tax rates and graduation terms are frozen at launch; there is no mint, no freeze, no blacklist, no post-launch tax, and no access to locked liquidity for anyone.
Locked means locked.
Graduated liquidity is permanently locked: the locker holds the v4 position and exposes no withdrawal, no unlock schedule and no privileged path. The reserved-supply remainder that cannot enter the pool without moving its price is locked in the same contract. Tokens sent directly to a contract address by mistake are unrecoverable.
Graduation's two-step design bounds the worst case: if a launch gets stuck between the sweep and the pool seed (in practice, a quote asset that stopped transferring exactly), the protocol can return the collected funds, but only after seven full days during which anyone may still complete the seed permissionlessly, and a rescued launch is permanently marked as such onchain. Fee escrow balances are per-recipient, so one recipient's failure to receive can never block anyone else's claim.
Read this before trading.
Launch tokens are volatile and can lose all value. Anyone can create a launch with any name, symbol and image, including deliberate imitations; names are neither unique nor verified. In particular:
1. The token address is the only identifier that cannot be copied. Verify it.
2. A launch may carry a creator tax up to 10% on every trade. Read it on the token page before trading.
3. Buys in the first seconds of a launch pay the snipe tax, up to 99%.
4. Graduation is not a quality signal; it only means the curve sold out.
5. A launch quoted in a volatile asset carries that asset's risk on top of its own.
6. Transactions are signed by your wallet and are generally irreversible.
The full legal terms, including the platform fee schedule, are in the Terms of Use.
The curve suite, Base mainnet.
Per-launch token and curve addresses must be resolved from the factory (getLaunchedToken), never hardcoded. The suite:
| Contract | Address | Role |
|---|---|---|
| B420CurveFactory | 0x292Bc01D6Ad381Ede5474Fa51286067522B9C3a7 | deploys launches, holds per-quote config, drives graduation |
| B420LaunchAndBuy | 0xdAF4c31Bdf7945a5412f1df58F8e8b4C83aEc407 | optional router: create the launch and buy atomically |
| B420MemeHook | 0x0138920ce01045abddB9F20A526695313b42a044 | singleton v4 hook; accrues and splits post-graduation fees |
| B420FeeEscrow | published at go-live | claimable creator balances, every currency, one contract |
| B420BuybackVault | published at go-live | bought-back supply; linear five-year weighted vest |
| B420LaunchLocker | published at go-live | permanently holds graduated positions and excess supply |
| B420LaunchDeployer | published at go-live | CREATE2 deploys of each curve and token pair |
| B420GraduationExecutor | published at go-live | mints the full-range v4 position into the locker |
| B420GraduationGuard | published at go-live | preflights that reserves can seed a valid v4 pool |
Addresses marked "published at go-live" land here, on Basescan (verified source), and in the escrow/vault getters of the factory the moment the stack deploys. The legacy instant-liquidity suite is listed in section 23.
One transaction, curve and first buy.
The core entry point is launchToken(params, launchConfigId, pairToken, snipeTaxExemptions) on the factory (value = the launch fee). For a launch with an opening buy, use the router's launchAndBuy instead: it creates the curve and settles your buy in the same transaction, so no intermediate state exists for anyone to trade against and the buy is snipe-exempt. Config id 0 is the standard 1B-supply, 1%-fee launch.
Before sending, a well-behaved integrator reads, in order:
1. canLaunch(you): launching may be closed or allowlisted.
2. launchFee(): sent as the transaction value.
3. approvedPairTokens(quote) and pairTokenEconomics(quote): the quote must be approved, and your threshold must sit inside its {minThreshold, maxThreshold} sanity band; format amounts with the declared decimals, never assume 18.
3b. Compute graduationThreshold yourself: the platform's dollar target divided by the quote's live price, in the quote's own units. It goes in TokenParams and is frozen for the launch's life.
4. maxCreatorTaxBps(): your tax must be at or under the cap.
5. previewLaunchEconomics(configId, quote): pin the result in expectedEconomics so a config change mid-flight reverts your launch instead of silently repricing it. Pass bytes32(0) to waive.
6. A fresh random salt: it namespaces your CREATE2 addresses per wallet and supports vanity mining.
import { parseAbi } from "viem";
const FACTORY_ABI = parseAbi([
"struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
"struct TokenParams { string name; string symbol; string logo; string description; Socials socials; address creatorFeeRecipient; uint16 creatorTaxBps; bool buybackEnabled; bytes32 expectedEconomics; bytes32 salt; }",
"function canLaunch(address launcher) view returns (bool)",
"function launchFee() view returns (uint256)",
"function maxCreatorTaxBps() view returns (uint256)",
"function approvedPairTokens(address) view returns (bool)",
"function pairTokenEconomics(address) view returns (uint256 phantomQuote, uint256 graduationThreshold, uint8 decimals)",
"function previewLaunchEconomics(uint256 launchConfigId, address pairToken) view returns (bytes32)",
"function launchToken(TokenParams params, uint256 launchConfigId, address pairToken, address[] snipeTaxExemptions) payable returns (address token, address curve)",
]);
const ROUTER_ABI = parseAbi([
"struct Socials { string twitter; string telegram; string discord; string website; string farcaster; }",
"struct TokenParams { string name; string symbol; string logo; string description; Socials socials; address creatorFeeRecipient; uint16 creatorTaxBps; bool buybackEnabled; bytes32 expectedEconomics; bytes32 salt; }",
"function launchAndBuy(TokenParams params, uint256 launchConfigId, address pairToken, uint256 quoteIn, uint256 minTokensOut, address recipient, address[] snipeTaxExemptions) payable returns (address token, address curve, uint256 tokensOut)",
]);
// 1. the checklist reads (see the flow above)
const [ok, fee, econ] = await Promise.all([
pub.readContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "canLaunch", args: [account.address] }),
pub.readContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "launchFee" }),
pub.readContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "previewLaunchEconomics", args: [0n, WETH] }),
]);
if (!ok) throw new Error("launching is closed for this wallet right now");
// 2. approve the router for the first buy (ERC-20 quote; wrap ETH to WETH first)
await wallet.writeContract({ address: WETH, abi: erc20Abi, functionName: "approve", args: [ROUTER, quoteIn] });
// 3. one transaction: create the curve AND settle your first buy inside it
const params = {
name: "My Token", symbol: "MTK", logo: "ipfs://...", description: "...",
socials: { twitter: "", telegram: "", discord: "", website: "", farcaster: "" },
creatorFeeRecipient: account.address,
creatorTaxBps: 100, // 1% creator tax on top of the base fee, forever
buybackEnabled: false,
graduationThreshold: thresholdUnits, // target USD / live quote price, your figure
expectedEconomics: econ, // pins the terms you were quoted
salt: crypto.getRandomValues(new Uint8Array(32)), // unique per wallet
};
const hash = await wallet.writeContract({
address: ROUTER, abi: ROUTER_ABI, functionName: "launchAndBuy",
args: [params, 0n, WETH, quoteIn, minTokensOut, account.address, []],
value: fee, // ERC-20 quote: value is the launch fee only
});Straight to the curve, no router.
Pre-graduation trades call the curve contract directly. Approve the quote asset for a buy (or the token for a sell), then call buy or sell with your slippage bound. After graduation both revert CurveGraduated; route to the v4 pool instead (section 20).
const CURVE_ABI = parseAbi([
"function buy(uint256 quoteIn, uint256 minTokensOut, address recipient) payable returns (uint256 tokensOut)",
"function sell(uint256 tokensIn, uint256 minQuoteOut, address recipient) returns (uint256 quoteOut)",
"function pairToken() view returns (address)",
"function graduated() view returns (bool)",
"function readyToGraduate() view returns (bool)",
]);
// BUY: approve the curve for the quote asset, then buy. No router involved.
await wallet.writeContract({ address: quote, abi: erc20Abi, functionName: "approve", args: [curve, quoteIn] });
const tokensOut = await wallet.writeContract({
address: curve, abi: CURVE_ABI, functionName: "buy", args: [quoteIn, minTokensOut, account.address],
});
// Near graduation an oversized buy is filled to what remains, priced for what
// was actually received, and the difference refunded in the SAME transaction.
// Always read the real tokensOut from the return value or the CurveBuy event.
// SELL: approve the curve for the token, then sell.
await wallet.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [curve, tokensIn] });
await wallet.writeContract({
address: curve, abi: CURVE_ABI, functionName: "sell", args: [tokensIn, minQuoteOut, account.address],
});No quoter: the price is deterministic.
The curve exposes no quote function because none is needed: pricing is pure constant-product math over public reads. The one rule that trips integrators is the fee order: buys deduct every fee from the input before pricing; sells price first and deduct fees from the gross output. Quoting a sell as a mirrored buy overstates proceeds.
// The curve has no quote function: pricing is deterministic, reproduce it.
const [quoteReserve, tokenReserve] = await pub.readContract({ address: curve, abi: CURVE_ABI, functionName: "getReserves" });
const [sellable, feeBps, taxBps, snipeBps] = await Promise.all([
pub.readContract({ address: curve, abi: CURVE_ABI, functionName: "sellableTokens" }),
pub.readContract({ address: curve, abi: CURVE_ABI, functionName: "feeBps" }),
pub.readContract({ address: curve, abi: CURVE_ABI, functionName: "creatorTaxBps" }),
pub.readContract({ address: curve, abi: CURVE_ABI, functionName: "currentSnipeTaxBps", args: [recipient] }),
]);
const out = (a, rIn, rOut) => (a * rOut) / (rIn + a); // constant product
// BUY: deduct every fee from the input FIRST, then price the remainder.
const cut = (spend * (feeBps + taxBps + snipeBps)) / 10_000n;
let tokensOut = out(spend - cut, quoteReserve, tokenReserve);
if (tokensOut > sellable) tokensOut = sellable; // clamped fill
// SELL: price through the curve FIRST, then deduct fees from the output.
const gross = out(tokensIn, tokenReserve, quoteReserve);
const quoteOut = gross - (gross * (feeBps + taxBps)) / 10_000n; // no snipe tax on sells
// Use getReserves() for pricing (it includes the virtual reserve and excludes
// pending fees). realQuoteReserve() is for graduation progress, not pricing.sellableTokens() reaches zero, and sells as soon as readyToGraduate() is true, even while graduated() is still false; route to the pool after graduation.One registry read, then the curve.
const rec = await pub.readContract({
address: FACTORY, abi: FACTORY_ABI, functionName: "getLaunchedToken", args: [token],
});
// rec: { token, curve, deployer, creatorFeeRecipient, pairToken,
// graduationThreshold, poolFee, tickSpacing, creatorTaxBps,
// buybackEnabled, phase, sweptQuote, sweptTokens, sweptAt, exists }
// phase: 0 NotGraduated (curve trading) - 1 Swept (pool pending)
// 2 PoolCreated (v4 trading) - 3 Rescued
// Display math
const [qr, tr] = await pub.readContract({ address: rec.curve, abi: CURVE_ABI, functionName: "getReserves" });
const marginalPrice = Number(qr) / Number(tr); // quote per token
const raised = await pub.readContract({ address: rec.curve, abi: CURVE_ABI, functionName: "realQuoteReserve" });
const progress = Number(raised) / Number(rec.graduationThreshold); // 0 -> 1
// Metadata lives ON the token contract, immutably:
// name(), symbol(), logo(), description(), socials(), getTokenInfo()Fee terms per launch: feeBps(), creatorTaxBps() and buybackEnabled() on the curve, and getLaunchFeePolicy(token) on the factory for the frozen split, including the treasury leg on stock quotes. Buyback vest: totalLocked, totalReleased, releasable and vestedAmount on the vault, all keyed by token. Pending recipient recovery: pendingCreatorFeeRecipient(token), zeros when nothing is pending.
Sweep, then claim.
Total owed to a creator = escrow balance (already swept) + unswept accruals on the curve or hook. Default-mode sweeps are callable by the creator and are never blocked: they contain no swap. The one exception is a sweep that would run a buyback (a real market buy), which only the designated sweep operator may execute with an explicit minimum; the same applies to the batch-conversion fallback of holder-routed pools, whose ordinary conversions happen inline per swap without anyone's involvement.
const ESCROW_ABI = parseAbi([
"function balanceOfToken(address recipient, address token) view returns (uint256)",
"function claimToken(address token) returns (uint256)",
]);
const CURVE_SWEEP_ABI = parseAbi([
"function quoteFeeBalance() view returns (uint256)",
"function creatorTaxBalance() view returns (uint256)",
"function sweepFees(uint256 minBuybackTokensOut)",
]);
// What is already claimable (swept into the escrow), per currency:
const owed = await pub.readContract({
address: ESCROW, abi: ESCROW_ABI, functionName: "balanceOfToken", args: [creator, quote],
});
// Plus what is still accrued on the curve, awaiting a sweep:
const pending = await pub.readContract({ address: curve, abi: CURVE_SWEEP_ABI, functionName: "quoteFeeBalance" });
// The creator (or the protocol's sweep operator) sweeps; with buybacks off,
// minBuybackTokensOut is 0. Then the claim pays the caller's own balance:
await wallet.writeContract({ address: curve, abi: CURVE_SWEEP_ABI, functionName: "sweepFees", args: [0n] });
await wallet.writeContract({ address: ESCROW, abi: ESCROW_ABI, functionName: "claimToken", args: [quote] });Everything an indexer needs.
| Event | Contract | Signals |
|---|---|---|
| TokenLaunched | factory | every launch: token, curve, deployer indexed; pairToken, configId, threshold |
| CurveBuy / CurveSell | curve | each trade with quote and token amounts; base fee and creator tax reported separately |
| SnipeTaxCharged | curve | a launch-window buy paid the snipe tax (amount included) |
| CurveBuyRefunded | curve | a clamped final buy returned unspent quote |
| LaunchSwept / CurveCompleted | factory / curve | the curve closed; reserves handed to the factory (phase 1) |
| PoolGraduated | factory | the v4 pool exists and its position is locked (phase 2) |
| AutoGraduationFailed | curve | the in-buy graduation ran out of gas; retry permissionlessly |
| FeesSwept / PoolFeesSwept | curve / hook | a distribution: protocol, treasury, buyback and creator amounts |
| HookFeeConverted / InlineConversionSkipped | hook | a holder-routed pool's per-swap fee conversion (or its harmless retry-later skip) |
| CreditedToken / ClaimedToken | escrow | escrow accruals and withdrawals per currency |
| BuybackLocked / Locked / Released | curve, hook / vault | buyback executed; tokens entered the vest; vested tokens paid out |
| CreatorFeeRecipientUpdated / ...ChangeProposed | factory | payout redirects and timelocked recovery proposals |
| PoolConversionSkipped / PoolBuybackSkipped | hook | a sweep declined a bad price rather than accept it |
Zero-fee pools, the hook charges instead.
A graduated pool's key: sort the launch token and its quote by address for currency0/currency1, fee: 0, tickSpacing: 60, hooks: the B420MemeHook singleton; poolId = keccak256(abi.encode(poolKey)). The zero fee tier is intentional: the pool's core charges nothing and the hook takes the 1% base fee plus the launch's creator tax on each swap's unspecified currency, accounting it per pool for the sweep.
The hook enables exactly two callbacks: before-initialize (only the B420 factory can attach a pool to it) and after-swap (the fee take). Everything else reverts. Liquidity is one full-range position minted at graduation and held by the locker forever; aggregators generally do not index custom-hook pools, so to trade onchain describe the pool yourself through the Universal Router's V4_SWAP, quoting via the v4 Quoter with this exact key.
What a revert is telling you.
| Error | Cause |
|---|---|
| SlippageExceeded | your minimum output cannot be met at the available price |
| CurveGraduated | the curve is finished; route the trade to the v4 pool |
| NotReadyToGraduate | graduation called before the curve sold out |
| LaunchEconomicsMismatch | your pinned economics are stale; re-read the preview and retry |
| PairTokenNotApproved | that quote asset is not enabled for launches |
| PairTokenDecimalsMismatch | the quote asset reports different decimals than recorded |
| LaunchFeeNotPaid | transaction value does not equal launchFee() |
| CreatorTaxTooHigh | tax above maxCreatorTaxBps() (10%) |
| NotWhitelisted | public launching is closed and the wallet is not allowlisted |
| ExemptionListTooLong | more than 32 snipe-tax exemptions |
| LaunchConfigDisabled | the chosen launch config is switched off |
| NativeValueMismatch / UnexpectedNativeValue | ETH attached where the quote is an ERC-20 (approve and send zero value) |
| NotCreatorFeeRecipient | a fee redirect from a wallet that is not the current recipient |
| RecipientLockedToDistributor | fees stream to the holder distributor; the routing is permanent |
| InternalSwapRequiresOperator | this sweep needs a price-sensitive swap; only the operator may run it |
| TimelockNotElapsed / TimelockExpired | a recovery proposal executed too early or too late |
What B420 is onchain.
B420 is a launchpad and trading venue on Base built around B20, Base's native token standard. The B420Factory mints a B20 through the Base precompile in a single transaction: fixed supply, no token admin, and a deterministic address that the factory requires to end in b420 (the salt is mined off-chain, the suffix is enforced at mint). Each launch opens a Uniswap v4 pool with a static 1% fee on each side, and its liquidity is locked in the LP locker forever.
Trading fees split 50/50 between the creator and the protocol; the factory rejects any other layout. The creator half accrues in the ClankerFeeLocker and can optionally route a slice to a per-token DividendVault, where holders stake the token and earn the pool's quote currency. The protocol half feeds the flywheel: tokenized-stock fees are forwarded in kind to StakingB420, and accumulated WETH is converted by swapAndFund and split 55/25/20 between the stock basket for B420 stakers, a B420 buyback streamed to B69 stakers, and the treasury.
Deployed addresses, Base mainnet.
All contracts are deployed and verified on Base (chain id 8453). Addresses link to Basescan; the button copies.
| Contract | Address | Role |
|---|---|---|
| B420Factory | 0x760AFca74b37B7D8a5a2b062eCB9DBDC3f0018fE | deployToken; enforced 50/50 fee split; b420 vanity seal |
| ClankerLpLockerFeeConversion | 0x0c0b04d8bd761da1899b1a13cd3353d0974f99d3 | LP locker; tokenRewards(token) stores each launch's fee split |
| ClankerFeeLocker | 0x20835181fD6F4e62AA8d630A89b0e5c8676808C6 | accrued fees per fee owner; claim(feeOwner, token) is permissionless |
| B420FeeCollector | 0xB9366B662b610F730a50408Db17E550d64F06F44 | protocol slice recipient; forward and swapAndFund keeper calls |
| B420DividendVaultFactory | 0xa0e85c7e3866c3bdc20cc2bee78e04fdf214583a | create(meme, rewardToken, cooldown, owner) deploys a holder vault |
| B420StockRegistry | 0x5e4643c2f48c14e09f209cae5a51455211e10c1e | governed set of tokenized stocks accepted as deep quotes |
| StakingB420 | 0xC411bA66d1819054f67cDE26424cd876DB703E79 | stake B420, earn Coinbase tokenized stocks |
| StakingB69 | 0x82E6b3CEE079432F31D64855ed3DD5faCA71d309 | stake B69, earn B420 |
Tokens
| Token | Address | Notes |
|---|---|---|
| B420 | 0xb200000000000000000000231d6c1f1ce455ba32 | 18 decimals, fixed supply 69 |
| B69 | 0xb2000000000000000000007594fe5acd56df3937 | 18 decimals, fixed supply 420 |
| WETH | 0x4200000000000000000000000000000000000006 | 18 decimals, default launch quote |
| NVDAc | 0xb20000000000000000000078ee7ce2fE4908108C | 8 decimals, Coinbase tokenized stock |
| AAPLc | 0xb200000000000000000000C2e324d24d7eEcd1fb | 8 decimals, Coinbase tokenized stock |
| GOOGLc | 0xb2000000000000000000002D0BA3164cc74f58B7 | 8 decimals, Coinbase tokenized stock |
| METAc | 0xb2000000000000000000008bC8786B856E61707C | 8 decimals, Coinbase tokenized stock |
Every token launched through the factory lives at an address of the form 0xB2 + 10 zero bytes + 9 hash bytes, ending in b420. The tokenized-stock set can grow through the onchain B420StockRegistry; a launch quoted in a newly registered stock works without any code change here.
Prepare over REST, sign it yourself.
Launching is a two-step pattern: POST /api/launch validates your inputs, resolves the quote token's price when it is not WETH, mines the ...b420 vanity salt and returns the complete DeploymentConfig. No keys and no signing happen server-side; your wallet signs factory.deployToken(config) and pays the gas (about $0.05 on Base). There is no launch fee and no cap.
POST https://b420.io/api/launch
Content-Type: application/json
{
"name": "My Token", // required, up to 48 chars
"symbol": "MTK", // required, up to 12 chars, uppercased
"rewardRecipient": "0xYourWallet", // required: the launching wallet
"image": "ipfs://...", // optional token image URL
"description": "...", // optional, up to 500 chars
"supply": "100000000000", // optional whole tokens (digits only), default 100 billion
"pairedTokenAddress": "0x...", // optional quote token, default WETH
"dividendBps": 2500, // optional holder dividend, see the two-tx flow
"dividendMode": "currency" // optional: the stock-share dividend mode
}
// 200 response
{
"success": true,
"factory": "0x760AFca74b37B7D8a5a2b062eCB9DBDC3f0018fE",
"chainId": 8453,
"config": { ... }, // full DeploymentConfig, bigints as strings
"predictedAddress": "0x...b420",
"startingTick": -250400,
"dividend": { ... }, // present only when a dividend was requested
"warnings": ["..."] // optional
}Standard launch, complete viem flow
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const FACTORY_ABI = parseAbi([
"struct TokenConfig { address tokenAdmin; string name; string symbol; bytes32 salt; string image; string metadata; string context; uint256 originatingChainId; uint256 supply; }",
"struct PoolConfig { address hook; address pairedToken; int24 tickIfToken0IsClanker; int24 tickSpacing; bytes poolData; }",
"struct LockerConfig { address locker; address[] rewardAdmins; address[] rewardRecipients; uint16[] rewardBps; int24[] tickLower; int24[] tickUpper; uint16[] positionBps; bytes lockerData; }",
"struct MevModuleConfig { address mevModule; bytes mevModuleData; }",
"struct ExtensionConfig { address extension; uint256 msgValue; uint16 extensionBps; bytes extensionData; }",
"struct DeploymentConfig { TokenConfig tokenConfig; PoolConfig poolConfig; LockerConfig lockerConfig; MevModuleConfig mevModuleConfig; ExtensionConfig[] extensionConfigs; }",
"function deployToken(DeploymentConfig deploymentConfig) payable returns (address tokenAddress)",
]);
const account = privateKeyToAccount("0xYourPrivateKey");
const pub = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ account, chain: base, transport: http() });
// 1. Prepare: the server validates, resolves prices and mines the ...b420 salt.
const prep = await fetch("https://b420.io/api/launch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "My Token",
symbol: "MTK",
rewardRecipient: account.address,
}),
}).then((r) => r.json());
if (!prep.success) throw new Error(prep.error);
// 2. Restore the two bigint fields the JSON transport carries as strings.
const config = {
...prep.config,
tokenConfig: {
...prep.config.tokenConfig,
originatingChainId: BigInt(prep.config.tokenConfig.originatingChainId),
supply: BigInt(prep.config.tokenConfig.supply),
},
};
// 3. Gas: estimate on a real Base node, pad 1.5x, fall back to 3M.
let gas = 3_000_000n;
try {
const est = await pub.estimateContractGas({
address: prep.factory, abi: FACTORY_ABI, functionName: "deployToken",
args: [config], account: account.address,
});
gas = (est * 15n) / 10n;
} catch {}
// 4. Sign and send. The token mints at prep.predictedAddress (ends in b420).
const hash = await wallet.writeContract({
address: prep.factory, abi: FACTORY_ABI, functionName: "deployToken",
args: [config], gas,
});
await pub.waitForTransactionReceipt({ hash });
console.log("live at", prep.predictedAddress);0xef), which third-party wallet simulators cannot execute. Without a dapp-supplied gas limit those wallets substitute their own failed estimate and refuse to send. Estimate on a real Base node, pad 1.5x, and fall back to 3,000,000. The real requirement is around 1.5M gas.Dividend launch: two transactions
A launch can dedicate a slice of the creator's own half to holders. The slice's recipient is a per-token DividendVault, so the vault must exist before the launch transaction, bound to the meme's predicted address. Transaction 1 creates the vault; then the config is rebuilt with the same salt (the predicted address does not move) and the vault as the dividend slice's recipient; transaction 2 is deployToken. In the stock-share mode (dividendMode: "currency") the vault slice is capped at 2500 bps and paid FeeIn.Paired, while the creator keeps one FeeIn.Both slice, so the fee engine never sells the creator's token. The factory rejects FeeIn.Clanker outright, and the protocol slice must always be last.
dividendBps and the vault factory refuses create. Existing vaults keep operating. The flow below is documented for them only and will be replaced by the distributor API.import { encodeAbiParameters, parseAbi } from "viem";
const VAULT_FACTORY = "0xa0e85c7e3866c3bdc20cc2bee78e04fdf214583a";
const VAULT_FACTORY_ABI = parseAbi([
"function create(address meme, address stock, uint256 cooldown, address vaultOwner) returns (address vault)",
"function vaultFor(address meme) view returns (address)",
]);
// 1. Prepare with a dividend. dividendMode "currency" is the stock-share mode:
// the vault slice (1..2500 bps of ALL trading fees) is paid in the paired
// token only (FeeIn.Paired); you keep the rest of your half as one
// FeeIn.Both slice, so the fee engine never sells your token.
const prep = await fetch("https://b420.io/api/launch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "My Token",
symbol: "MTK",
rewardRecipient: account.address,
pairedTokenAddress: "0xb20000000000000000000078ee7ce2fE4908108C", // NVDAc
dividendBps: 2500, // holder share of all fees, 1..2500 in currency mode
dividendMode: "currency",
}),
}).then((r) => r.json());
if (!prep.success || !prep.dividend) throw new Error(prep.error);
// 2. Transaction 1: create the holder vault, bound to the PREDICTED meme
// address. It must exist before deployToken because its address goes into
// the fee split.
const createHash = await wallet.writeContract({
address: prep.dividend.vaultFactory, abi: VAULT_FACTORY_ABI,
functionName: "create",
args: [prep.predictedAddress, prep.dividend.stock,
BigInt(prep.dividend.cooldown), account.address],
});
await pub.waitForTransactionReceipt({ hash: createHash });
const vault = await pub.readContract({
address: prep.dividend.vaultFactory, abi: VAULT_FACTORY_ABI,
functionName: "vaultFor", args: [prep.predictedAddress],
});
// 3. Rebuild the config with the vault as the dividend slice recipient. The
// SAME salt is reused (it is already inside config.tokenConfig), so the
// predicted address does not move. Creator slices come first, the protocol
// slice stays LAST; FeeIn.Clanker (2) is rejected by the factory.
const FeeIn = { Both: 0, Paired: 1 };
const bps = prep.dividend.bps;
const cfg = {
...config, // prep.config from THIS dividend prepare call, bigints restored
// (never reuse the standard-flow prep: its salt binds a different address)
lockerConfig: {
...config.lockerConfig,
rewardAdmins: [account.address, account.address, config.lockerConfig.rewardAdmins[1]],
rewardRecipients: [account.address, vault, config.lockerConfig.rewardRecipients[1]],
rewardBps: [5000 - bps, bps, 5000],
lockerData: encodeAbiParameters(
[{ type: "tuple", components: [{ name: "feePreference", type: "uint8[]" }] }],
[{ feePreference: [FeeIn.Both, FeeIn.Paired, FeeIn.Paired] }],
),
},
};
// 4. Transaction 2: deployToken(cfg) exactly as in the standard flow,
// including the dapp-supplied gas limit.The percent mode (dividendBps 1..5000 without dividendMode) is also accepted by the API: at 5000 the entire creator half goes to the vault as a single FeeIn.Paired slice. The vault factory is generic: on a WETH launch the vault streams WETH, on a stock pair it streams the stock.
Route through the token's own pool.
Launched tokens trade on Uniswap v4 pools with a custom static-fee hook, and aggregators generally do not index custom-hook pools: to swap onchain you describe the exact pool yourself (Universal Router V4_SWAP with the pool key, quoting through the v4 Quoter), or you route through a thin secondary pool and get a bad fill. The pool's quote currency is the one to route through: it is WETH for a standard launch, the tokenized stock for an equity-paired launch, and another B20 for meme-paired launches.
The markets endpoint tells you which: every row of GET /api/tokens carries a pairedAddr field, the address of the deepest pool's quote token. Swap token ↔ pairedAddr on the launch pool; for an equity-paired token that means two hops, ETH to the stock to the token, which the app builds automatically. Pool parameters at launch: tick spacing 200, 1% static fee per side, one locked liquidity position from the starting tick to the top of the range.
For UI trading, every token has a terminal at b420.io/terminal/<address> with charts, the live tape and one-click swaps. Browse the markets.
Vaults pay stakers, not sitters.
A DividendVault is a single-reward staking pool: stake the launched token, earn the pool's quote currency. B20 tokens have no transfer hooks, so there is no passive drip; holders earn by staking. Find a token's vault in the dividendVault field of GET /api/launches, or onchain via vaultFor(meme) on the vault factory.
import { erc20Abi, parseAbi } from "viem";
const VAULT_ABI = parseAbi([
"function stake(uint256 amount)",
"function requestUnstake(uint256 amount)",
"function withdraw()",
"function cancelUnstake()",
"function getReward()",
"function sync()",
"function earned(address account) view returns (uint256)",
"function stakedBalance(address account) view returns (uint256)",
"function unbondingAmount(address account) view returns (uint256)",
"function unbondingUnlockAt(address account) view returns (uint256)",
"function cooldownPeriod() view returns (uint256)",
"function stakingToken() view returns (address)",
"function rewardToken() view returns (address)",
]);
// Approve, then stake the meme token.
await wallet.writeContract({ address: meme, abi: erc20Abi, functionName: "approve", args: [vault, amount] });
await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "stake", args: [amount] });
// sync() is permissionless: it pulls newly accrued reward from the fee locker
// into the vault's accumulator. Anyone (or any bot) may call it.
await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "sync" });
// Rewards are claimable the moment they arrive; there is no lock on claiming.
const owed = await pub.readContract({ address: vault, abi: VAULT_ABI, functionName: "earned", args: [account.address] });
if (owed > 0n) await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "getReward" });
// Unstaking is a 48 hour unbonding: request, wait, withdraw.
await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "requestUnstake", args: [amount] });
const unlockAt = await pub.readContract({ address: vault, abi: VAULT_ABI, functionName: "unbondingUnlockAt", args: [account.address] });
// ...once unlockAt (a unix timestamp) has passed:
await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "withdraw" });
// ...or put the whole unbonding balance back to work early:
await wallet.writeContract({ address: vault, abi: VAULT_ABI, functionName: "cancelUnstake" });requestUnstake stops that amount earning immediately. A second request resets the 48 hour clock for the entire unbonding balance, not just the new amount. withdraw takes the whole unbonding balance once unlocked. cancelUnstake re-stakes all of it. Claiming rewards is never locked.Platform pools
StakingB420 (stake B420, earn Coinbase tokenized stocks) and StakingB69 (stake B69, earn B420) share one contract shape, B420MultiStaking: a multi-reward accumulator with no reward duration, the same 48 hour unbonding, and the same write functions as the vault. The difference is the reward side: earned(account, token) takes the reward token, and rewardTokens() lists the basket.
const MULTI_STAKING_ABI = parseAbi([
"function stake(uint256 amount)",
"function requestUnstake(uint256 amount)",
"function withdraw()",
"function cancelUnstake()",
"function getReward()",
"function earned(address account, address token) view returns (uint256)",
"function rewardTokens() view returns (address[])",
"function stakedBalance(address account) view returns (uint256)",
"function unbondingUnlockAt(address account) view returns (uint256)",
]);
// StakingB420 pays a basket: iterate rewardTokens() and read earned() per token.
const rewards = await pub.readContract({
address: "0xC411bA66d1819054f67cDE26424cd876DB703E79",
abi: MULTI_STAKING_ABI, functionName: "rewardTokens",
});
for (const t of rewards) {
const owed = await pub.readContract({
address: "0xC411bA66d1819054f67cDE26424cd876DB703E79",
abi: MULTI_STAKING_ABI, functionName: "earned", args: [account.address, t],
});
}Claim the creator half from the fee locker.
The creator's share of trading fees accrues in the ClankerFeeLocker under the creator's address, per currency: the launched token itself and its quote, according to each slice's FeeIn. availableFees(feeOwner, token) reads what is claimable; claim(feeOwner, token) pays it out.
import { parseAbi } from "viem";
const FEE_LOCKER = "0x20835181fD6F4e62AA8d630A89b0e5c8676808C6";
const FEE_LOCKER_ABI = parseAbi([
"function availableFees(address feeOwner, address token) view returns (uint256)",
"function claim(address feeOwner, address token)",
]);
// Fees accrue per currency: the launched token itself and its quote token,
// according to each slice's FeeIn set at launch. Read both.
const owed = await pub.readContract({
address: FEE_LOCKER, abi: FEE_LOCKER_ABI,
functionName: "availableFees", args: [creator, currency],
});
// The claim is permissionless: ANY wallet can send it, the proceeds always go
// to the fee owner (the creator). Gas is the only cost to the caller.
if (owed > 0n) {
await wallet.writeContract({
address: FEE_LOCKER, abi: FEE_LOCKER_ABI,
functionName: "claim", args: [creator, currency],
});
}The trigger is permissionless by design: a bot, a friend or the creator can send the claim, and the proceeds always go to the fee owner. On a launch that routed the entire creator half to the dividend vault (a 5000 bps dividend), no creator slice exists and there is nothing to claim here.
Public endpoints, no key required.
Base URL https://b420.io. All endpoints below are public, need no authentication, and are CDN-cached; the cache column shows the edge TTL and the stale-while-revalidate window. USD numerics from the database travel as decimal strings to avoid float rounding.
The full B20 markets list: { tokens: [...], source }. Each row has address, symbol, name, priceUsd, change24h/6h/1h/5m, vol24h, liqUsd, mcap, buys24, sells24, pairAddress (the deepest pool), pairedAddr (that pool's quote token, the currency to route a swap through), pairedStock, pairedMeme, b420 (launched through the factory), logo, spark (24 hourly closes).
The launches feed, newest first, as a bare array. limit up to 200 (default 50); creator filters one deployer. Rows: token, creator, name, symbol, image, pairedToken, pairedSymbol, supply (base units, string), startingTick, poolId, dividendVault, dividendBps, block (string), txHash, launchedAt, creatorProfile (null when unclaimed).
One token's market info: { success, info, gtPool, b420? }. info carries price, changes, volume, liquidity, mcap, links and logo; gtPool is the pool id accepted by the OHLC and trades endpoints. Returns 400 for a non-B20 address.
Profiled holders of a token (users with a public profile), biggest position first, as a bare array: { profile, amount, decimals, valueUsd, unrealizedUsd, realizedUsd, costRemainingUsd, thesis }. Numerics are strings; at most 100 rows.
One public profile: identity, wallets, merged positions, PnL rollups for all windows, trade stats, social counts, launches, recent activity and 90 days of portfolio snapshots. A bare 0x address resolves through the wallets table; an unclaimed address returns claimed: false with its onchain-derived stats. Profiles with private positions return identity only.
The PnL leaderboard as a bare array; limit up to 50, offset up to 5000, total row count in the X-Total-Count header. Rows: rank, profile, realizedUsd, unrealizedUsd, totalUsd, portfolioUsd (strings).
Live staking aggregate: { live, pools, collector } with each pool's staking token, total staked and reward-token basket, plus the collector's WETH balance, last fund time and pending fees per asset. Bigints as decimal strings.
Lifetime payout totals for both platform pools, from an incremental onchain log scan persisted server-side: { success, ready, cursorBlock, startBlock, pending, history, pools } with distributed, claimed and unclaimed amounts per reward token.
Up to 200 candles for a pool: { candles: [ts, o, h, l, c, v][] } with millisecond timestamps and USD prices. pool is the pool id from gtPool or pairAddress.
Recent trades for a pool: { trades: [{ ts, kind, priceUsd, amountToken, volumeUsd, tx, wallet }] }.
The launch prepare endpoint documented in section 03. Its GET variant is a liveness probe: { live, factory, predictedSuffix, dividendLive }.
Authenticated endpoints also exist (Privy bearer token) for editing profiles, follows and theses; they are app-internal and not part of the public API.
One package for all of it.
@b420/sdk is a TypeScript package built on viem (its only peer dependency) that wraps everything on this page: the launch flow including the two-transaction dividend path and the gas handling, vault and pool staking, creator fee claims, the permissionless keeper calls, and typed reads of the public REST API. It also exports the full address book and every ABI from the sections above.
npm install @b420/sdk
# publishing to npm is in progress; until it lands, copy the sdk/ folder from
# the platform repository or use the raw viem snippets in this pageimport {
B420_ADDRESSES, STOCK_TOKENS, // the address book from the Contracts section
prepareLaunch, deployPrepared, // standard launch: prepare then sign
launchWithDividend, // two-transaction dividend flow
vaultStake, vaultSync, claimCreatorFees, // staking + rewards
getTokens, // typed public REST reads
} from "@b420/sdk";
// Standard launch: server prepares (validation, prices, vanity salt), you sign.
const prep = await prepareLaunch({
name: "My Token", symbol: "MTK", rewardRecipient: account.address,
});
const { token } = await deployPrepared({ walletClient, publicClient, prepared: prep });
// Dividend launch (vault create, same-salt rebuild, deployToken) in one call.
// const { token, vault } = await launchWithDividend({ walletClient, publicClient,
// params: { name: "My Token", symbol: "MTK", rewardRecipient: account.address,
// pairedTokenAddress: STOCK_TOKENS.METAc,
// dividendMode: "currency", dividendBps: 1250 } });
// Stake a launched token in its dividend vault (approve handled).
await vaultStake({ walletClient, publicClient, vault, amount });
// Permissionless: pull newly accrued rewards into the vault.
await vaultSync({ walletClient, publicClient, vault });
// Claim a creator's accrued fees (anyone can trigger, proceeds to the creator).
await claimCreatorFees({ walletClient, publicClient, feeOwner: creator, token });
// Market reads over the public REST API.
const tokens = await getTokens();The package is pre-1.0: the address book and ABI exports are stable, and the high-level call signatures track the package README until the npm publish. The REST and viem flows in this page are the underlying interface and always work.
The platform is bot-friendly by design.
The whole surface an agent needs is public: read markets, launches, holders and candles from the REST API with no key, and sign transactions with any viem wallet. The prepare endpoint does the heavy lifting for launches (validation, price resolution, salt mining), so an agent's launch code is a fetch and a writeContract. Launches are indexed onto the site automatically, so an agent-launched token gets a token page, charts and a launches-feed entry with no extra work.
Everything permissionless is fair game for bots, and some of it is useful work:
const COLLECTOR = "0xB9366B662b610F730a50408Db17E550d64F06F44";
const COLLECTOR_ABI = parseAbi([
"function forward(address token)", // move pending protocol fees into the collector
"function swapAndFund()", // convert accumulated WETH and fund the pools
"function pendingOf(address token) view returns (uint256)",
"function wethBalance() view returns (uint256)",
"function lastFundAt() view returns (uint256)",
]);
// All four keeper surfaces are permissionless. A bot loop:
// 1. vault.sync() on any dividend vault with accrued fees
// 2. feeLocker.claim(creator, currency) for any creator (proceeds go to them)
// 3. collector.forward(token) when pendingOf(token) > 0
// 4. collector.swapAndFund() once enough WETH is pending (it reverts below
// the onchain threshold and inside the fund cooldown; read params first)Rate-limit courtesy. The read endpoints are edge-cached, so polling faster than the cache TTL (5s for trades, 15 to 60s for everything else) only returns the same cached body. Poll at or above the TTL, back off on errors, and reuse pairedAddr and gtPool instead of re-deriving pools. There is no API key to revoke, so good citizenship is the interface.
Where security actually stands.
The curve suite is a faithful port of a system running in production on another chain, whose original is undergoing three concurrent independent reviews. The B420 port adds three deliberate changes (the fee split values, a treasury leg on stock quotes, and direct protocol payouts into the staking flywheel), each marked in the verified source. It ships with a test suite of more than 240 tests, including full-lifecycle tests executed against the real Uniswap v4 contracts on a Base mainnet fork.
An independent audit of the B420 deployment itself has not been completed yet. Treat the system accordingly and size positions to that risk. Every contract is verified on Basescan, so the source is public and reviewable; this section will state the auditors and link the reports when they exist.
One channel, answered.
Integration questions, partnership requests, proposing a quote asset for approval, and anything else: legal@b420.io. Security researchers: please practice responsible disclosure and report vulnerabilities to the same address before any public disclosure; we respond to every credible report.