Developer documentation

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.

01 · Overview

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.

02 · Launch lifecycle

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).

03 · Bonding curve

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.

Partial fills near the end. A buy larger than the remaining sellable allocation is not rejected: it fills what remains, the buyer is charged only for what was actually received, and the difference comes back in the same transaction. The slippage bound (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.

04 · Graduation

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.

Sells close slightly earlier than buys. Once the curve is ready to graduate it holds exactly the pool's reserves, so it rejects sells even before the graduated flag flips: a sell then would drain the pool seed. Anyone blocked by this can trigger graduation themselves and sell into the pool in the same transaction.
05 · Quote assets

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.

QuoteAddressDecimalsFamilyFee split (creator / protocol / treasury)
ETH (WETH)0x420000000000000000000000000000000000000618canonical50 / 50
USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA029136stablecoin50 / 50
cbBTC0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf8Coinbase wrapped50 / 50
cbXRP0xcb585250f852C6c6bf90434AB21A00f02833a4af6Coinbase wrapped50 / 50
cbDOGE0xcbD06E5A2B0C65597161de254AA074E489dEb5108Coinbase wrapped50 / 50
cbADA0xcbADA732173e39521CDBE8bf59a6Dc85A9fc7b8c6Coinbase wrapped50 / 50
cbLTC0xcb17C9Db87B595717C857a08468793f5bAb6445F8Coinbase wrapped50 / 50
cbZEC0xb2000000000000000000008501b13360000cb2ec8B20 wrap50 / 50
cbHYPE0xb200000000000000000000451d033a5000cb479e18B20 wrap50 / 50
NVDAc0xb20000000000000000000078ee7ce2fE4908108C8tokenized stock50 / 30 / 20
AAPLc0xb200000000000000000000C2e324d24d7eEcd1fb8tokenized stock50 / 30 / 20
GOOGLc0xb2000000000000000000002D0BA3164cc74f58B78tokenized stock50 / 30 / 20
METAc0xb2000000000000000000008bC8786B856E61707C8tokenized stock50 / 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.

Quote risk is additive. A launch quoted in cbBTC carries bitcoin's volatility on top of its own: if the quote asset drops, your position drops in dollar terms even when the token is flat in quote terms. And since graduation is measured in the quote asset, a rising quote raises the real cost of graduating.
06 · Snipe protection

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.

Integrators: quote with the tax. The tax is keyed to the token recipient, not the sender. Read 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.
07 · Fees

Every rate, stated plainly.

FeeRateWhenGoes to
Launch fee0.0005 ETHonce, at creationprotocol
Base trade fee1% of the quote legevery curve trade and every pool swapsplit: creator 50% / protocol 50% (stock quotes: creator 50% / protocol 30% / treasury 20%)
Creator tax0 to 10%, creator-chosen at launchsame trades as the base feecreator, in full
Snipe tax99% fading to 0 over 3sbuys in the launch windowjoins the base-fee split
Pool fee tier0graduated poolthe 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.

Interface fees. Curve trades through b420.io carry no platform routing fee: you pay the curve's own fees only. Swaps on graduated pools through the interface carry the standard 1% routing fee described in the Terms, like every other pool swap.
08 · Getting paid

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).

09 · Buyback and vesting

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.

10 · Creator controls

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.

Abandoned launches. For a token whose creator key is lost or gone, the protocol owner can propose a new fee recipient onchain. The proposal waits a public 3-day timelock, is then executable by anyone for a further 3 days, and expires unexecuted after that. The owner can cancel during the wait. It moves fee routing only: supply, pricing, taxes and locked liquidity are untouchable by this path, and it is refused entirely once fees stream to a holder distributor.
11 · Safety and recovery

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.

12 · Risk disclosures

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.

13 · Contracts

The curve suite, Base mainnet.

Per-launch token and curve addresses must be resolved from the factory (getLaunchedToken), never hardcoded. The suite:

ContractAddressRole
B420CurveFactory0x292Bc01D6Ad381Ede5474Fa51286067522B9C3a7deploys launches, holds per-quote config, drives graduation
B420LaunchAndBuy0xdAF4c31Bdf7945a5412f1df58F8e8b4C83aEc407optional router: create the launch and buy atomically
B420MemeHook0x0138920ce01045abddB9F20A526695313b42a044singleton v4 hook; accrues and splits post-graduation fees
B420FeeEscrowpublished at go-liveclaimable creator balances, every currency, one contract
B420BuybackVaultpublished at go-livebought-back supply; linear five-year weighted vest
B420LaunchLockerpublished at go-livepermanently holds graduated positions and excess supply
B420LaunchDeployerpublished at go-liveCREATE2 deploys of each curve and token pair
B420GraduationExecutorpublished at go-livemints the full-range v4 position into the locker
B420GraduationGuardpublished at go-livepreflights 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.

14 · Launching a token

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.

launch-and-buy.ts
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
});
Metadata is onchain and final. Name, symbol, logo URI, description and socials are constructor arguments of the token itself. Nothing, including B420, can edit them afterwards. Caps: name 64, symbol 16, logo 512, description 2048, each social 256 bytes.
15 · Buying and selling

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).

trade.ts
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],
});
16 · Getting a quote

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.

quote.ts
// 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.
Stop quoting at the edges. Stop quoting buys when sellableTokens() reaches zero, and sells as soon as readyToGraduate() is true, even while graduated() is still false; route to the pool after graduation.
17 · Reading state

One registry read, then the curve.

reads.ts
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.

18 · Claiming fees

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.

claim.ts
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] });
19 · Events to index

Everything an indexer needs.

EventContractSignals
TokenLaunchedfactoryevery launch: token, curve, deployer indexed; pairToken, configId, threshold
CurveBuy / CurveSellcurveeach trade with quote and token amounts; base fee and creator tax reported separately
SnipeTaxChargedcurvea launch-window buy paid the snipe tax (amount included)
CurveBuyRefundedcurvea clamped final buy returned unspent quote
LaunchSwept / CurveCompletedfactory / curvethe curve closed; reserves handed to the factory (phase 1)
PoolGraduatedfactorythe v4 pool exists and its position is locked (phase 2)
AutoGraduationFailedcurvethe in-buy graduation ran out of gas; retry permissionlessly
FeesSwept / PoolFeesSweptcurve / hooka distribution: protocol, treasury, buyback and creator amounts
HookFeeConverted / InlineConversionSkippedhooka holder-routed pool's per-swap fee conversion (or its harmless retry-later skip)
CreditedToken / ClaimedTokenescrowescrow accruals and withdrawals per currency
BuybackLocked / Locked / Releasedcurve, hook / vaultbuyback executed; tokens entered the vest; vested tokens paid out
CreatorFeeRecipientUpdated / ...ChangeProposedfactorypayout redirects and timelocked recovery proposals
PoolConversionSkipped / PoolBuybackSkippedhooka sweep declined a bad price rather than accept it
20 · Uniswap v4 pools

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.

21 · Errors

What a revert is telling you.

ErrorCause
SlippageExceededyour minimum output cannot be met at the available price
CurveGraduatedthe curve is finished; route the trade to the v4 pool
NotReadyToGraduategraduation called before the curve sold out
LaunchEconomicsMismatchyour pinned economics are stale; re-read the preview and retry
PairTokenNotApprovedthat quote asset is not enabled for launches
PairTokenDecimalsMismatchthe quote asset reports different decimals than recorded
LaunchFeeNotPaidtransaction value does not equal launchFee()
CreatorTaxTooHightax above maxCreatorTaxBps() (10%)
NotWhitelistedpublic launching is closed and the wallet is not allowlisted
ExemptionListTooLongmore than 32 snipe-tax exemptions
LaunchConfigDisabledthe chosen launch config is switched off
NativeValueMismatch / UnexpectedNativeValueETH attached where the quote is an ERC-20 (approve and send zero value)
NotCreatorFeeRecipienta fee redirect from a wallet that is not the current recipient
RecipientLockedToDistributorfees stream to the holder distributor; the routing is permanent
InternalSwapRequiresOperatorthis sweep needs a price-sensitive swap; only the operator may run it
TimelockNotElapsed / TimelockExpireda recovery proposal executed too early or too late
22 · Platform economy

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.

23 · Legacy contracts

Deployed addresses, Base mainnet.

All contracts are deployed and verified on Base (chain id 8453). Addresses link to Basescan; the button copies.

ContractAddressRole
B420Factory0x760AFca74b37B7D8a5a2b062eCB9DBDC3f0018fEdeployToken; enforced 50/50 fee split; b420 vanity seal
ClankerLpLockerFeeConversion0x0c0b04d8bd761da1899b1a13cd3353d0974f99d3LP locker; tokenRewards(token) stores each launch's fee split
ClankerFeeLocker0x20835181fD6F4e62AA8d630A89b0e5c8676808C6accrued fees per fee owner; claim(feeOwner, token) is permissionless
B420FeeCollector0xB9366B662b610F730a50408Db17E550d64F06F44protocol slice recipient; forward and swapAndFund keeper calls
B420DividendVaultFactory0xa0e85c7e3866c3bdc20cc2bee78e04fdf214583acreate(meme, rewardToken, cooldown, owner) deploys a holder vault
B420StockRegistry0x5e4643c2f48c14e09f209cae5a51455211e10c1egoverned set of tokenized stocks accepted as deep quotes
StakingB4200xC411bA66d1819054f67cDE26424cd876DB703E79stake B420, earn Coinbase tokenized stocks
StakingB690x82E6b3CEE079432F31D64855ed3DD5faCA71d309stake B69, earn B420

Tokens

TokenAddressNotes
B4200xb200000000000000000000231d6c1f1ce455ba3218 decimals, fixed supply 69
B690xb2000000000000000000007594fe5acd56df393718 decimals, fixed supply 420
WETH0x420000000000000000000000000000000000000618 decimals, default launch quote
NVDAc0xb20000000000000000000078ee7ce2fE4908108C8 decimals, Coinbase tokenized stock
AAPLc0xb200000000000000000000C2e324d24d7eEcd1fb8 decimals, Coinbase tokenized stock
GOOGLc0xb2000000000000000000002D0BA3164cc74f58B78 decimals, Coinbase tokenized stock
METAc0xb2000000000000000000008bC8786B856E61707C8 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.

24 · Legacy: factory launches

Prepare over REST, sign it yourself.

Deprecated. The instant-liquidity factory below no longer accepts new launches: every new token launches on the bonding curve (sections 01 to 21). This section stays for the tokens already launched through it, whose trading, fee claims and vaults keep working unchanged.

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.

Prepare request and response
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

launch.ts
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);
Supply the gas limit yourself. deployToken mints through the B20 precompile (an address whose code is 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.
A failed wallet simulation is cosmetic. Some wallets show a red simulation warning before signing for the same precompile reason. Sent with the correct gas limit, the transaction confirms onchain in seconds.

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.

Deprecated. The staking vault is being replaced by a fee distributor that streams creator fees directly to holders, pro rata, with no staking. Dividend launches are disabled meanwhile: the API rejects 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.
dividend-launch.ts
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.

Your launch appears on b420.io automatically. A worker indexes every TokenCreated event from the factory into the platform database, so tokens launched from the SDK or a bot show up in the markets list and the launches feed with no registration step, typically within about two minutes.
25 · Pool trading

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.

26 · Stake and dividends

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.

vault.ts
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" });
Unbonding semantics, stated plainly. The cooldown is 48 hours. 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.

platform-pools.ts
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],
  });
}
27 · Legacy creator claims

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.

claim-fees.ts
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.

28 · REST API

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.

GET/api/tokenscached 20s, swr 60s

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).

GET/api/launches?limit=&creator=0x...cached 30s, swr 120s

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).

GET/api/token/[address]cached 15s, swr 60s

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.

GET/api/token/[address]/holderscached 20s, swr 90s

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.

GET/api/profile/[usernameOr0x]cached 15s, swr 60s

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.

GET/api/leaderboard?window=all|30d|7d|24h&limit=&offset=cached 30s, swr 120s

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).

GET/api/rewardscached 30s, swr 120s

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.

GET/api/rewards/statss-maxage=60, swr 300

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.

GET/api/ohlc/[pool]?tf=1m|5m|15m|1h|4h|1Dcached 15s, swr 60s

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.

GET/api/trades/[pool]cached 5s, swr 20s

Recent trades for a pool: { trades: [{ ts, kind, priceUsd, amountToken, volumeUsd, tx, wallet }] }.

POST/api/launchno cache

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.

29 · SDK

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.

Install
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 page
quickstart.ts
import {
  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.

30 · Building agents

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:

keeper.ts
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.

31 · Audits

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.

32 · Support

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.