協議
本文件僅提供英文版本。
Fusor Protocol Documentation
Fusor lets anyone create an ERC-20 token whose market is denominated in a tokenized stock rather than in a stablecoin or the chain's native asset. You pick between one and five stock tokens, the protocol mints a fixed supply, opens a Uniswap V4 pool against each of them, and locks the liquidity forever — all inside a single transaction. There is no presale, no vesting schedule, no team allocation, and no step where a human has to approve your launch.
Everything below describes what the deployed contracts actually do. Where a number is a deployment parameter rather than a constant, this page says so and tells you which getter returns it — reading it from the chain is always correct, and copying it into your code is always a latent bug.
What the design commits to
Three properties are enforced by the contracts rather than by policy, which is what makes them worth stating:
- Supply is fixed at mint. Every token is created with exactly 1,000,000,000 units and no mint function. The number cannot grow later, by anyone.
- Liquidity is locked, not vested. The launch positions are transferred to
FusorV4Lockerduring the launch transaction. There is no unlock date and no withdrawal path — not a delayed one, not an admin one. - The market never moves. Trading happens in the pools created at launch, before and after every milestone. Nothing migrates, and no second pool supersedes the first.
The pieces
You only ever call the launchpad proxy. The rest of the table exists so that when you read a transaction trace or an event log, you know what you are looking at.
| Component | What it does |
|---|---|
| FusorLaunchpadV5Upgradeable | The proxy you call. Creates the token, opens and funds the pools, locks the positions, and records the launch on-chain. UUPS-upgradeable, so its implementation address changes over time — never pin it. |
| FusorLaunchFacet | Holds the body of launchTokenMulti. It is an immutable constructor argument of the implementation, split out because the combined contract exceeds the EIP-170 code-size limit. |
| FusorTokenV5 | The ERC-20 that gets deployed per launch. Fixed supply, no mint function, plus a short post-launch guard described under Launch Protection. |
| FusorV4Hook | A Uniswap V4 hook attached to every Fusor pool. Its address encodes its permission bits, which is why it looks unusual. |
| FusorV4Locker | Custodian of every launch position. Also does the fee accounting and holds creator and protocol balances until they are claimed. |
| FusorV5MultiPoolAggregator | Optional. Executes one trade across several of a token’s pools at once, using USDG as the common leg. |
| StockTokenRegistry | The allowlist of quote assets. Stores each one’s decimals, symbol, and price-feed address, and is the only authority on whether a launch against it will succeed. |
| FusorPriceOracle + FusorOracleFeed | Price plumbing for the assets that have no official Chainlink feed. The oracle stores prices; one feed contract per asset re-exposes them behind the standard Chainlink interface. |
What this release does not do
Stated up front so you do not build against something that is not there:
- No developer buy. The adapter is deployed and the parameters exist in the launch struct, but this release ships with the feature off. Pass the skip sentinel — see Creating a Token.
- No quote HTTP API. There is no endpoint that prices a swap for you. Routing means reading pool state over RPC and calling the quoter yourself.
- No token approval process. Nobody reviews a launch. A symbol appearing on this site is not an endorsement of the project behind it.
The Pairing Model
A conventional launchpad opens one pool against a stablecoin. Fusor opens up to five, each against a different tokenized stock, and lets the creator decide how the supply is divided among them. That single change is where most of the protocol's behaviour comes from, so it is worth being precise about what it does and does not mean.
Allocations are liquidity placement, not backing
When you allocate 60% to one market and 40% to another, you are saying where the project tokens go as single-sided liquidity. You are not creating a basket, a claim, or a redemption right. Nobody can hand back a project token and receive a proportional slice of the stocks. Each pool is an ordinary Uniswap V4 market that happens to be denominated in that stock token, and each one finds its own price.
Weights must total exactly 100%
Allocations are integer basis points and their sum has to equal 10000 exactly. There is no rounding tolerance in the contract — 9,999 reverts. If you are splitting evenly across a count that does not divide cleanly, put the remainder somewhere explicit: three markets are 3334 / 3333 / 3333, never three times 3333.
| Split | Basis points | Accepted |
|---|---|---|
| Single market | 10000 | Yes |
| Two markets, even | 5000 + 5000 | Yes |
| Three markets, even | 3334 + 3333 + 3333 | Yes — remainder placed on the first |
| Three markets, naive | 3333 + 3333 + 3333 | No — totals 9999, reverts |
| Any market weighted zero | 10000 + 0 | No — every market must carry a positive weight |
| Same market twice | 5000 + 5000 on one token | No — duplicates are rejected |
Which assets can be a quote market
The authority is StockTokenRegistry, and the only question that matters at launch time is what getConfig(token) returns for it. If enabled is false, or the configured price feed is the zero address, the launch reverts — regardless of what any interface displayed.
This site shows a filtered view of the registry rather than all of it, and the difference trips up integrators, so here is the exact layering:
| Layer | Where it lives | Effect |
|---|---|---|
| Registry entry | On-chain, StockTokenRegistry | Decides whether a launch succeeds. This is the only layer the contracts consult. |
| Non-stock assets | Off-chain product policy | WETH, USDG and cbBTC are valid registry entries and can legitimately be paired against, but they are not equities, so this site leaves them out of the stock picker. |
| Site-level exclusions | Off-chain product policy | A small number of registered assets are withheld from the picker here. They remain fully usable on-chain. |
ETH is handled separately
ETH is not a registry entry. It is represented by the zero-address sentinel and priced through the launchpad's own ethPriceFeed(), because every launch needs an ETH price whether or not ETH is one of the chosen markets — the launch fee and the milestone target are both denominated through it.
Currently registered assets
Read live from the registry as you load this page, filtered to what this site offers:
| Currently enabled symbols | Source |
|---|---|
| Loading current registry… | Live registry entries that are enabled, with COIN excluded by website policy |
- Symbols are stored on-chain as
bytes32, right-padded with zero bytes. - Decimals come from the registry entry, not from the token contract — the launchpad rejects an entry that reports zero.
- Each entry carries its own price-feed address, and those are swappable by the registry admin without touching the launchpad.
Token Lifecycle
A Fusor token has a much shorter life story than most launchpad tokens, because the phases that usually exist — bonding curve, migration, unlock — are simply absent. There are two states, and the transition between them changes a flag and nothing else.
Phase 1 — creation
One transaction deploys the ERC-20, opens a pool per chosen market, places the allocated supply as single-sided liquidity, hands every position to the locker, and emits MultiPairLaunchCreated. Either all of it happens or none of it does; there is no partial launch to clean up. From the block it lands in, the token is tradeable by anyone.
Phase 2 — open market, indefinitely
Everything after creation is ordinary Uniswap V4 trading in those same pools. Price is whatever the pool state says. Fees accumulate inside each locked position and are periodically collected and split. The creator can claim their share whenever a non-zero balance exists.
The milestone is an event, not a phase
When locked principal reaches the target recorded at launch, anyone may call syncGraduation and the token is flagged as graduated. This is the part most people expect to work differently, so, explicitly:
| Before | After | |
|---|---|---|
| Where trading happens | The launch pools | The same launch pools |
| Liquidity | Locked in FusorV4Locker | Locked in FusorV4Locker |
| Price ceiling | None | None |
| Fee split | 70 / 30 creator / protocol | 70 / 30 creator / protocol |
| On-chain flag | Not set | Set, and permanent |
What never happens
- No migration. There is no second pool that supersedes the first, so no address in your integration goes stale.
- No unlock. The locker has no withdrawal path for launch positions — for the creator, for the protocol, or for anyone else.
- No supply change. No mint, no burn hook, no rebase.
- No trading halt. Nothing in the protocol can pause a launched token's market.
Creating a Token
One call does everything: launchTokenMulti on the launchpad proxy. It takes a single struct and requires the launch fee as msg.value. Everything the token needs to exist and trade is established before the transaction returns.
What the transaction performs
- Deploys a
FusorTokenV5ERC-20 with a fixed supply of 1,000,000,000 units. - Creates one Uniswap V4 pool per chosen quote market, each with the Fusor hook attached.
- Places the supply across those pools as single-sided concentrated liquidity, in the weights you gave.
- Transfers every resulting position to
FusorV4Locker, permanently. - Records the launch and its milestone target on-chain, and emits
MultiPairLaunchCreated.
The parameter struct
Eleven fields, in this order. The overload matters: the contract declares two functions named launchTokenMulti, and only the 11-field one (0x07489e0a) is callable on this deployment. Encoding against the other one produces calldata that reverts for reasons that look unrelated to the mistake.
| Field | Type | Meaning |
|---|---|---|
| name | string | ERC-20 name. Stored on the token; not validated for uniqueness. |
| symbol | string | ERC-20 symbol. Also not unique — two tokens may share one. |
| metadataURI | string | Where off-chain metadata lives. Written to the token and indexed; the contract does not fetch it. |
| metadataHash | bytes32 | keccak256 of the metadata bytes, so anyone can verify the URI still serves what you committed to. Zero is accepted and means “not committed”. |
| allocations | (address,uint16)[] | One to five entries of quote token plus weight in basis points, summing to exactly 10000. |
| creatorFeeRecipient | address | Receives the creator share of swap fees. Can differ from the sender. |
| developerBuyRecipient | address | Must be non-zero even when no developer buy is requested — set it to the creator. |
| developerBuyPairIndex | uint8 | Index into allocations for the developer buy, or 255 to skip it. |
| developerTokenAmountOut | uint256 | Exact project tokens to buy. Zero when skipping. |
| maxQuoteAmountIn | uint256 | Spend ceiling for that buy. Zero when skipping. |
| deadline | uint256 | Unix seconds after which the call reverts. Guards against a transaction sitting in the mempool while prices move. |
developerBuyRecipient is validated as non-zero even when the developer buy is skipped. Leaving it at the zero address while setting the index to 255 is the single most common way a first integration reverts.Developer buy — off in this release
The struct carries the parameters and the adapter is deployed, but this release does not enable the feature. Skip it by setting developerBuyPairIndex to 255, both amounts to 0, and developerBuyRecipient to the creator's address. When it is enabled, it will be a real market purchase at launch price with an exact-output amount and a spend ceiling — not a free allocation.
The launch fee is read, not assumed
msg.value must equal launchFeeWei() exactly; anything else reverts with IncorrectLaunchFee. That getter is a deployment parameter, not a protocol constant, so read it in the same session you launch in. The fee is separate from gas, and separate from any quote tokens a developer buy would spend.
Preconditions worth checking first
All of these are enforced on-chain and will revert the launch. Checking them beforehand costs a few read calls and turns an opaque revert into a sentence:
| Condition | Reverts with |
|---|---|
| Every quote market enabled in the registry | StockTokenDisabled |
| Weights total exactly 10000 | WeightsNotOneHundredPercent |
| No duplicate quote market | DuplicatePair |
| Between one and five markets | InvalidPairCount |
| Each market has a price feed configured | InvalidOracle |
| Every relevant price fresh enough for maxOracleAge() | StaleOracle |
| msg.value equals launchFeeWei() | IncorrectLaunchFee |
| deadline still in the future | DeadlineExpired |
Reading the result correctly
The function returns the new token's address, and a simulation will give you that value before you send anything. But the returned value is the address the token would get at the launchpad's current nonce — if another launch confirms first, the real address differs.
MultiPairLaunchCreated event in the receipt, and only accept the event when it was emitted by the launchpad proxy itself. Also check receipt.status: a reverted transaction still returns a perfectly ordinary receipt, and treating that as success is how a failed launch gets recorded as a live token.Launch Protection
Fusor tokens carry no transfer tax and no blacklist. Instead, FusorTokenV5 enforces three limits for a brief window — the launch block plus the next five — and then stops enforcing anything, on its own, with no transaction required to lift them.
| Limit | Value | Applies to |
|---|---|---|
| Buy size | 5.5% of supply — 55,000,000 tokens | Any single acquisition |
| Wallet holding | 5.0% of supply — 50,000,000 tokens | Balance after any acquisition |
| Launch-block recipient | Only the designated developer-buy recipient | Pool-originated buys, launch block only |
What is deliberately not restricted
- Selling. There is no limit on disposals at any point, inside the window or outside it.
- Ordinary transfers. The caps apply to acquisition, not to moving tokens you already hold.
- Anything after the window. Block six onwards behaves like any ERC-20.
What this does and does not achieve
The caps make it expensive for one address to take an outsized share of the opening supply, which is the specific failure they target. They do not prevent an actor with many funded addresses from doing the same thing across them, and nothing in the protocol claims otherwise. Treat the window as friction against the naive version of the attack, not as a distribution guarantee.
Separate from the pool fee
The 1% swap fee applies to every trade in a Fusor pool, inside the protection window and forever after. It is a property of the pool, not of the window, and the two are unrelated.
Liquidity & Pricing
There is no bonding curve at any point. The launch does not sell into a formula and then move funds somewhere else when a threshold is crossed — it opens real Uniswap V4 pools immediately and leaves them there. Everything about price after that is ordinary AMM behaviour against locked liquidity.
How the liquidity is placed
- Each chosen market gets its own pool, funded with that market's share of the supply.
- The position is single-sided: only project tokens go in. The quote side fills up as people buy.
- The starting price comes from the oracle at launch time, so the first trade is not arbitrary.
- The range runs from that starting point out to Uniswap's extreme usable tick, so there is no upper bound the price can hit.
- Every position is handed to
FusorV4Lockerin the same transaction and stays there.
Tick direction depends on token ordering
Uniswap orders a pool's two tokens by address, and that ordering decides which way the range extends. It has no economic meaning, but it changes how you read the numbers:
| Project token sorts as | Range runs | Reading it |
|---|---|---|
| token0 | launch tick → +887200 | Higher tick means a higher project-token price |
| token1 | −887200 → launch tick | Higher tick means a lower project-token price |
Why locked liquidity behaves differently
In a pool where liquidity can leave, a falling price and withdrawing providers reinforce each other. Here the launch liquidity cannot be withdrawn by anyone, so depth does not evaporate under selling pressure. This is a real structural difference, and it is worth being equally clear about what it does not do: it does not put a floor under the price, and it does not stop the price from falling as far as the pool allows.
Composing a market cap from several pools
A token paired with three stocks has three prices at once. Turning that into one displayed number takes three steps:
- Read each pool's current
sqrtPriceto get the project token's price in that stock token. - Convert each to USD using that stock's oracle price.
- Combine the USD prices using the token's configured weights, then multiply by the fixed 1,000,000,000 supply.
Legacy single-market tokens
Tokens created before multi-market launches existed have one market and route through V3. They are represented as a single 100% V3_LEGACY allocation, remain viewable and tradeable, and are not migrated into the V4 model. Nothing on this page about multi-pool routing applies to them.
Trading & Routing
Every trade ends up in one or more of the token's launch pools. What differs is how a trade gets there. There are exactly two paths, they use different contracts, and they require different approvals.
Path A — one named market
You choose a market by ticker and trade only that pool, spending or receiving that stock token directly. Execution goes through Permit2 and the Universal Router in a single transaction. This is the simpler path and the right default when you already hold the quote asset, or when you specifically want exposure through one market.
Path B — balanced across markets
FusorV5MultiPoolAggregator takes one exact-input trade and spreads it over up to five of the token's pools, presenting USDG as the single asset you spend or receive. It converts USDG to each pool's stock token as needed, executes the legs, and settles back — all atomically. It is permissionless: it creates nothing, custodies nothing between transactions, and does not replace the pools it trades against.
How it decides:
- Enumerates the canonical pools registered for the token.
- Quotes both legs for each candidate: the USDG ↔ stock conversion and the stock ↔ project-token swap.
- Discards markets it cannot support, and any route whose live price impact exceeds 15%.
- Compares the best single pool against discrete multi-pool splits.
- Takes a split only when the improvement is worth the extra gas it costs.
- Applies a minimum per leg and a minimum on the total, then re-quotes and simulates immediately before signing.
| Direction | You provide | What happens | You receive |
|---|---|---|---|
| Buy | USDG | Converted to each pool’s stock token as needed, then swapped across the chosen pools | Project token |
| Sell | Project token | Split across the chosen pools, then the stock proceeds are converted | USDG |
Guarantees the aggregator enforces
- At most five distinct pool legs per trade.
- Every supplied pool key is rebuilt and checked against the launchpad's registered pool — you cannot point it at a pool of your own.
- Unsupported conversion assets, zero recipients, expired deadlines, and deadlines more than a day out are all rejected.
- The aggregate minimum is checked against the recipient's actual balance change, not against an internal accounting number.
- Its own token balances are restored to their opening values, so a trade cannot leave funds sitting in it.
Choosing between them
| Situation | Path |
|---|---|
| You hold the stock token already | A — named market |
| You hold USDG and the token has several markets | B — balanced |
| You want exposure through one specific market | A — named market |
| Trade is large relative to a single pool’s depth | B — balanced, if it finds a route |
| No safe balanced route can be built | A — pick a market explicitly |
Approvals differ by path
| Path | Approve what, to whom |
|---|---|
| Named market | Input token → Permit2, then Permit2 → Universal Router |
| Balanced | Input token → FusorV5MultiPoolAggregator directly |
Existing allowances are checked before anything is requested, so an approval prompt appearing means one is genuinely missing.
There is no quote API
Fusor is not the only venue
These pools are public Uniswap V4 markets. Any interface can trade them, and an external router may add its own fee or route through unrelated pools with different fee tiers. A fee charged by someone else's router is not part of the Fusor fee schedule and does not reach the creator or the treasury. Inspect the whole route before signing.
Price Oracles
A launch needs a price for ETH and for every stock token it pairs against — that is what sets the opening pool price and what the milestone target is denominated through. Prices come from the feed address recorded on each registry entry, and those feeds fall into two classes that behave differently enough that mixing them up will waste your afternoon.
Two classes of feed
| Class | What it is | Kept fresh by |
|---|---|---|
| Official Chainlink | A real Chainlink aggregator deployed on this chain. Most large-cap equities have one. | Chainlink’s own node operators — nothing on our side. |
| Fusor self-priced | A FusorOracleFeed contract wrapping FusorPriceOracle, re-exposed behind the standard Chainlink interface so the launchpad consumes it identically. | Our off-chain keeper, pushing prices on a schedule. |
Both answer the same no-argument latestRoundData() call and both report 8 decimals, which is the point of the wrapper — the launchpad cannot tell them apart and does not need to. You can, by looking at the deployed code size: a Fusor feed is a few hundred bytes, a Chainlink aggregator is thousands.
Staleness is enforced twice, at very different limits
This is the single most useful thing to know about Fusor pricing, and it is not obvious from either contract on its own:
| Check | Limit | Where it lives | Applies to |
|---|---|---|---|
| launchpad.maxOracleAge() | A deployment parameter — read it, do not assume it | The launchpad, applied to whatever the feed returned | Every asset |
| FusorPriceOracle.MAX_STALENESS | 2 hours, a hard-coded constant | Inside the oracle — latestRoundData reverts outright | Self-priced assets only |
Because the inner check reverts before the outer one can run, the tighter limit always wins. The practical result:
- Chainlink-backed assets are governed by
maxOracleAge(), which on this deployment is generous enough that a price well over a day old is still accepted. - Self-priced assets are effectively governed by a 2-hour window. If the keeper has not pushed within that window, the feed reverts and no launch against that asset can proceed — regardless of what
maxOracleAge()says.
maxOracleAge() you get the launchpad's StaleOracle. Past two hours on a self-priced asset you get a revert from inside the feed read itself — so a naive integration reports “could not read the price feed” rather than “this price is stale”. Same cause, different symptom.FusorPriceOracle
An owner-controlled registry storing one price per asset at 8 decimals, Chainlink's convention — $200.00 is 20000000000. Two reads:
latestRoundData(address asset)— enforces the 2-hour guard and reverts past it. This is the safety-critical path; the per-asset feeds proxy to it.priceOf(address asset)— the raw stored value with no staleness check. Suitable for display, never for deciding whether an action will succeed.
FusorOracleFeed
One instance per self-priced asset. It holds the oracle address and the asset address as immutables and does nothing but forward. Two consequences worth knowing:
- A feed's asset binding is immutable. Pointing an asset at a different feed means deploying a new feed and updating the registry entry — the registry admin can do that without touching the launchpad.
- Because it is a pure pass-through, its staleness behaviour is the oracle's, not its own.
The keeper
An off-chain service fetches quotes and batches them into updatePriceBatch(). Its sourcing is deliberately conservative, because a wrong price written on-chain sets the opening price of a new pool and cannot be taken back:
| What | Source order | On total failure |
|---|---|---|
| Stock prices | Twelve Data first when a key is configured, Yahoo Finance as fallback | The asset is skipped for that cycle and keeps its previous on-chain price. |
| ETH/USD | DeFiLlama | Same — skipped, never fabricated. |
Three behaviours follow from that, and all three are visible from outside:
- Only self-priced assets get pushed. Chainlink-backed ones are skipped on purpose — pushing to them would achieve nothing and would consume quota.
- A price is cross-checked before it is written. Each quote is compared against an independent on-chain reference read by token address, so a symbol mix-up cannot slip through. If the two disagree by more than the configured tolerance, the keeper refuses to push rather than picking one. Disagreement means at least one of them is wrong, and an unrefreshed price merely blocks launches — a wrong one mis-prices a pool permanently.
- No value is ever invented. An asset with no usable quote is skipped and keeps whatever it had. It never receives a zero, a guess, or a stale-but-plausible substitute.
Checking freshness the way the contract does
The mistake worth avoiding: reading FusorPriceOracle directly for every asset and comparing against one hard-coded limit. That is not the code path the launchpad takes. To match it, resolve each asset's own feed — launchpad.ethPriceFeed() for ETH, and registry.getConfig(token).priceFeed for each stock token — call latestRoundData() on that feed, and compare against the live maxOracleAge(). Anything else can pass your check and then revert on-chain, which is the hardest class of bug to diagnose because both sides look correct in isolation.
Fees & Revenue
Two fees exist, and they behave nothing alike. One is a flat charge paid once by the creator. The other is a percentage of every trade that accrues inside locked liquidity and is later split between the creator and the protocol.
| Fee | Amount | Paid by | Goes to | When |
|---|---|---|---|---|
| Launch fee | Whatever launchFeeWei() returns — a deployment parameter | Creator, as msg.value | Protocol treasury | Once, in the launch transaction |
| Pool swap fee | 1% of the swap amount | Whoever trades | The locked V4 position, until collected | Every trade, forever |
launchFeeWei() in the session you launch in and send exactly that as msg.value — a mismatch in either direction reverts with IncorrectLaunchFee. It also is not the whole cost of launching: gas is separate.Swap fees do not go straight to anyone
The 1% accrues inside each locked Uniswap V4 position. Getting it into a claimable balance takes three distinct steps, and it is worth knowing which of them you need to do yourself:
| Stage | Call | Who can trigger it |
|---|---|---|
| 1 — Collect | FusorV4Locker.collectFees(tokenId) | Anyone. Our keeper does it on a schedule, but the function is permissionless — you never have to wait for us. |
| 2 — Allocate | Happens inside collection | Nobody. The 70 / 30 split is applied automatically as part of the same call. |
| 3 — Claim | FusorV4Locker.claim(asset) | The recipient, per asset. Balances sit there until claimed; nothing expires. |
The split
- 70% to the creator's fee recipient — the address given as
creatorFeeRecipientat launch. - 30% to the protocol treasury.
The split is applied separately to each asset. A trade against the AAPL market accrues both project tokens and AAPL, so both get split, and both end up as separate claimable balances.
Project-token fees stay project tokens
Fees collected in the project token are not swapped into anything on your behalf. You claim them as the project token and decide what to do with them. This is deliberate: an automatic swap would sell into the token's own pool, which is both a price impact you did not ask for and a decision the protocol has no business making for you.
Claiming
- Claims are per asset, so a multi-market token produces several independent balances.
- There is no minimum. A small position keeps accruing and becomes claimable as soon as collection credits anything non-zero.
- Nothing expires and nothing is swept away. An unclaimed balance simply stays unclaimed.
collectFees yourself — it is permissionless precisely so that nobody depends on our schedule.Fees charged elsewhere are not these fees
Fusor pools are public. An external router or aggregator can add a fee of its own and can route through unrelated Uniswap pools with their own fee tiers. None of that reaches the Fusor creator or treasury, and none of it appears in the table above. When comparing costs across interfaces, compare the whole route.
Graduation Milestone
The on-chain function is called syncGraduation, and the word invites a wrong assumption worth clearing up immediately: on most launchpads “graduation” means liquidity migrates from an internal curve to a public market. Here there is no internal curve and no migration. Graduation sets a boolean and changes nothing else about how the token works.
The target
At launch the contract records a USD target derived from a fixed 4.2 ETH, converted at the ETH/USD price in that block and stored with 8 decimals. It is fixed per token at creation — a later move in ETH does not re-price an existing token's target, and two tokens launched at different times will have different USD targets.
Progress
Progress compares the principal held in the token's canonical locked positions against that recorded target:
progress = locked principal ÷ recorded target
Principal only. Accrued fees, liquidity anyone added outside the launch positions, and cumulative trading volume are all excluded — none of them count toward the milestone.
Anyone can trigger it
syncGraduation(address projectToken) has no access control. Once a token qualifies, any account may submit the transaction and pay its gas. Our keeper attempts it for qualifying tokens as a convenience, but the contract does not call itself and nothing about the milestone depends on us being online.
What an interface can honestly display
Progress is computed from observations that can be stale, so there is a real difference between “qualifies” and “is flagged”. Four states, and conflating the middle two is the usual mistake:
| State | Means |
|---|---|
| Below target | The latest trustworthy observation puts progress under 100%. |
| Eligible | A fresh observation is at or above 100%, but no successful sync has been recorded yet. The token is not graduated. |
| Unknown | The observation is too old to make a claim from. An interface should say so rather than guess in either direction. |
| Graduated | A sync succeeded and the on-chain flag is set. Permanent. |
What it is not
- Not a price ceiling. The price can be anywhere above or below the target, before or after.
- Not a maximum market cap. The target is denominated in locked principal, which is a different quantity entirely.
- Not a liquidity event. Nothing unlocks, nothing is released, nothing is distributed.
- Not a quality signal. It measures deposited principal, which anyone can supply for any reason.
SDK Quickstart
Two SDKs are supported — TypeScript and Python — built from one shared spec so they encode identical calldata and produce identical metadata hashes. Pick whichever matches your stack; everything on this page applies to both.
npm install @fusor/sdk # Node 22+
pip install fusor # Python 3.11+Before you start
- A funded key. Enough ETH for the launch fee plus gas. The fee is read from the chain at call time, not a constant you can hard-code.
- A metadata URI, or an API base URL so the SDK can upload one for you.
- Nothing else. No API key, no registration, no allowlist, and no launchpad address — the SDK carries the deployment record.
Launch a token
This is the whole thing. One call signs and sends one transaction, waits for the receipt, and returns the token address read out of the event — so when it returns, the token exists on chain.
import { FusorClient } from '@fusor/sdk';
// No launchpad address, no API key, no registration — the deployment
// comes from the SDK's own spec, keyed by chain ID.
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const fusor = new FusorClient({ privateKey });
const launch = await fusor.launchToken({
name: 'My Token',
symbol: 'MTK',
markets: [
{ market: 'AAPL', weightBps: 6000 },
{ market: 'NVDA', weightBps: 4000 },
],
metadataURI: 'https://example.com/metadata.json',
});
// One transaction, already mined by the time this line runs.
console.log(launch.txHash);
console.log(launch.explorerUrl);
// Read out of the MultiPairLaunchCreated event in the receipt — the real
// address, not the prediction a dry run gives you.
console.log(launch.tokenAddress);
// What the launch actually cost, read from the chain rather than assumed.
console.log(launch.launchFeeWei);
// Which implementation the proxy pointed at. Log it — it changes.
console.log(launch.implementation);import os
from fusor import FusorClient
# No launchpad address, no API key, no registration — the deployment
# comes from the SDK's own spec, keyed by chain ID.
fusor = FusorClient(private_key=os.environ["PRIVATE_KEY"])
launch = fusor.launch_token(
name="My Token",
symbol="MTK",
markets=[("AAPL", 6000), ("NVDA", 4000)],
metadata_uri="https://example.com/metadata.json",
)
# One transaction, already mined by the time this line runs.
print(launch.tx_hash)
print(launch.explorer_url)
# Read out of the MultiPairLaunchCreated event in the receipt — the real
# address, not the prediction a dry run gives you.
print(launch.token_address)
# What the launch actually cost, read from the chain rather than assumed.
print(launch.launch_fee_wei)
# Which implementation the proxy pointed at. Log it — it changes.
print(launch.implementation)markets: ['AAPL', 'NVDA']) the SDK splits evenly; give every market a weight or none of them, since mixing the two is ambiguous and is rejected locally before any request is made. One to five markets, weights in basis points summing to 10000.When it fails
Which of the three error types you get tells you whether anything was spent and whether retrying can possibly help. This distinction is the whole reason they are separate types — a single catch-and-retry is how you burn gas on a call that will never succeed.
import {
FusorConfigError, FusorPreflightError, FusorLaunchError,
} from '@fusor/sdk';
try {
const launch = await fusor.launchToken({ /* … */ });
return launch.tokenAddress;
} catch (e) {
// Your input. Nothing was sent, nothing was spent, and the identical call
// will fail identically forever. Never retry this one.
if (e instanceof FusorConfigError) throw e;
// A chain precondition failed during preflight — the transaction was never
// built. Nothing was spent. Retrying later can work; retrying now will not.
if (e instanceof FusorPreflightError) return scheduleRetry(e);
// The contract rejected it. Branch on errorName, never on the message text.
if (e instanceof FusorLaunchError) {
switch (e.errorName) {
case 'IncorrectLaunchFee': // the fee changed under you
case 'DeadlineExpired': // sat in the mempool past the deadline
return retryNow(); // launchToken re-reads every input
case 'StaleOracle': // a feed went stale mid-flight
case 'StockTokenDisabled': // the market was turned off
return scheduleRetry(e); // may clear on its own
default:
throw e;
}
}
throw e;
}from fusor import FusorConfigError, FusorPreflightError, FusorLaunchError
try:
launch = fusor.launch_token(...)
except FusorConfigError:
# Your input. Nothing was sent, nothing was spent, and the identical call
# will fail identically forever. Never retry this one.
raise
except FusorPreflightError as e:
# A chain precondition failed during preflight — the transaction was never
# built — nothing was spent. Retrying later can work; now will not.
schedule_retry(e)
except FusorLaunchError as e:
# Contract rejected it. Branch on error_name, never the message text.
if e.error_name in ("IncorrectLaunchFee", "DeadlineExpired"):
retry_now() # launch_token re-reads every input
elif e.error_name in ("StaleOracle", "StockTokenDisabled"):
schedule_retry(e) # may clear on its own
else:
raise- Nothing spent, never retry.
FusorConfigError— bad input, caught before any request. - Nothing spent, retry later.
FusorPreflightError— a chain precondition, usually oracle freshness. - Gas possibly spent, depends on the name.
FusorLaunchError— the contract reverted; all 32 custom errors are decoded to a name.
What one call actually does
launchToken is not a thin wrapper around eth_sendTransaction. It runs seven steps, and every one of them reads live chain state rather than trusting a cached or hard-coded value:
| # | Step | Why it is there |
|---|---|---|
| 1 | Preflight | Confirms the proxy is a Fusor launchpad and reads its live parameters — fee, staleness limit, registry, feed addresses. |
| 2 | Resolve markets | Turns symbols into addresses, checks the weights locally, then reads getConfig on each one. |
| 3 | Oracle freshness | Reads each asset’s own feed, so a StaleOracle revert becomes “which market, how old”. |
| 4 | Metadata | Uses your URI as-is, or uploads for you if you configured an API base URL. |
| 5 | Build params | Fills the 11-field struct, including the skip sentinels for the developer buy. |
| 6 | Simulate | eth_call against pending state. Reverts here cost nothing and are decoded to a named error. |
| 7 | Send and confirm | Signs the exact simulated request, waits for the receipt, then reads the address from the event. |
Steps 1 through 6 are also exported individually, so the signing step can live in a KMS, a hardware wallet or a multisig instead of in your process — see SDK Recipes.
Check before you send
Adding dryRun stops after step 6. It sends no transaction and spends nothing, but it does exercise the real contract against real state — so it is worth running once in staging, or before a launch you cannot repeat. It is not a required step: the same simulation runs inside every real launch.
const preview = await fusor.launchToken({
name: 'My Token',
symbol: 'MTK',
markets: [
{ market: 'AAPL', weightBps: 6000 },
{ market: 'NVDA', weightBps: 4000 },
],
metadataURI: 'https://example.com/metadata.json',
dryRun: true,
});
console.log(preview.launchFeeWei); // what it will cost, read from the chain
console.log(preview.estimatedGas);
console.log(preview.prices); // every feed the launch depends on
console.log(preview.tokenAddress); // predicted only — see the warning belowpreview = fusor.launch_token(
name="My Token",
symbol="MTK",
markets=[("AAPL", 6000), ("NVDA", 4000)],
metadata_uri="https://example.com/metadata.json",
dry_run=True,
)
print(preview.launch_fee_wei) # what it will cost, read from the chain
print(preview.estimated_gas)
print(preview.prices) # every feed the launch depends on
print(preview.token_address) # predicted only — see the warning belowSDK Recipes has a read-only probe for that, and it needs no signer.SDK Concepts
Six decisions shape how these SDKs behave. Each one exists because the obvious alternative fails in a specific way, so they are worth reading before you build around them.
1. Bring your own signer
The default is a raw private key, because the primary use case is an unattended process. But the core only needs something that can sign a transaction, so a KMS, a hardware signer, or a multisig flow all drop in. No signer at all is also valid — every read path works without one.
// 1) A raw key — bots, backends, CI
new FusorClient({ privateKey: process.env.PRIVATE_KEY as `0x${string}` });
// 2) Any viem Account — KMS, hardware, multisig, a custom signer
new FusorClient({ account: myKmsAccount });
// 3) No signer at all — read-only calls still work
const readonly = new FusorClient();
await readonly.preflight();
readonly.listQuoteMarkets();# 1) A raw key
FusorClient(private_key=os.environ["PRIVATE_KEY"])
# 2) Any eth-account LocalAccount
FusorClient(account=my_local_account)
# 3) No signer at all — read-only calls still work
readonly = FusorClient()
readonly.preflight()
readonly.list_quote_markets()2. Nothing about the deployment is hard-coded
The launchpad is an upgradeable proxy, and several of its numbers are deployment parameters rather than protocol constants. The SDK reads all of them on every launch:
| Value | Read from | What hard-coding it would cause |
|---|---|---|
| Implementation address | The proxy’s ERC-1967 slot | Every user breaks the moment the proxy is upgraded. |
| launchFeeWei | launchpad.launchFeeWei() | Every launch reverts with IncorrectLaunchFee once the fee changes. |
| maxOracleAge | launchpad.maxOracleAge() | Markets get wrongly reported as stale — or wrongly as fresh. |
| Registry address | launchpad.stockRegistry() | Eligibility checked against the wrong allowlist. |
| ETH price feed | launchpad.ethPriceFeed() | Freshness checked against a feed the contract does not use. |
3. The predicted address is not the real address
Simulation returns the address the token would receive at the launchpad's current nonce. Another launch landing first changes it. After sending, the SDK reads the address from the MultiPairLaunchCreated event in the receipt and only accepts the event if the launchpad proxy emitted it — anyone can emit an event with the same signature.
receipt.status and raises if the transaction reverted. This matters more than it sounds: a reverted transaction returns a completely normal receipt, so code that skips the check reads an empty log list, gets undefined for the address, and reports a failed launch as a success.4. Errors are typed and named
Three classes tell you whose problem it is, and a decoded errorName tells you which contract condition failed. All 32 of the launchpad's custom errors are decodable, which is the difference between “execution reverted” and WeightsNotOneHundredPercent.
| Class | Cause | Retrying helps? |
|---|---|---|
| ConfigError | Your input. Caught locally, before any request. | Only after you change the input. |
| PreflightError | Chain state — stale price, missing feed, wrong address. | Sometimes. A stale price becomes fresh. |
| LaunchError | The contract rejected the transaction. | Depends on errorName. |
import { FusorError, FusorConfigError, FusorPreflightError, FusorLaunchError } from '@fusor/sdk';
try {
await fusor.launchToken({ /* … */ });
} catch (e) {
if (e instanceof FusorConfigError) { /* your input — fix and retry immediately */ }
if (e instanceof FusorPreflightError) { /* chain state — retry later may work */ }
if (e instanceof FusorLaunchError) { /* the contract rejected it */ }
// Branch on errorName, never on the message text.
if (e instanceof FusorError && e.errorName === 'StaleOracle') {
await waitForKeeper();
}
}from fusor import FusorError, FusorConfigError, FusorPreflightError, FusorLaunchError
try:
fusor.launch_token(...)
except FusorConfigError:
... # your input — fix and retry immediately
except FusorPreflightError as e:
if e.error_name == "StaleOracle":
wait_for_keeper()
except FusorLaunchError as e:
... # the contract rejected it; e.error_name is set when decodableerrorName, never on message text. The messages are written for humans and will be reworded; the names come from the contract ABI and are stable.5. Metadata has a zero-network path
Pass metadataURI and the SDK makes no HTTP request at all — it needs nothing but an RPC endpoint, which is what lets a launch bot be fully self-hosted. Pass a metadata object plus an API base URL instead and the SDK uploads for you.
- The hash is computed over the bytes we send, not taken from the upload response. If a server reordered fields or added defaults, its hash would not describe what the URI serves — and the on-chain commitment is permanent.
- Field order in the canonical JSON is fixed. The hash is over exact bytes, so both SDKs emit the same key order, the same separators, and no HTML escaping. Golden vectors enforce this across languages.
- Neither is not an option. Omitting both raises rather than silently launching a token with no name, image, or links anywhere off-chain.
6. The SDKs cannot drift apart
Addresses, ABIs, selectors, constants and the quote-asset table are generated from the compiled contracts into one spec, then distributed into every package. On top of that, three sets of golden vectors are asserted by both test suites:
| Vector set | Asserts | Failure it prevents |
|---|---|---|
| launch-calldata | Byte-identical encoded calldata | An offset error in a hand-written encoder — which shows up as a revert with no apparent connection to the cause. |
| metadata-hash | Byte-identical canonical JSON and hash | Python emitting ASCII escapes and padded separators by default, or a language escaping HTML in JSON. |
| oracle-staleness | Identical freshness classification | Integer semantics diverging — one language wrapping a negative into an enormous positive, another treating it as fresh. |
A separate structural test asserts that the spec copies inside every package are byte-identical to the source. Without it, editing the spec and forgetting to regenerate would leave every SDK using stale addresses while every existing test stayed green — each package is self-consistent even when all of them are wrong.
SDK Recipes
Find out what is launchable right now
Do this before offering a market to a user. Roughly half the registered stock tokens are priced by our own keeper, and those have an effective two-hour freshness window — so “is this asset registered” and “can I launch against it in this block” are genuinely different questions.
import { FusorClient, checkOracleFreshness, FusorPreflightError } from '@fusor/sdk';
const fusor = new FusorClient(); // no signer needed to read
const pre = await fusor.preflight();
async function launchable(symbol: string) {
const asset = fusor.listQuoteMarkets().find((a) => a.symbol === symbol);
if (!asset) return { symbol, ok: false, why: 'unknown symbol' };
try {
await checkOracleFreshness(fusor.publicClient, {
ethPriceFeed: pre.ethPriceFeed,
stockRegistry: pre.stockRegistry,
tokens: [{ symbol, token: asset.token }],
maxAgeSeconds: pre.maxOracleAgeSeconds,
});
return { symbol, ok: true };
} catch (e) {
return { symbol, ok: false, why: e instanceof FusorPreflightError ? e.message : String(e) };
}
}
console.log(await Promise.all(['AAPL', 'NVDA', 'AMC'].map(launchable)));from fusor import FusorClient, check_oracle_freshness, FusorPreflightError, quote_asset_by_symbol
fusor = FusorClient() # no signer needed to read
pre = fusor.preflight()
def launchable(symbol: str):
asset = quote_asset_by_symbol(symbol)
if asset is None:
return symbol, False, "unknown symbol"
try:
check_oracle_freshness(
fusor.w3,
eth_price_feed=pre.eth_price_feed,
stock_registry=pre.stock_registry,
tokens=[(symbol, asset.token)],
max_age_seconds=pre.max_oracle_age_seconds,
)
return symbol, True, None
except FusorPreflightError as e:
return symbol, False, str(e)
for s in ("AAPL", "NVDA", "AMC"):
print(launchable(s))maxOracleAge() reports as stale with an age. A self-priced asset past two hours fails on the feed read itself, because the oracle reverts internally — the message says it could not read the feed. Both mean “not launchable now”; only the second is likely to resolve within minutes.Let the SDK upload your metadata
Configure an API base URL and pass a metadata object instead of a URI. Images go up first as a data URL and only the resulting link is committed on-chain — image bytes never touch the chain.
const fusor = new FusorClient({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
apiBaseUrl: 'https://fusor.fun',
});
const { tokenAddress } = await fusor.launchToken({
name: 'My Token',
symbol: 'MTK',
markets: ['AAPL'],
metadata: {
description: 'What this token is for.',
image: 'data:image/png;base64,iVBORw0KGgo…', // PNG, JPEG or WebP
twitter: 'https://x.com/example',
website: 'https://example.com',
},
});fusor = FusorClient(
private_key=os.environ["PRIVATE_KEY"],
api_base_url="https://fusor.fun",
)
result = fusor.launch_token(
name="My Token",
symbol="MTK",
markets=["AAPL"],
metadata={
"description": "What this token is for.",
"image": "data:image/png;base64,iVBORw0KGgo…", # PNG, JPEG or WebP
"twitter": "https://x.com/example",
"website": "https://example.com",
},
)- PNG, JPEG and WebP are accepted; anything else is rejected before a request is made.
- An
https://image URL is passed through untouched — no upload happens. - The committed hash is computed over the exact bytes the SDK sent, not over the server's reply.
Use your own RPC
The default endpoint is public and rate-limited. Anything doing volume should point at its own node; some providers also need an auth header or an allowlisted origin.
const fusor = new FusorClient({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
rpcUrl: 'https://my-node.example/rpc',
// Some providers authenticate by header, or require an allowlisted Origin.
rpcHeaders: { Authorization: `Bearer ${process.env.RPC_TOKEN}` },
});Keep the key somewhere else
Every step of the launch is also exported as a standalone function, so you can run the read-and-validate part in your service and hand only the final signing step to whatever custody you use. Nothing is hidden behind the client class.
import {
preflight, resolveMarkets, checkOracleFreshness,
resolveMetadata, buildLaunchParams, simulateLaunch,
sendLaunch, tokenAddressFromReceipt,
} from '@fusor/sdk';
// Every step is exported separately, so the signing step can live anywhere —
// a KMS, a hardware wallet, a multisig proposal, an air-gapped machine.
const pre = await preflight(publicClient, launchpad);
const markets = await resolveMarkets(publicClient, pre.stockRegistry, ['AAPL', 'NVDA']);
await checkOracleFreshness(publicClient, {
ethPriceFeed: pre.ethPriceFeed,
stockRegistry: pre.stockRegistry,
tokens: markets,
maxAgeSeconds: pre.maxOracleAgeSeconds,
});
const { metadataURI, metadataHash } = await resolveMetadata({
name: 'My Token', symbol: 'MTK', metadataURI: 'https://example.com/m.json',
});
const params = buildLaunchParams({
name: 'My Token', symbol: 'MTK', metadataURI, metadataHash, markets,
creator: signerAddress,
deadline: BigInt(Math.floor(Date.now() / 1000) + 1800),
});
const simulated = await simulateLaunch(publicClient, {
launchpad, account: signerAddress, params, launchFeeWei: pre.launchFeeWei,
});
// …hand `simulated.request` to whatever holds the key, then:
const hash = await sendLaunch(walletClient, simulated, account);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
const tokenAddress = tokenAddressFromReceipt(receipt, launchpad);Things worth getting right
- Re-run preflight per launch. Do not cache it across launches. The proxy can be upgraded and the fee can change between two calls, and a cached preflight is how you sign against parameters that no longer hold.
- Keep deadlines short. The default is 30 minutes. A long deadline lets a transaction sit in the mempool while prices move; the deadline is the guard against exactly that.
- Never trust the predicted address. Read it from the receipt event, and verify the event came from the launchpad proxy.
- Do not retry blindly on failure. A
ConfigErrorwill fail identically forever. Only aPreflightErrorabout freshness is worth retrying, and then only after waiting.
REST API
A read-mostly HTTP API over indexed chain data, plus a few helpers for uploads and reference prices. Everything is under /api. Most endpoints need no authentication at all.
Conventions
| Aspect | Behaviour |
|---|---|
| Addresses | Accepted checksummed or lowercase; compared case-insensitively. |
| Paging | page and limit query params, with a server-side maximum on limit. |
| Errors | Non-2xx with a JSON body. Rate limiting returns 429 with Retry-After. |
| Auth | Read endpoints are open. Profile and watchlist writes need a one-time signed challenge. Admin routes need an allowlisted wallet. |
| Caching | Content-addressed responses (images, metadata) are cached immutably; indexed data is not. |
Write endpoints use single-use challenges
Anything that mutates a profile or a watchlist requires a signature over a challenge issued moments earlier. The pattern is deliberate and has two consequences you have to design around:
- A challenge expires in five minutes, so fetch it immediately before asking the user to sign — not when your form loads.
- Each challenge works exactly once. Retrying a failed request needs a new challenge, not the same one again.
Tokens
/api/tokensLists launched tokens with paging, sorting, filtering and free-text search. Tokens marked hidden by moderation are omitted.
Query Parameters
| page | number | Page number (default: 1) |
| limit | number | Results per page, max 50 (default: 20) |
| sort | string | Sort field: newest (default) | market_cap | volume_24h | progress | recent_buy | creator_fees | holders |
| filter | string | Filter set: all (default) | recent_buys | newly_launched | market_cap |
| timeframe | string | Limit to tokens launched in: all (default) | 24h | 7d |
| quoteToken | address | Filter by quote stock token address |
| graduated | boolean | true = confirmed on-chain graduation flag; false = not yet confirmed |
| search | string | Search by name, symbol, token address, or creator address |
Response
{ items: Token[], total: number, page: number, limit: number }
/api/tokens/:addressEverything known about one token: identity, metadata, its markets, and milestone progress with the block it was observed at plus a freshness label. Legacy tokens report their own progress shape instead.
Path Parameters
:addressToken contract address (checksummed or lowercase)Response
Token object. Public fields include launchTxHash. V5 fields include combinedGraduationProgress, graduationProgressObservedBlock, graduationProgressObservedAt, graduationProgressFreshness (fresh | stale | unknown), and the sticky graduated flag.
/api/tokens/graduatedTokens whose on-chain graduated flag is actually set, newest first. A token sitting at 100% that nobody has synced yet does not appear here — eligibility and the flag are different states.
Response
Token[] — subset of tokens where graduated = true
/api/tokens/:address/tradesSwap history for one token, newest first, paged.
Path Parameters
:addressToken contract addressQuery Parameters
| page | number | Page number (default: 1) |
| limit | number | Results per page (default: 50) |
| filter | string | buys | sells (omit for all trades) |
Response
{ items: Trade[], total: number, page: number, limit: number }. Each Trade includes txHash, wallet, isBuy, amountIn, amountOut, priceUsd, valueUsd, timestamp, and a Blockscout explorerUrl.
/api/tokens/:address/candlesOHLCV candles for charting, at the requested interval.
Path Parameters
:addressToken contract addressQuery Parameters
| resolution | string | Candle size: 1 | 5 | 15 | 60 | 240 | 1D |
| from | unix timestamp | Start of range (inclusive) |
| to | unix timestamp | End of range (inclusive) |
Response
Candle[] — up to 1,000 candles. Each candle: { timestamp, open, high, low, close, volume, quoteVolume, tradeCount }
/api/tokens/:address/feesIndexed fee state for one token — what has been collected, what has been allocated, broken down per market so a multi-market token’s pools can be told apart.
Path Parameters
:addressToken contract addressResponse
{ tokenAddress, pendingProjectTokenFees, pendingQuoteTokenFees, totalCreatorFeesCollected, totalProtocolFeesCollected, lastCollectedAt, lastConvertedAt, aggregateUsd, pairs[] }. Each pair includes project/quote pending amounts plus gross-collected and 70/30 allocated counters.
/api/tokens/:address/metricsMarket metrics for one token in a single response — price, market cap, volume, holder count, milestone progress — for callers that want the numbers without the full token object.
Path Parameters
:addressToken contract address (checksummed or lowercase)Response
{ address, priceQuote, priceUsd, marketCapUsd, fdvUsd, volume24hUsd, volume24hQuote, change24hPct, change1hPct, holders, trades24h, progress, graduationProgressObservedBlock, graduationProgressObservedAt, graduationProgressFreshness, quoteInPool, graduationTargetUsd, liquidityUsd, totalDepthUsd, … }
Quote Assets
/api/stock-tokensThe quote-asset allowlist as this site presents it, ordered by symbol. Note that a launch depends on the on-chain registry, not on this response: read enabled here as a display hint and verify against the contract before acting.
Response
StockToken[] — { address, symbol, decimals, enabled, ethRouteEnabled, priceFeedAddress, graduationTargetUsdE8, logoUrl, launchedTokenCount }
/api/stock-tokens/:addressOne quote asset by its token address.
Path Parameters
:addressStock token contract addressResponse
StockToken object
Fees
/api/fees/claimable/:walletFee balances a wallet can claim right now, gathered across every locker generation the protocol has used — so an older token’s fees are not silently missing.
Path Parameters
:walletWallet address (checksummed or lowercase)Response
ClaimableFeeEntry[] — live non-zero balances with quoteToken metadata, claimableAmount, claimableAmountUsd, lockerAddress, assetType (PROJECT | QUOTE), projectTokenAddress, claimAssetAddress, claimFunction, and project-token conversionRoutes.
/api/fees/pending/:walletFees that have accrued inside positions but have not been collected yet, shown as the creator’s 70% share. These are not claimable until someone calls collect — which anyone may do.
Path Parameters
:walletWallet addressResponse
PendingFeeEntry[] — { tokenAddress, tokenSymbol, tokenImageUrl, quoteTokenAddress, pendingCreatorQuote, pendingAmountUsd }
/api/fees/history/:walletPast claim transactions for a wallet, paged.
Path Parameters
:walletWallet addressQuery Parameters
| page | number | Page number (default: 1) |
| limit | number | Results per page (default: 20) |
Response
{ items: FeeClaimEvent[], total, page, limit }. Each item: { txHash, quoteToken, amount, amountUsd, timestamp }
/api/fees/manifest/:walletReturns a batch manifest of a wallet's own launched (non-imported) tokens with per-pair locker/position/quote-token metadata, so the client can construct its own claim transactions. Read-only — the server never signs on the wallet's behalf.
Path Parameters
:walletWallet address (checksummed or lowercase)Query Parameters
| tokens | string | Optional comma-separated token address allowlist, up to 100; only addresses the wallet actually created are included regardless |
Response
{ wallet, readOnly: true, serverSigning: false, projects: [{ tokenAddress, symbol, marketVersion, sweeps: [{ lockerAddress, positionTokenId, quoteTokenAddress, capabilities }] }] }
Profiles
/api/profiles/:walletReturns a user profile. The website routes /portfolio to the connected wallet's /profile/:wallet page. Only that wallet can edit the profile, using a fresh one-time signed challenge with no gas transaction.
Path Parameters
:walletWallet addressResponse
{ wallet, username, bio, imageUrl, twitterHandle, createdTokenCount, totalTradesCount, createdAt }
/api/profiles/:wallet/challengeIssues a single-use challenge, valid five minutes, that a profile or watchlist change must be signed against. Request it immediately before signing — a challenge held too long expires, and each one works exactly once.
Path Parameters
:walletWallet address that must sign the mutationRequest Body (JSON)
| action | string | profile:update | watchlist:update |
Response
{ nonce, expiresAt } — include both values in the canonical Fusor Ownership v1 message and signed mutation.
/api/profiles/:walletCreates or updates a profile. Requires a signature over the canonical ownership message from the same wallet, carrying a challenge that has not been used before.
Path Parameters
:walletWallet addressRequest Body (JSON)
| profile | object | { username?, bio?, imageUrl?, twitterHandle? } |
| nonce | string | Nonce returned by the matching challenge |
| expiresAt | date-time | Exact challenge expiry returned by the API |
| signature | hex string | EIP-191 signature of the canonical profile-update message |
Response
Updated Profile object
/api/profiles/:wallet/tokensTokens this wallet launched, newest first. Tokens merely imported by an admin are excluded — this answers “what did they create”, not “what is associated with them”.
Path Parameters
:walletWallet addressQuery Parameters
| page | number | Page number (default: 1) |
| limit | number | Results per page (default: 20) |
Response
{ items: Token[], total, page, limit }
/api/profiles/:wallet/holdingsIndexed non-zero balances for this wallet. Kept for API consumers; the profile page in this interface derives its holdings differently.
Path Parameters
:walletWallet addressResponse
Holding[] — { token, balance (wei string), balanceUsd, percentOfSupply }
/api/profiles/:wallet/tradesIndexed trades by this wallet across all tokens. Kept for API consumers; this interface does not surface it.
Path Parameters
:walletWallet addressQuery Parameters
| page | number | Page number (default: 1) |
| limit | number | Results per page (default: 50) |
Response
{ items: Trade[], total, page, limit }. Each trade includes tokenAddress, tokenSymbol, tokenName, and quoteTokenSymbol in addition to standard trade fields.
/api/profiles/:wallet/watchlistReturns tokens on a wallet's watchlist, most recently added first.
Path Parameters
:walletWallet addressResponse
Token[]
/api/profiles/:wallet/watchlistAdds or removes one token from a wallet’s watchlist. Requires a fresh single-use watchlist challenge signed by that wallet.
Path Parameters
:walletWallet addressRequest Body (JSON)
| watchlist | object | { tokenAddress, watching } |
| nonce | string | Nonce returned by the matching challenge |
| expiresAt | date-time | Exact challenge expiry returned by the API |
| signature | hex string | EIP-191 signature of the canonical watchlist-update message |
Response
{ tokenAddress, watching }
Stats
/api/stats/platformAggregate counters computed from indexed data — launches, volume, fees, and similar.
Response
{ totalTokensLaunched, totalTradesAllTime, totalVolumeUsd, totalFeesDistributedUsd, graduatedCount, activeTokens24h }
/api/stats/trendingThe most actively traded visible tokens over the last 24 hours.
Query Parameters
| limit | number | Number of tokens to return, max 20 (default: 10) |
Response
Token[] sorted by 24h trade count descending
Prices
/api/pricesLive reference quotes for one or more symbols, used by the launch interface for display. Stock symbols come from Yahoo Finance and ETH from DeFiLlama. These are display prices only — they are not what the contracts read, so never treat a value here as evidence that a launch will pass the on-chain freshness check.
Query Parameters
| symbols | string | Comma-separated Yahoo Finance symbols, e.g. ETH-USD,AAPL,TSLA |
Response
{ "AAPL": 234.56, "ETH-USD": 3012.00 } — null for symbols not found. Response includes Cache-Control: public, max-age=30.
Images & Metadata
/api/imagesStores a raster image and returns a stable URL. The upload is validated by magic bytes rather than by the declared type, and SVG is refused outright because it can carry script.
Request Body (JSON)
| data | string | Strict base64-encoded image data (no data: prefix), up to 1 MiB decoded |
| contentType | string | Matching raster MIME type: image/png, image/jpeg, or image/webp |
Response
{ url: string, hash: string } — URL is a permanent /api/images/<hash> path.
/api/images/:hashServes a stored image as raw bytes with the right content type. Content-addressed, so the response is cached immutably.
Path Parameters
:hashSHA-256 hex hash returned by POST /api/imagesResponse
Raw image bytes
/api/metadataStores token metadata and returns the URL to commit as the token’s metadataURI on-chain. Because the on-chain hash covers exact bytes, submit the same canonical JSON you intend to hash — do not reformat it afterwards.
Request Body (JSON)
| name | string | Token name |
| symbol | string | Token symbol |
| description | string? | Token description |
| image | string? | Image URL (from POST /api/images) |
| links.twitter | string? | Twitter/X profile URL |
| links.telegram | string? | Telegram URL |
| links.website | string? | Website URL |
Response
{ url: string, hash: string } — url is the permanent metadata URI written on-chain.
/api/metadata/:hashServes stored metadata in the DexScreener token-info schema, CORS-open and immutably cached, so external explorers can read it directly.
Path Parameters
:hashSHA-256 hex hash returned by POST /api/metadataResponse
{ schemaVersion: "1.0.0", icon?, description?, links?: [{ type, label, url }] }
Launch Registration
/api/launches/register-v5仅从已确认的交易回执推导并注册一次发射。返回 201 与 token,或 202 表示确认阈值未达到。
Request Body (JSON)
| txHash | hex string | 已确认的发射交易哈希 |
Response
{ status: 'registered', token } | 202 { status: 'pending' }
/api/launches/v5-beta-status在不可逆的浏览器端工作前,校验 API/索引器是否认识给定的 launchpad 代理。响应不可用或格式错误一律 fail closed。
Query Parameters
| launchpadAddress | address | 待校验的 launchpad 代理地址 |
Response
{ recognized: boolean, ... }
Health
/api/healthzLiveness probe. Returns 200 while the process is up; it says nothing about the database.
Response
{ status: "ok" }
Admin
/api/admin/treasury协议金库余额。需要签名的 admin 请求头,钱包须在 ADMIN_WALLETS 白名单内。
Response
TreasuryBalance[]
/api/admin/tokens/import按地址导入一个外部代币,身份信息通过实时 ERC-20 读取获得。需要 admin 鉴权。
Request Body (JSON)
| address | address | 代币合约地址 |
Response
Token object
/api/admin/tokens/:address/moderate设置代币的 hidden / flagged 标记。需要 admin 鉴权与 reason。
Path Parameters
:address代币合约地址Request Body (JSON)
| hidden | boolean? | 从公开列表隐藏 |
| flagged | boolean? | 标记为可疑 |
| reason | string | 必填 —— 审核操作要留痕 |
Response
Token object
Deployed Addresses
Everything below is on Robinhood Chain (chain ID 4663). One address is the entry point — the launchpad proxy. The rest are here so that a transaction trace or an event log makes sense, not because you need to call them.
Fusor protocol
| Contract | Address | Source |
|---|---|---|
| FusorLaunchpad (Proxy) | 0xc46ce20e47709d38044bd507275f10f0b06376c4 | The only entry point — integrate against this, never the implementation |
| — Implementation (current) | 0x62d281caea772897d988008f8c81b86b75c051d9 | Read live from the proxy’s ERC-1967 slot — changes on upgrade |
| FusorLaunchFacet | 0xd5300bbefdfdf97ba626a8a24bae5045fe826b42 | Immutable constructor arg of the implementation; holds the launchTokenMulti body |
| FusorV5MultiPoolAggregator | 0x94c0f173b0481b085981605c2d096bb0e1fe1347 | Permissionless balanced-routing execution contract |
| FusorV4Hook | 0x9e7352833bbac3c620ed201764fe21701174a000 | Read from launchpad.pairHook() — low 14 bits encode beforeInitialize (0x2000) |
| FusorV4Locker | 0xffeb663f6359dd7609726097a604b186e7213da3 | Read from launchpad.locker() — all LP positions are permanently locked here |
| FusorV4DeveloperBuyAdapter | 0xbc1457d94dec5590955ded1d47b49a3cde63557c | Read from launchpad.developerBuyAdapter() — developer buy is not enabled in this release |
| FusorPriceOracle | 0x7b71b20309676073a2dfd044d19650ac7c0ce0d1 | Serves only the self-priced quote assets; the rest read official Chainlink feeds directly |
| ETH/USD price feed | 0x78f3556b67e17df817d51ef5a990cdaf09e8d3a9 | Official Chainlink feed — read from launchpad.ethPriceFeed(); no setter exists |
| StockTokenRegistry (Proxy) | 0xd6f2b4bc593ab14391e960be1d7f81c3a4340a60 | Read from launchpad.stockRegistry() — the quote-asset allowlist; no setter exists |
| Protocol Treasury | 0xcc4da2f57a0818c7b1b8fa24879cf81b60e92e82 | Read from launchpad.protocolTreasury() — no setter exists |
Uniswap V4 (Robinhood Chain)
| Contract | Address | Source |
|---|---|---|
| PoolManager | 0x8366a39cc670b4001a1121b8f6a443a643e40951 | Cross-checked against the immutable slots of our own deployed contracts |
| PositionManager | 0x58daec3116aae6d93017baaea7749052e8a04fa7 | The canonical V4 deployment on this chain |
| StateView | 0xf3334192d15450cdd385c8b70e03f9a6bd9e673b | The canonical V4 deployment on this chain |
| Universal Router | 0x8876789976decbfcbbbe364623c63652db8c0904 | Read from launchpad.universalRouter() — note the non-standard minHopPriceX36 field |
| V4Quoter | 0x8dc178efb8111bb0973dd9d722ebeff267c98f94 | No on-chain getter exposes this one — it is a client-side constant, so verify it against the explorer before relying on it |
| Permit2 | 0x000000000022d473030f116ddee9f6b43ac78ba3 | Read from launchpad.permit2() — canonical cross-chain address |
Robinhood Universal Router compatibility
ExactInputSingleParams tuple includes a uint256 minHopPriceX36field immediately before bytes hookData. Fusor sets that optional field to 0and uses amountOutMinimum for the 1% minimum-received guard. Omitting the extra field misaligns the remaining calldata and causes the router call to revert.Direct swaps execute the V4 action sequence SWAP_EXACT_IN_SINGLE → SETTLE → TAKE(0x060b0e). SETTLE pays the full input-token debt from the connected wallet, and TAKE sends the full output-token credit to that wallet. Integrators should validate calldata against the deployed Robinhood router ABI rather than copying the standard Uniswap struct.
Robinhood Chain
- Chain ID: 4663
- RPC:
https://rpc.mainnet.chain.robinhood.com - Explorer: robinhoodchain.blockscout.com
Constants & Limits
Every number the protocol enforces, in one place. The split below matters more than any individual value: some of these are compiled into the contracts and can be relied on, and some are set at deployment and must be read.
Read these — do not embed them
These are deployment parameters. They are correct only for the deployment you are talking to, and they can change. The table gives you the getter, deliberately not the number.
| Value | Read from | What goes wrong if you hard-code it |
|---|---|---|
| Launch fee | launchpad.launchFeeWei() | Every launch reverts with IncorrectLaunchFee — msg.value must match exactly, in both directions. |
| Oracle staleness limit | launchpad.maxOracleAge() | Markets get reported as stale when they are fine, or fresh when they are not. |
| Registry address | launchpad.stockRegistry() | Eligibility checked against the wrong allowlist. |
| ETH price feed | launchpad.ethPriceFeed() | Freshness checked against a feed the contract does not consult. |
| Implementation address | The proxy’s ERC-1967 slot | Everything breaks the moment the proxy is upgraded. |
| A quote asset’s price feed | registry.getConfig(token).priceFeed | Same as above, per asset — and each one can be swapped independently. |
Protocol constants
These are compiled in. They do not vary by deployment and are safe to rely on.
| Constant | Value | Meaning |
|---|---|---|
| Total supply | 1,000,000,000 | Every token, always. No mint function exists. |
| Max quote markets | 5 | Upper bound on markets per launch. Minimum is 1. |
| Basis-point denominator | 10000 | Allocations must sum to exactly this — no rounding tolerance. |
| Skip-developer-buy sentinel | 255 | The value for developerBuyPairIndex that means “no developer buy”. |
| Max developer buy | 550 bps (5.5%) | Ceiling on a developer buy when the feature is enabled. |
| Milestone base | 4.2 ETH | Converted to USD at launch-block price and stored per token. |
| Oracle price decimals | 8 | Chainlink convention — $200.00 is 20000000000. |
| Self-priced staleness | 2 hours | FusorPriceOracle.MAX_STALENESS. Hard-coded; the feed reverts past it. |
Launch protection window
| Limit | Value | Notes |
|---|---|---|
| Window length | Launch block + 5 blocks | Measured in blocks, not seconds. Lifts automatically. |
| Max single buy | 5.5% of supply — 55,000,000 | Per acquisition. |
| Max wallet holding | 5.0% of supply — 50,000,000 | Checked after each acquisition. |
| Selling | Unrestricted | No limit at any point. |
Fees
| Fee | Value |
|---|---|
| Pool swap fee | 1% of every swap |
| Creator share of collected fees | 70% |
| Protocol share of collected fees | 30% |
| Minimum claimable balance | None — any non-zero balance can be claimed |
| Launch fee | Deployment parameter — see the first table |
Routing
| Limit | Value |
|---|---|
| Max pool legs per trade | 5 |
| Max live price impact per route | 15% — routes above this are rejected |
| Max deadline | 1 day ahead |
| Common asset for balanced routes | USDG |
Chain and encoding
- Chain ID: 4663
- launchTokenMulti selector:
0x07489e0a— the 11-field overload. The contract declares a second function of the same name; only this one is callable here. - Custom errors: 32, all decodable from the ABI. Branch on the error name, never on message text.
- Liquidity tick bounds: ±887200 — Uniswap's usable extremes, not a Fusor parameter and not a price target.
- ERC-1967 implementation slot: the standard one,
keccak256("eip1967.proxy.implementation") - 1.