The launchpad has four parts. Contracts on Base (a factory, a Uniswap v4 hook and a small router) hold every rule that matters: supply, liquidity, fees and who may claim them. An indexer follows Base, waits for confirmations and stores confirmed launches, swaps, transfers, profile changes and fee events. The web app renders pages, widgets other sites can frame and a public JSON API from those rows; for new launches it also reads the factory directly so trading works while indexing catches up. A shared core library holds the stock registry, the price math, the launch quote and the event decoders used by all of them.
Everything on a page is a confirmed event, a number derived from confirmed events, or a live eth_call — with one exception, and it is signed: the creator of a token with a fixed profile may replace its presentation fields, and the server stores that only after checking the signature against the creator address recorded onchain. Nothing else the web app receives from a client is ever stored.
The launchpad's own addresses are under Deployments. Each deployment is one factory, one hook and one router.
StockPairFactory is Ownable2Step. The owner can register stocks (address, Chainlink feed, symbol, decimals), enable or disable a stock for new launches, set the creation fee (capped at 0.01 ETH), set the opening valuation (between $100 and $1,000,000) and change the treasury. The owner cannot touch any launched token, its pool or its profile, and the hook address can be set exactly once.
StockPairHook implements beforeInitialize (only the factory may create pools with this hook), beforeSwap and afterSwap with return deltas. It charges the fee on whichever side of the swap is the stock, mints the fee to itself as ERC-6909 claims inside the PoolManager and books 70% to the creator and 30% to the treasury. claim(stock) and claimMany(stocks) burn the claims and transfer real stock tokens to the caller. It rejects swaps in pools the factory has not registered yet.
StockPairRouter exposes swapExactIn(poolKey, zeroForOne, amountIn, minAmountOut, recipient, deadline). It pulls the input token from the caller, runs the swap through the PoolManager and pays the output to the recipient. Any Uniswap v4 router can trade these pools; this one is just the simplest.
A new deployment never replaces an old one. Hooks cannot be swapped on a live pool, so every factory and hook stays live: tokens launched through them keep trading, earning fees and claiming there, and the indexer follows all of them. Only the newest factory takes launches from this site.
The first deployment, from 2026-09-06, came before buy at launch, editable profiles and the partial-fill check. Its factory has launch only, so every token from it has a fixed profile. Its launch also registers the new pool with the hook before it pays the treasury, and it checks only the length of the name, symbol and contract URI, not for control characters. Its launch, Launched, launchOf, previewOpening, predictToken and stockInfo have the same signatures and selectors as the newer factory's, so one decoder reads both. Where this page describes the launch flow, buy at launch, editable profiles or partial fills, it describes the newest deployment.
The newest factory has three ways to launch; the first factory has only launch. All three take the same LaunchParams, must be sent with exactly the creation fee, and create the token at predictToken(creator, salt). This site sends launchWithOptions or launchAndBuy to a factory that has them. While the newest factory it is configured with has only launch, it sends that, with a fixed profile and no buy, and the create form hides both options.
struct LaunchParams { string name; string symbol; string contractURI; address stock; bytes32 salt; }
struct LaunchOptions { bool metadataEditable; uint256 openingFdvUsd8; uint256 deadline; }
struct CreatorBuy { uint128 stockIn; uint128 minTokensOut; }
launch(LaunchParams) -> (address token, PoolId poolId)
launchWithOptions(LaunchParams, LaunchOptions) -> (address token, PoolId poolId)
launchAndBuy(LaunchParams, LaunchOptions, CreatorBuy) -> (address token, PoolId poolId, uint256 tokensOut)launch: fixed profile, no buy, no deadline. The same function and selector as the first factory, so existing integrations keep working. The newest factory's checks are stricter: it also rejects control characters, and it pays the treasury before the pool is registered.launchWithOptions: adds a deadline (the launch reverts with Expired after it; the deadline itself still passes), a check that openingFdvUsd8 equals the valuation the creator reviewed (OpeningFdvChanged), and the choice of an editable profile, which needs an ipfs:// contract URI that is a bare CID, with no path.launchAndBuy: all of that, then a buy for the sender in the same transaction. stockIn and minTokensOut must both be above zero. See Buy at launch.In one transaction the newest factory:
msg.value equals the creation fee, the stock is enabled, the name is 1 to 64 bytes, the symbol 1 to 16 and the contract URI at most 512, with no control characters.createB20: version 1, 18 decimals, admin set to the zero address, and three bootstrap calls that cap the supply at 1,000,000,000, mint it to the factory and store the contractURI. For an editable profile a fourth call grants the factory METADATA_ROLE, and the factory checks the token holds exactly the role it asked for. The token address is predictToken(creator, salt), so the UI can open the token page before the block lands.sqrtPriceX96 so the full supply is worth the configured valuation in USD.0xdead, pays the creation fee to the treasury, records the launch, registers the pool with the hook and emits Launched, followed by MetadataEditable for an editable profile.launchAndBuy only: swaps exactly stockIn of the stock for the token, delivers the tokens to the sender and emits CreatorBought.The treasury is paid before the pool is registered, on purpose. The treasury is an address the owner sets, and the payment is a call with full gas. Until registerPool runs, the hook rejects every swap in the new pool (UnknownPool), so nothing the treasury does during that call can trade it: not at the opening price, and not ahead of the creator's buy.
Log order: Initialize, ModifyLiquidity, PoolRegistered, Launched, MetadataEditable (editable only), then for a buy FeeCharged, Swap and CreatorBought. So Launched comes before any Swap in its pool.
The factory keeps the position forever; it has no function that calls modifyLiquidity with a negative delta.
On the newest factory only, launchAndBuy lets the creator buy their own token in the transaction that creates it. The buy runs after the pool is registered and before the transaction ends, so nobody can trade the pool before it.
The buy is an exact-input swap of stockIn. If it would return fewer than minTokensOut tokens, the factory reverts with TooLittleReceived and the whole launch is undone: no token, no pool, no creation fee. The factory never holds the stock or the tokens, and it only ever pulls stock from the sender of the launch, so a leftover approval cannot be spent by anyone else. The terms are the same as launching and then buying through the router a moment later; the difference is that nobody can get in between. CreatorBought(token, creator, poolId, stockIn, fee, tokensOut) records it, and the token page shows the share as "Dev buy P%".
Why the tolerance defaults to 2%. The quote itself is exact: quoteLaunchBuy in the core library replays the pool math and matches the contracts to the wei, checked against vectors the contract tests write. What can move between review and inclusion is the opening price, which the factory reads from the stock's Chainlink feed when the transaction runs. The opening range starts on a 100-tick boundary, so a feed move changes the output in steps of about 0.995%. A 1% tolerance would revert on ordinary feed moves; 2% covers a move of up to about 1.5%. A bigger move reverts, and costs only gas.
Off by default, and only on the newest factory. A creator who launches with launchWithOptions or launchAndBuy and sets metadataEditable can later point the token at a new profile. A token from launch, and every token from the first factory, is always fixed.
The bootstrap that creates an editable token has one extra call, which grants the B20 METADATA_ROLE to the factory and to nobody else. updateContractURI(token, uri) checks that the caller is the launch's creator and that the token is still editable, accepts only ipfs:// followed by a bare CID (letters and digits, so no path, dot, slash or percent sign can lead it anywhere else), sets it on the token and emits ContractURIChanged; the token emits ContractURIUpdated(). The launch itself checks an editable profile's first link the same way. The factory checks the link, not the document behind it. When the image that document names is also ipfs:// and a bare CID, every change to what the token shows is an onchain event, and a locked profile's content is fixed. A document can still name an image at a web address, which can change with no event at all; the token page says so whenever the link or the image is anything but a bare CID, and this site only ever pins bare-CID images.
lockMetadata(token) makes the factory renounce the role. The token has no admin, so nothing can ever grant it again. metadataStatus reads the role from the token itself, so it says Locked from that block on.
What it cannot do: at the token level the role also covers the name, symbol and extra-metadata setters. Only the factory's code keeps them out of reach, and that code has no path to them: no rename function, no generic call, no proxy, no upgrade. Tests pin the factory's complete list of state-changing functions and scan its bytecode for the rename selectors.
On this site an editable token is edited onchain only: the signed off-chain editor is closed for it. The token page shows how many times the profile changed and when. Until the profile is locked, whoever holds the creator's key can change its links.
When the stock is the amount a swap specifies (an exact-input buy, or an exact-output sell) the fee is taken from it in beforeSwap, so FeeCharged comes before the pool's Swap log. When the stock is the other side (an exact-input sell, which is every sell through this site's router, or an exact-output buy) it is taken from the stock amount in afterSwap, after Swap. A fee that rounds to zero is not charged and emits nothing. The current rate is currentFeeBps(poolId), which the quote endpoint reads from the token's own hook. Fees never sit in a server: they are ERC-6909 claims owned by the hook and booked per account, withdrawn with claim.
Partial fills. When the stock is the amount a swap specifies (an exact-input buy or an exact-output sell), the hook takes the fee on that whole amount before the swap runs. The newest hook then checks that the pool filled all of it, and otherwise reverts with PartialFill; Uniswap v4 delivers that inside WrappedError. Pools on the first deployment's hook have no such check: a swap there that stops early at a price limit its caller set still pays the fee on the full amount it asked for. This site's router sets no price limit of its own, so its swaps are not affected; third-party routers that set one can be overcharged.
Uniswap prices are currency1 per currency0 in raw units; the token has 18 decimals and every stock has 8. With the token as currency0, P = 10^8 · FDV / (stockUsd · 10^27); as currency1 the fraction inverts. The factory computes sqrtPriceX96 = sqrt(P · 2^128) · 2^32 with full-precision integer math and derives the tick.
The indexer stores every swap's post-trade price as whole stock per whole token with 30 decimals; the web app multiplies by the stock's latest Chainlink USD price to show dollars. FDV is that price times one billion. Pool reserves are computed from the locked position (liquidity, tick range) and the live sqrtPriceX96 from StateView.
The indexer polls Base every 2 seconds and processes blocks that are at least 3 confirmations deep (INDEXER_CONFIRMATIONS, its default). It fetches Launched, CreatorBought, MetadataEditable, ContractURIChanged and MetadataLocked from every factory, FeeCharged and FeesClaimed from every hook, PoolManager Swap logs filtered by the known pool ids, and ERC-20 Transfer logs of launched tokens, then writes everything in one database transaction: launches, swaps (with side, price and trader), fee events, profile changes, transfers, balances and rebuilt one-minute candles.
It follows every deployment in STOCKPAIR_DEPLOYMENTS, and a deployment added later gets a catch-up pass from its own deploy block. A creator's buy at launch is stored as a buy by the creator, taken from CreatorBought, so it is attributed correctly even when a bundler sent the transaction.
Reorgs are detected by comparing the stored hash of the last processed block with the chain; on mismatch the indexer rolls back to the last common ancestor and replays, including any profile change in the rolled-back blocks. Every minute it refreshes the 13 Chainlink quotes, and it fills token metadata (description, image, website, X, Telegram) from IPFS in the background, again whenever an editable token points at a new profile. The web app's /api/health reports how far behind the chain head the indexer is.
Launch metadata (name, symbol, description, image, website, X, Telegram) is written into the token's ERC-7572 contractURI on IPFS. For a token with a fixed profile (every launch that did not choose an editable one, and every token from the first factory) that record can never change, so the creator can update presentation fields off-chain instead: description, image, website, X and Telegram. Name, symbol and supply never change. A token with an editable profile is updated onchain (see Editable profile), and this off-chain path is closed for it.
An update is an EIP-712 message signed by the creator wallet over the token address, the normalised fields, the keccak256 of the new image (or zero when unchanged) and a timestamp. The server accepts it only when the signer equals the launch creator recorded onchain, the timestamp is within 15 minutes of its clock and newer than the stored profile, the uploaded image matches the signed hash, and the signature verifies (offline for EOAs, via ERC-1271 / ERC-6492 on Base for smart wallets such as Base Account). The stored profile overrides the launch metadata field by field and the token page says when the creator last updated it.
Every route is public, needs no key and answers JSON; reads come from the indexer and live pool reads, and a confirmed launch can be quoted and traded before it is indexed. Missing data is null, never a placeholder. The transaction routes return the exact calls this site sends, for the user's own wallet to sign. The full reference, with every parameter, error, a curl line and a live Try it, is on its own page, and the same list is published for tools and AI agents.
GET /api/marketsList marketsGET /api/stocksList quote stocksGET /api/tokens/{address}Get one tokenGET /api/tokens/{address}/swapsList tradesGET /api/tokens/{address}/candlesGet candlesGET /api/tokens/{address}/holdersList holdersGET /api/tokens/{address}/imageGet the token imageGET /api/tokens/{address}/dex-paidDEX Screener paid statusGET /api/tokens/{address}/profileGet the creator profilePOST /api/tokens/{address}/profileUpdate the creator profileGET /api/wallet/{address}Get a walletGET /api/activityActivity feedGET /api/statsPlatform totalsGET /api/namesResolve BasenamesGET /api/launch-configLaunch configurationPOST /api/quoteQuote a tradePOST /api/tx/swapBuild a swapPOST /api/metadataPin a token profilePOST /api/tx/launchBuild a launchBuilding transactions. POST /api/tx/swap and POST /api/tx/launch return the calls for the account that will send them, built by the same code as this site's trade panel and create form: an approval for the exact amount when the allowance is short, then the swap or the launch, with the minimum output, a ten-minute deadline and a simulation. A buy at launch keeps this site's limits (15% of the supply needs acknowledgeShare, half is never built), and builderCode attributes the transaction to the caller beside this site.
Limits and partner access. Each caller may make 120 quotes, 30 transaction builds, 120 token reads and 60 wallet reads a minute; more answer 429. Browsers on listed partner origins (zkCodex by default; PARTNER_ORIGINS replaces the list) may call the routes the reference marks partner CORS, so those limits and the eligibility rule apply to each visitor. Any site can use the widgets instead, which need no listing.
Two pages exist to be framed by other sites: /embed/trade/:token, the buy / sell panel of one token, and /embed/create, the create form. Widgets builds the iframe code with a live preview. The widget pages answer Content-Security-Policy: frame-ancestors *; every other page answers X-Frame-Options: DENY.
A widget is this site running inside the frame: the same quote endpoint, slippage and impact limits, review step, eligibility rule and contracts. The visitor connects their own wallet in the frame and signs there; the host page never sees a key, an approval, a balance or an address. A launch from the create widget comes from the visitor's wallet, so the visitor is the creator and earns the creator's 70%; the host earns no share of the fees. The trade widget follows a new launch from submitted to indexed without reloading, and the create widget moves to the new token's trade widget once the wallet returns a hash. A link to any other page of the site opens in a new tab.
The price, fee, minimum received, review step, eligibility question and the "powered by" line cannot be hidden. The default frame is 820px tall for trade, taller with a chart or lists, and 900px for create, which scrolls inside the frame; the optional script on the Widgets page grows the frame to the widget. A host page with its own Content-Security-Policy has to allow this site in frame-src, and must not sandbox the iframe: wallets open popups and browser wallets inject into the frame.
Events. The widget posts to its host with source: 'bstocks-launchpad': ready with the widget, resize with its height, swap with the token, side and transaction hash, and launch with the token and transaction hash. Check event.origin. Any page can post a message that looks like these, so treat swap and launch as a hint to refresh, and look the transaction up on Base before rewarding anyone for it.
Every pool here is quoted in a Coinbase tokenized stock, and the issuer offers those only to eligible persons outside the United States. The site asks rather than guesses: an IP address says where a connection comes from, not who is behind it.
The trade panel and the create form ask right above their button; the site also asks once on arrival, and a widget asks at the button only, where a dialog could land outside the host page's view. A widget in another site's frame may be refused the cookie (Safari refuses it by default), so the browser also sends the answer as the x-bstocks-eligibility header. Reading stays open to everyone. The swap itself is a call from the visitor's own wallet to the contracts, which no server can stop; this rule covers only the routes this site controls.
A Telegram channel posts every launch, every large trade, every market cap milestone and every new high a token sets against its stock. It is fed by the indexer rather than by polling the API: a row is written into an outbox table inside the same database transaction that commits the swap it describes, so an announcement is exactly as durable as the fact behind it. A reorg deletes the unsent rows for the blocks it rolled back, so the channel cannot announce a trade that did not survive.
A trade qualifies by clearing an absolute dollar floor or a share of that token's own 24-hour volume, because one threshold cannot serve a token doing $200 a day and one doing $20,000. Not every buy and not every sell: nobody can filter a shared channel, so one busy token posting each of its trades would bury every other. Milestones are recorded only after the post is actually delivered, so a failed send retries rather than silently skipping a level.
A creator's buy at launch appears on the launch post rather than as a separate trade. Profile changes are not posted.
Coinbase tokenized stocks on Base, registered in the factory with their Chainlink total-return feeds (8 decimals, 24/5). Icons come from each token's onchain metadata.
The limits this site applies before a wallet opens. Only the rows marked onchain are enforced by the contracts; anyone calling the contracts directly sets their own.
| Setting | Range | Behaviour |
|---|---|---|
| Trade slippage | 0.1% to 5% · default 1% · presets 0.5, 1, 3, 5% | Amber from 3%. A value outside the range is rejected, never rounded into it. |
| Price impact | amber from 5% · red from 25% | Red needs a tick before Confirm. Impact alone never blocks a trade. |
| Buy-at-launch tolerance | 0.5% to 5% · default 2% · presets 1, 2, 3, 5% | Covers the Chainlink price moving before the launch lands. Not remembered between visits. |
| Buy-at-launch share | 5% amber · 15% red · 50% blocked | Share of supply, checked at review and again right before sending. Red needs a tick. The 50% block is this site's; the contract has no cap. |
| Deadline | 10 minutes | Trades, and launches through launchWithOptions or launchAndBuy. Counted from the later of the latest block and your clock, after any approval confirms. |
| Feed age | 7 days onchain | A launch reverts when the feed is older than 7 days. The create form uses the latest Chainlink value to estimate the USDC equivalent. |
Selectors and topics of the newest factory, hook and router. The first deployment has the subset its factory and hook implement, with the same selectors. PoolId is bytes32; structs are ABI-encoded as tuples. A PartialFill revert reaches callers as the reason inside Uniswap's WrappedError.
launch(LaunchParams p) payable returns (address token, PoolId poolId)0x28314fb2launchWithOptions(LaunchParams p, LaunchOptions o) payable returns (address token, PoolId poolId)0x01499600launchAndBuy(LaunchParams p, LaunchOptions o, CreatorBuy b) payable returns (address token, PoolId poolId, uint256 tokensOut)0xd0150e4fupdateContractURI(address token, string newURI)0x6697392elockMetadata(address token)0x37d1df5fmetadataStatus(address token) view returns (MetadataStatus)0x9ce0634dpredictToken(address creator, bytes32 salt) view returns (address)0x486e36c8previewOpening(address stock, address token) view returns (uint160 sqrtPriceX96, int24 tick, uint256 stockUsd8)0x9affe51alaunchOf(address token) view returns (Launch)0x029282d7poolKeyOf(address token) view returns (PoolKey)0x8652edf9stockInfo(address stock) view returns (Stock)0x4949a2e7creationFee() view returns (uint256)0xdce0b4e4openingFdvUsd8() view returns (uint256)0xbf778e65setCreationFee(uint256 fee)0xb7d86225setOpeningFdv(uint256 fdvUsd8)0xbb771f5dsetTreasury(address treasury)0xf0f44260addStock(address stock, address feed, string symbol, uint8 decimals)0xba5e9189setStockEnabled(address stock, bool enabled)0x492a5578setHook(address hook)0x3dfd3873claim(address stock) returns (uint256 amount)0x1e83409aclaimMany(address[] stocks) returns (uint256[] amounts)0x7e686e01claimable(address stock, address account) view returns (uint256)0xd4570c1ctotalFees(PoolId id) view returns (uint256)0x6b491bd0currentFeeBps(PoolId id) view returns (uint256)0x1d8ef3efpoolInfo(PoolId id) view returns (PoolInfo)0x8cebd942swapExactIn(PoolKey key, bool zeroForOne, uint128 amountIn, uint128 minAmountOut, address recipient, uint256 deadline) returns (uint256 amountOut)0x8409da66Launched(address indexed token, address indexed creator, address indexed stock, PoolId poolId, uint160 sqrtPriceX96, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 stockUsd8, string name, string symbol, string contractURI)0x545827070fae462314f8e79f25d99f8e713bf3cecb38728eb4c4804e1e20d0a6MetadataEditable(address indexed token, address indexed creator)0x1241a65d78d66378b38c08d6c0fc8deaa8e6f592ffc4bfc685d51af653f1bb6fCreatorBought(address indexed token, address indexed creator, PoolId indexed poolId, uint256 stockIn, uint256 fee, uint256 tokensOut)0x43d15e9d32712a10d4ab74467519cf3a13edcf6de23f4894edbb0abf319b9f65ContractURIChanged(address indexed token, address indexed creator, string contractURI)0xb23802208f960154d35feb1ddb4ed3718dd9ea1c6af17d702f83bb34f3c6ab4bMetadataLocked(address indexed token, address indexed creator)0x1653aa4ca9f980f3b8b3aaa209e5f5d445c8688450cd1bc630a19b5211679aa8StockAdded(address indexed stock, address indexed feed, string symbol, uint8 decimals)0x3572fbfa06ac9234af5d48e012aedc365ca671a1144b7ae431000abebbe72f1aStockEnabled(address indexed stock, bool enabled)0xac94a866eb4787d79661bd9f67018689dbfe5ed99cfc16f604a022911fdf54bbCreationFeeSet(uint256 fee)0x65cf44d7c3dc10549f322afbe745b2a569e6e2a177f9465749a393afbc9c354fOpeningFdvSet(uint256 fdvUsd8)0x2a64b70825c76cc32d90943f51cfe02ea329703ca5540f64c3412dea95dcce97TreasurySet(address indexed treasury)0x3c864541ef71378c6229510ed90f376565ee42d9c5e0904a984a9e863e6db44fHookSet(address indexed hook)0x4eab7b127c764308788622363ad3e9532de3dfba7845bd4f84c125a22544255aPoolRegistered(PoolId indexed poolId, address indexed token, address indexed stock, address creator)0x01bf263a1db1652580721573296e1a1fa70b3d4c87f61d02a69c4e1109d2d573FeeCharged(PoolId indexed poolId, address indexed stock, uint256 amount, uint256 creatorAmount, uint256 platformAmount, uint256 feeBps)0x8b0e4cf39120c70654d9e54bb37acd7e4b571480cac924f4d96ebaf14b35093dFeesClaimed(address indexed stock, address indexed account, uint256 amount)0xfe3464cd748424446c37877c28ce5b700222c5bc9f90d908afcc4e5cb22707ffSwapped(address indexed payer, address indexed recipient, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut)0xd6d4f5681c246c9f42c203e287975af1601f8df8035a9251f79aab5c8f09e2f8ContractURIUpdated()0xa5d4097edda6d87cb9329af83fb3712ef77eeb13738ffe43cc35a4ce305ad962WrongCreationFee()0x44402b7bStockNotEnabled()0xa57d5da7InvalidText()0x7a2a029cStaleFeed()0xa0cd3bb2InvalidFeed()0x1f86e170Expired()0x203d82d8OpeningFdvChanged()0x7156ad5eZeroAmount()0x1f2a2005TooLittleReceived()0xc9f52c71OutOfBounds()0xb4120f14NotCreator()0x93687c0bMetadataNotEditable()0x4a38a31aUnexpectedRoles()0xa507f32bUnexpectedSupply()0xe9531af4UnexpectedDelta()0x29a70758InvalidRange()0x561ce9bbTreasuryTransferFailed()0x0e373cf8HookNotSet()0x86972930HookAlreadySet()0xbef9156aStockAlreadyAdded()0xe3842940ZeroAddress()0xd92e233dOnlyPoolManager()0xf655705dPartialFill()0xd964f528UnknownPool()0xf7139e33NothingToClaim()0x969bf728OnlyFactory()0x0c6d42aeWrappedError(address target, bytes4 selector, bytes reason, bytes details)0x90bfb865launchAndBuy. The first factory registers the pool before it pays the treasury.launchWithOptions and launchAndBuy revert after their deadline and when the opening valuation changed after the creator reviewed it; a changed creation fee already fails the exact msg.value check.nonReentrant; unlock callbacks accept calls only from the PoolManager.https://x.com/handle and https://t.me/handle./embed may be framed, and by any site; every other page refuses. A widget holds no keys either: each trade or launch still ends in the visitor's own wallet, which a host page cannot draw over or answer for. Links out of a widget open a new tab rather than loading the site inside the host's frame.POST /api/region refuses a request whose Origin is another site.