Documentación

Protocolo

Este documento solo está disponible en inglés.

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.

Fusor never holds your assets. Every action is a transaction your own wallet signs against a public contract. There is no deposit step, no custody account, and no privileged address that can move a launched token or withdraw its liquidity — including ours.

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 FusorV4Locker during 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.

Fusor protocol components and the role each one plays
ComponentWhat it does
FusorLaunchpadV5UpgradeableThe 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.
FusorLaunchFacetHolds 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.
FusorTokenV5The ERC-20 that gets deployed per launch. Fixed supply, no mint function, plus a short post-launch guard described under Launch Protection.
FusorV4HookA Uniswap V4 hook attached to every Fusor pool. Its address encodes its permission bits, which is why it looks unusual.
FusorV4LockerCustodian of every launch position. Also does the fee accounting and holds creator and protocol balances until they are claimed.
FusorV5MultiPoolAggregatorOptional. Executes one trade across several of a token’s pools at once, using USDG as the common leg.
StockTokenRegistryThe 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 + FusorOracleFeedPrice 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.

Because the pools are independent, the same token can trade at different implied prices in each of its markets at the same moment. That is normal and is what arbitrage between them corrects. It also means a price you read from one pool is that pool's price, not “the” price of the token.

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.

Allocation examples and whether the contract accepts them
SplitBasis pointsAccepted
Single market10000Yes
Two markets, even5000 + 5000Yes
Three markets, even3334 + 3333 + 3333Yes — remainder placed on the first
Three markets, naive3333 + 3333 + 3333No — totals 9999, reverts
Any market weighted zero10000 + 0No — every market must carry a positive weight
Same market twice5000 + 5000 on one tokenNo — 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:

How registry contents relate to what this site offers
LayerWhere it livesEffect
Registry entryOn-chain, StockTokenRegistryDecides whether a launch succeeds. This is the only layer the contracts consult.
Non-stock assetsOff-chain product policyWETH, 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 exclusionsOff-chain product policyA small number of registered assets are withheld from the picker here. They remain fully usable on-chain.
The practical consequence: anything this site offers will launch, but not everything that will launch is offered here. If you are integrating directly, read the registry — do not derive eligibility from what the picker shows, and do not assume an asset missing from the picker is disabled 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 Stock Token symbols and their source
Currently enabled symbolsSource
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:

What graduation does and does not change
BeforeAfter
Where trading happensThe launch poolsThe same launch pools
LiquidityLocked in FusorV4LockerLocked in FusorV4Locker
Price ceilingNoneNone
Fee split70 / 30 creator / protocol70 / 30 creator / protocol
On-chain flagNot setSet, and permanent
Graduation is a progress marker. It does not release liquidity, cap the price, close anything, alter routing, or unlock a new venue. If your integration branches on the graduated flag for anything other than display, that is very likely a bug.

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.
The flip side of permanence: a launch cannot be undone. A wrong symbol, a wrong metadata URI, or a market you did not mean to include is final the moment the transaction confirms. Simulate first — the SDK's dry-run path exists for exactly this and costs nothing.

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 FusorTokenV5 ERC-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.

launchTokenMulti parameter struct fields
FieldTypeMeaning
namestringERC-20 name. Stored on the token; not validated for uniqueness.
symbolstringERC-20 symbol. Also not unique — two tokens may share one.
metadataURIstringWhere off-chain metadata lives. Written to the token and indexed; the contract does not fetch it.
metadataHashbytes32keccak256 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.
creatorFeeRecipientaddressReceives the creator share of swap fees. Can differ from the sender.
developerBuyRecipientaddressMust be non-zero even when no developer buy is requested — set it to the creator.
developerBuyPairIndexuint8Index into allocations for the developer buy, or 255 to skip it.
developerTokenAmountOutuint256Exact project tokens to buy. Zero when skipping.
maxQuoteAmountInuint256Spend ceiling for that buy. Zero when skipping.
deadlineuint256Unix 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:

Launch preconditions and the error each one produces
ConditionReverts with
Every quote market enabled in the registryStockTokenDisabled
Weights total exactly 10000WeightsNotOneHundredPercent
No duplicate quote marketDuplicatePair
Between one and five marketsInvalidPairCount
Each market has a price feed configuredInvalidOracle
Every relevant price fresh enough for maxOracleAge()StaleOracle
msg.value equals launchFeeWei()IncorrectLaunchFee
deadline still in the futureDeadlineExpired

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.

Take the authoritative address from the 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.

Restrictions active during the launch-protection window
LimitValueApplies to
Buy size5.5% of supply — 55,000,000 tokensAny single acquisition
Wallet holding5.0% of supply — 50,000,000 tokensBalance after any acquisition
Launch-block recipientOnly the designated developer-buy recipientPool-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.
The window is measured in blocks, not seconds, so its wall-clock length follows the chain's block time. Do not schedule anything against a fixed number of seconds.

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.

If you are building a bot or a trading integration, expect buys in the first few blocks to revert on the holding cap rather than on slippage. The failure looks like a size problem, not a price problem — retry smaller, or wait out the window.

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 FusorV4Locker in 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:

Liquidity range direction by token ordering
Project token sorts asRange runsReading it
token0launch tick → +887200Higher tick means a higher project-token price
token1−887200 → launch tickHigher tick means a lower project-token price
Those extremes are Uniswap's usable tick bounds, not Fusor parameters, and they are not a price target. Reaching a milestone does not correspond to reaching the end of a range.

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 sqrtPrice to 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.
A market cap built this way is a derived display value, not a protocol quantity and not a valuation. It moves with every trade in any of the pools, and it moves with the oracle even when nobody trades. It is not stored on-chain and nothing in the protocol depends on it.

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.
Buy and sell direction through the balanced aggregator
DirectionYou provideWhat happensYou receive
BuyUSDGConverted to each pool’s stock token as needed, then swapped across the chosen poolsProject token
SellProject tokenSplit across the chosen pools, then the stock proceeds are convertedUSDG

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

When each routing path is the right choice
SituationPath
You hold the stock token alreadyA — named market
You hold USDG and the token has several marketsB — balanced
You want exposure through one specific marketA — named market
Trade is large relative to a single pool’s depthB — balanced, if it finds a route
No safe balanced route can be builtA — pick a market explicitly
Balanced routing never silently falls back to a single pool. If it cannot build a safe route it says so, and choosing a market becomes an explicit decision rather than something that happened to you.

Approvals differ by path

Approvals required by routing path
PathApprove what, to whom
Named marketInput token → Permit2, then Permit2 → Universal Router
BalancedInput 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 exposes no HTTP endpoint that prices a swap. Integrations read pool metadata and state over RPC, call the deployed quoters themselves, build their own legs, and submit. Treating a successful simulation as a durable quote is the mistake to avoid: re-quote close to signing, set explicit per-leg and aggregate minimums, keep deadlines short, and simulate the final calldata against pending state.

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

The two kinds of price feed behind Fusor quote assets
ClassWhat it isKept fresh by
Official ChainlinkA real Chainlink aggregator deployed on this chain. Most large-cap equities have one.Chainlink’s own node operators — nothing on our side.
Fusor self-pricedA 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:

The two staleness checks and where each applies
CheckLimitWhere it livesApplies to
launchpad.maxOracleAge()A deployment parameter — read it, do not assume itThe launchpad, applied to whatever the feed returnedEvery asset
FusorPriceOracle.MAX_STALENESS2 hours, a hard-coded constantInside the oracle — latestRoundData reverts outrightSelf-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.
These two failures look different in a trace and you should expect both. Past 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:

Where keeper prices come from
WhatSource orderOn total failure
Stock pricesTwelve Data first when a key is configured, Yahoo Finance as fallbackThe asset is skipped for that cycle and keeps its previous on-chain price.
ETH/USDDeFiLlamaSame — 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.
The visible consequence of failing closed: when sources disagree or are unavailable, self-priced markets simply become unlaunchable for a while. That is the intended trade-off, not an outage.
Keeper scheduling is an operational service, not a protocol guarantee. Nothing in the contracts obliges anyone to keep a price fresh, and no contract will refund a launch attempt that failed because a price was old. If your integration launches against self-priced assets, check freshness immediately before submitting rather than at the start of a longer flow.

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.

The Fusor fee schedule
FeeAmountPaid byGoes toWhen
Launch feeWhatever launchFeeWei() returns — a deployment parameterCreator, as msg.valueProtocol treasuryOnce, in the launch transaction
Pool swap fee1% of the swap amountWhoever tradesThe locked V4 position, until collectedEvery trade, forever
The launch fee is not a protocol constant. Read 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:

The three stages of the fee pipeline and who triggers each
StageCallWho can trigger it
1 — CollectFusorV4Locker.collectFees(tokenId)Anyone. Our keeper does it on a schedule, but the function is permissionless — you never have to wait for us.
2 — AllocateHappens inside collectionNobody. The 70 / 30 split is applied automatically as part of the same call.
3 — ClaimFusorV4Locker.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 creatorFeeRecipient at 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.
Collection timing is operational, not a protocol guarantee. If you want fees collected right now, call 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.

The flag is sticky. Once set, later price or principal movement does not clear it — a token that graduates and then declines is still flagged as graduated. There is no un-graduate path.

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:

Milestone display states and what each one means
StateMeans
Below targetThe latest trustworthy observation puts progress under 100%.
EligibleA fresh observation is at or above 100%, but no successful sync has been recorded yet. The token is not graduated.
UnknownThe observation is too old to make a claim from. An interface should say so rather than guess in either direction.
GraduatedA 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.

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

Sends one transaction and waits for it
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);
Weights are optional. Given bare symbols (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.

Branch on the type first, then the name
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;
}
  • 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:

The seven steps behind a single launchToken call
#StepWhy it is there
1PreflightConfirms the proxy is a Fusor launchpad and reads its live parameters — fee, staleness limit, registry, feed addresses.
2Resolve marketsTurns symbols into addresses, checks the weights locally, then reads getConfig on each one.
3Oracle freshnessReads each asset’s own feed, so a StaleOracle revert becomes “which market, how old”.
4MetadataUses your URI as-is, or uploads for you if you configured an API base URL.
5Build paramsFills the 11-field struct, including the skip sentinels for the developer buy.
6Simulateeth_call against pending state. Reverts here cost nothing and are decoded to a named error.
7Send and confirmSigns 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.

No transaction is sent
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 below
The address a dry run returns is the address the token would get at the launchpad's current nonce. If another launch confirms before yours, the real address differs. Use it for preview only — never pre-register it or bake it into anything. The address in a real launch's result comes from the receipt event and is authoritative.
Whether a market can be launched against right now is a different question from whether it is registered — self-priced assets have a two-hour freshness window. SDK 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.

Three signing modes
// 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();

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:

Values the SDK reads live rather than embedding
ValueRead fromWhat hard-coding it would cause
Implementation addressThe proxy’s ERC-1967 slotEvery user breaks the moment the proxy is upgraded.
launchFeeWeilaunchpad.launchFeeWei()Every launch reverts with IncorrectLaunchFee once the fee changes.
maxOracleAgelaunchpad.maxOracleAge()Markets get wrongly reported as stale — or wrongly as fresh.
Registry addresslaunchpad.stockRegistry()Eligibility checked against the wrong allowlist.
ETH price feedlaunchpad.ethPriceFeed()Freshness checked against a feed the contract does not use.
Instead of pinning the implementation, preflight verifies behaviour: it reads the current implementation, then scans its deployed bytecode for the function selectors the SDK intends to call. An upgrade that keeps the interface passes; one that removes a function fails with a message naming the missing selector. That is both upgrade-proof and stronger than checking an ABI, which only proves what the source said, not what got deployed.

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.

The SDK also checks 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.

The three SDK error classes and what each implies
ClassCauseRetrying helps?
ConfigErrorYour input. Caught locally, before any request.Only after you change the input.
PreflightErrorChain state — stale price, missing feed, wrong address.Sometimes. A stale price becomes fresh.
LaunchErrorThe contract rejected the transaction.Depends on errorName.
Branch on the class, then 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();
  }
}
Branch on errorName, 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:

Golden vector sets and the drift each one prevents
Vector setAssertsFailure it prevents
launch-calldataByte-identical encoded calldataAn offset error in a hand-written encoder — which shows up as a revert with no apparent connection to the cause.
metadata-hashByte-identical canonical JSON and hashPython emitting ASCII escapes and padded separators by default, or a language escaping HTML in JSON.
oracle-stalenessIdentical freshness classificationInteger 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.

Read-only — no signer, no transaction
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)));
Two distinct failures show up here. A Chainlink-backed asset past 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.

Two HTTP requests, then one transaction
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',
  },
});
  • 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.

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

TypeScriptTypeScript — same steps launchToken runs internally
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);
The Python package exports the read and build steps the same way. Only TypeScript exports a standalone send step; in Python, submitting goes through the client.

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 ConfigError will fail identically forever. Only a PreflightError about 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.

This API is a convenience, not a source of truth. It serves indexed data, which can lag the chain, and display prices, which are not what the contracts read. Anything that decides whether a transaction will succeed — is this asset launchable, what is the fee, is this price fresh — must be read from the chain. Using an API response for that gives you a check that passes while the transaction reverts.

Conventions

API conventions that apply across endpoints
AspectBehaviour
AddressesAccepted checksummed or lowercase; compared case-insensitively.
Pagingpage and limit query params, with a server-side maximum on limit.
ErrorsNon-2xx with a JSON body. Rate limiting returns 429 with Retry-After.
AuthRead endpoints are open. Profile and watchlist writes need a one-time signed challenge. Admin routes need an allowlisted wallet.
CachingContent-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

GET/api/tokens

Lists launched tokens with paging, sorting, filtering and free-text search. Tokens marked hidden by moderation are omitted.

Query Parameters

Query parameters for GET /api/tokens
pagenumberPage number (default: 1)
limitnumberResults per page, max 50 (default: 20)
sortstringSort field: newest (default) | market_cap | volume_24h | progress | recent_buy | creator_fees | holders
filterstringFilter set: all (default) | recent_buys | newly_launched | market_cap
timeframestringLimit to tokens launched in: all (default) | 24h | 7d
quoteTokenaddressFilter by quote stock token address
graduatedbooleantrue = confirmed on-chain graduation flag; false = not yet confirmed
searchstringSearch by name, symbol, token address, or creator address

Response

{ items: Token[], total: number, page: number, limit: number }

GET/api/tokens/:address

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

GET/api/tokens/graduated

Tokens 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

GET/api/tokens/:address/trades

Swap history for one token, newest first, paged.

Path Parameters

:addressToken contract address

Query Parameters

Query parameters for GET /api/tokens/:address/trades
pagenumberPage number (default: 1)
limitnumberResults per page (default: 50)
filterstringbuys | 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.

GET/api/tokens/:address/candles

OHLCV candles for charting, at the requested interval.

Path Parameters

:addressToken contract address

Query Parameters

Query parameters for GET /api/tokens/:address/candles
resolutionstringCandle size: 1 | 5 | 15 | 60 | 240 | 1D
fromunix timestampStart of range (inclusive)
tounix timestampEnd of range (inclusive)

Response

Candle[] — up to 1,000 candles. Each candle: { timestamp, open, high, low, close, volume, quoteVolume, tradeCount }

GET/api/tokens/:address/fees

Indexed 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 address

Response

{ tokenAddress, pendingProjectTokenFees, pendingQuoteTokenFees, totalCreatorFeesCollected, totalProtocolFeesCollected, lastCollectedAt, lastConvertedAt, aggregateUsd, pairs[] }. Each pair includes project/quote pending amounts plus gross-collected and 70/30 allocated counters.

GET/api/tokens/:address/metrics

Market 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

GET/api/stock-tokens

The 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 }

GET/api/stock-tokens/:address

One quote asset by its token address.

Path Parameters

:addressStock token contract address

Response

StockToken object

Fees

GET/api/fees/claimable/:wallet

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

GET/api/fees/pending/:wallet

Fees 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 address

Response

PendingFeeEntry[] — { tokenAddress, tokenSymbol, tokenImageUrl, quoteTokenAddress, pendingCreatorQuote, pendingAmountUsd }

GET/api/fees/history/:wallet

Past claim transactions for a wallet, paged.

Path Parameters

:walletWallet address

Query Parameters

Query parameters for GET /api/fees/history/:wallet
pagenumberPage number (default: 1)
limitnumberResults per page (default: 20)

Response

{ items: FeeClaimEvent[], total, page, limit }. Each item: { txHash, quoteToken, amount, amountUsd, timestamp }

GET/api/fees/manifest/:wallet

Returns 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

Query parameters for GET /api/fees/manifest/:wallet
tokensstringOptional 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

GET/api/profiles/:wallet

Returns 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 address

Response

{ wallet, username, bio, imageUrl, twitterHandle, createdTokenCount, totalTradesCount, createdAt }

POST/api/profiles/:wallet/challenge

Issues 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 mutation

Request Body (JSON)

Request body fields for POST /api/profiles/:wallet/challenge
actionstringprofile:update | watchlist:update

Response

{ nonce, expiresAt } — include both values in the canonical Fusor Ownership v1 message and signed mutation.

PUT/api/profiles/:wallet

Creates 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 address

Request Body (JSON)

Request body fields for PUT /api/profiles/:wallet
profileobject{ username?, bio?, imageUrl?, twitterHandle? }
noncestringNonce returned by the matching challenge
expiresAtdate-timeExact challenge expiry returned by the API
signaturehex stringEIP-191 signature of the canonical profile-update message

Response

Updated Profile object

GET/api/profiles/:wallet/tokens

Tokens 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 address

Query Parameters

Query parameters for GET /api/profiles/:wallet/tokens
pagenumberPage number (default: 1)
limitnumberResults per page (default: 20)

Response

{ items: Token[], total, page, limit }

GET/api/profiles/:wallet/holdings

Indexed non-zero balances for this wallet. Kept for API consumers; the profile page in this interface derives its holdings differently.

Path Parameters

:walletWallet address

Response

Holding[] — { token, balance (wei string), balanceUsd, percentOfSupply }

GET/api/profiles/:wallet/trades

Indexed trades by this wallet across all tokens. Kept for API consumers; this interface does not surface it.

Path Parameters

:walletWallet address

Query Parameters

Query parameters for GET /api/profiles/:wallet/trades
pagenumberPage number (default: 1)
limitnumberResults per page (default: 50)

Response

{ items: Trade[], total, page, limit }. Each trade includes tokenAddress, tokenSymbol, tokenName, and quoteTokenSymbol in addition to standard trade fields.

GET/api/profiles/:wallet/watchlist

Returns tokens on a wallet's watchlist, most recently added first.

Path Parameters

:walletWallet address

Response

Token[]

POST/api/profiles/:wallet/watchlist

Adds or removes one token from a wallet’s watchlist. Requires a fresh single-use watchlist challenge signed by that wallet.

Path Parameters

:walletWallet address

Request Body (JSON)

Request body fields for POST /api/profiles/:wallet/watchlist
watchlistobject{ tokenAddress, watching }
noncestringNonce returned by the matching challenge
expiresAtdate-timeExact challenge expiry returned by the API
signaturehex stringEIP-191 signature of the canonical watchlist-update message

Response

{ tokenAddress, watching }

Stats

GET/api/stats/platform

Aggregate counters computed from indexed data — launches, volume, fees, and similar.

Response

{ totalTokensLaunched, totalTradesAllTime, totalVolumeUsd, totalFeesDistributedUsd, graduatedCount, activeTokens24h }

GET/api/stats/trending

The most actively traded visible tokens over the last 24 hours.

Query Parameters

Query parameters for GET /api/stats/trending
limitnumberNumber of tokens to return, max 20 (default: 10)

Response

Token[] sorted by 24h trade count descending

Prices

GET/api/prices

Live 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

Query parameters for GET /api/prices
symbolsstringComma-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

POST/api/images

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

Request body fields for POST /api/images
datastringStrict base64-encoded image data (no data: prefix), up to 1 MiB decoded
contentTypestringMatching raster MIME type: image/png, image/jpeg, or image/webp

Response

{ url: string, hash: string } — URL is a permanent /api/images/<hash> path.

GET/api/images/:hash

Serves 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/images

Response

Raw image bytes

POST/api/metadata

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

Request body fields for POST /api/metadata
namestringToken name
symbolstringToken symbol
descriptionstring?Token description
imagestring?Image URL (from POST /api/images)
links.twitterstring?Twitter/X profile URL
links.telegramstring?Telegram URL
links.websitestring?Website URL

Response

{ url: string, hash: string } — url is the permanent metadata URI written on-chain.

GET/api/metadata/:hash

Serves 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/metadata

Response

{ schemaVersion: "1.0.0", icon?, description?, links?: [{ type, label, url }] }

Launch Registration

POST/api/launches/register-v5

仅从已确认的交易回执推导并注册一次发射。返回 201 与 token,或 202 表示确认阈值未达到。

Request Body (JSON)

Request body fields for POST /api/launches/register-v5
txHashhex string已确认的发射交易哈希

Response

{ status: 'registered', token } | 202 { status: 'pending' }

GET/api/launches/v5-beta-status

在不可逆的浏览器端工作前,校验 API/索引器是否认识给定的 launchpad 代理。响应不可用或格式错误一律 fail closed。

Query Parameters

Query parameters for GET /api/launches/v5-beta-status
launchpadAddressaddress待校验的 launchpad 代理地址

Response

{ recognized: boolean, ... }

Health

GET/api/healthz

Liveness probe. Returns 200 while the process is up; it says nothing about the database.

Response

{ status: "ok" }

Admin

GET/api/admin/treasury

协议金库余额。需要签名的 admin 请求头,钱包须在 ADMIN_WALLETS 白名单内。

Response

TreasuryBalance[]

POST/api/admin/tokens/import

按地址导入一个外部代币,身份信息通过实时 ERC-20 读取获得。需要 admin 鉴权。

Request Body (JSON)

Request body fields for POST /api/admin/tokens/import
addressaddress代币合约地址

Response

Token object

POST/api/admin/tokens/:address/moderate

设置代币的 hidden / flagged 标记。需要 admin 鉴权与 reason。

Path Parameters

:address代币合约地址

Request Body (JSON)

Request body fields for POST /api/admin/tokens/:address/moderate
hiddenboolean?从公开列表隐藏
flaggedboolean?标记为可疑
reasonstring必填 —— 审核操作要留痕

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.

Integrate against the proxy, never the implementation. The implementation address is upgradeable and changes without notice; the row below reads it live from the proxy for exactly that reason. Any address you hard-code that is not the proxy will eventually be the wrong one.

Fusor protocol

Core Fusor protocol contract addresses and how each one is verified
ContractAddressSource
FusorLaunchpad (Proxy)0xc46ce20e47709d38044bd507275f10f0b06376c4The only entry point — integrate against this, never the implementation
— Implementation (current)0x62d281caea772897d988008f8c81b86b75c051d9Read live from the proxy’s ERC-1967 slot — changes on upgrade
FusorLaunchFacet0xd5300bbefdfdf97ba626a8a24bae5045fe826b42Immutable constructor arg of the implementation; holds the launchTokenMulti body
FusorV5MultiPoolAggregator0x94c0f173b0481b085981605c2d096bb0e1fe1347Permissionless balanced-routing execution contract
FusorV4Hook0x9e7352833bbac3c620ed201764fe21701174a000Read from launchpad.pairHook() — low 14 bits encode beforeInitialize (0x2000)
FusorV4Locker0xffeb663f6359dd7609726097a604b186e7213da3Read from launchpad.locker() — all LP positions are permanently locked here
FusorV4DeveloperBuyAdapter0xbc1457d94dec5590955ded1d47b49a3cde63557cRead from launchpad.developerBuyAdapter() — developer buy is not enabled in this release
FusorPriceOracle0x7b71b20309676073a2dfd044d19650ac7c0ce0d1Serves only the self-priced quote assets; the rest read official Chainlink feeds directly
ETH/USD price feed0x78f3556b67e17df817d51ef5a990cdaf09e8d3a9Official Chainlink feed — read from launchpad.ethPriceFeed(); no setter exists
StockTokenRegistry (Proxy)0xd6f2b4bc593ab14391e960be1d7f81c3a4340a60Read from launchpad.stockRegistry() — the quote-asset allowlist; no setter exists
Protocol Treasury0xcc4da2f57a0818c7b1b8fa24879cf81b60e92e82Read from launchpad.protocolTreasury() — no setter exists
Every address above except the proxy is read back out of the proxy’s own getters, so the table is self-consistent by construction — it always describes one launchpad’s view of its own wiring. Use the proxy as the launch entry point; the implementation and facet are not user entry points.

Uniswap V4 (Robinhood Chain)

Uniswap V4 deployment addresses on Robinhood Chain and verification source
ContractAddressSource
PoolManager0x8366a39cc670b4001a1121b8f6a443a643e40951Cross-checked against the immutable slots of our own deployed contracts
PositionManager0x58daec3116aae6d93017baaea7749052e8a04fa7The canonical V4 deployment on this chain
StateView0xf3334192d15450cdd385c8b70e03f9a6bd9e673bThe canonical V4 deployment on this chain
Universal Router0x8876789976decbfcbbbe364623c63652db8c0904Read from launchpad.universalRouter() — note the non-standard minHopPriceX36 field
V4Quoter0x8dc178efb8111bb0973dd9d722ebeff267c98f94No on-chain getter exposes this one — it is a client-side constant, so verify it against the explorer before relying on it
Permit20x000000000022d473030f116ddee9f6b43ac78ba3Read from launchpad.permit2() — canonical cross-chain address

Robinhood Universal Router compatibility

Robinhood Chain's deployed router does not use the standard package's exact-input struct unchanged. Its dynamic 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.

Check any address you are about to interact with against this page, and prefer reading it from the proxy's own getters over trusting a copy — including this one. Nobody from Fusor will ever ask you to approve or send funds to a contract that is not reachable from the proxy above.

Robinhood Chain

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.

Deployment parameters and where to read each one
ValueRead fromWhat goes wrong if you hard-code it
Launch feelaunchpad.launchFeeWei()Every launch reverts with IncorrectLaunchFee — msg.value must match exactly, in both directions.
Oracle staleness limitlaunchpad.maxOracleAge()Markets get reported as stale when they are fine, or fresh when they are not.
Registry addresslaunchpad.stockRegistry()Eligibility checked against the wrong allowlist.
ETH price feedlaunchpad.ethPriceFeed()Freshness checked against a feed the contract does not consult.
Implementation addressThe proxy’s ERC-1967 slotEverything breaks the moment the proxy is upgraded.
A quote asset’s price feedregistry.getConfig(token).priceFeedSame 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.

Compile-time protocol constants
ConstantValueMeaning
Total supply1,000,000,000Every token, always. No mint function exists.
Max quote markets5Upper bound on markets per launch. Minimum is 1.
Basis-point denominator10000Allocations must sum to exactly this — no rounding tolerance.
Skip-developer-buy sentinel255The value for developerBuyPairIndex that means “no developer buy”.
Max developer buy550 bps (5.5%)Ceiling on a developer buy when the feature is enabled.
Milestone base4.2 ETHConverted to USD at launch-block price and stored per token.
Oracle price decimals8Chainlink convention — $200.00 is 20000000000.
Self-priced staleness2 hoursFusorPriceOracle.MAX_STALENESS. Hard-coded; the feed reverts past it.

Launch protection window

Limits active during the launch-protection window
LimitValueNotes
Window lengthLaunch block + 5 blocksMeasured in blocks, not seconds. Lifts automatically.
Max single buy5.5% of supply — 55,000,000Per acquisition.
Max wallet holding5.0% of supply — 50,000,000Checked after each acquisition.
SellingUnrestrictedNo limit at any point.

Fees

Fee values
FeeValue
Pool swap fee1% of every swap
Creator share of collected fees70%
Protocol share of collected fees30%
Minimum claimable balanceNone — any non-zero balance can be claimed
Launch feeDeployment parameter — see the first table

Routing

Balanced aggregator limits
LimitValue
Max pool legs per trade5
Max live price impact per route15% — routes above this are rejected
Max deadline1 day ahead
Common asset for balanced routesUSDG

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.
If you want these values as code rather than prose, the SDKs ship the compile-time constants and read every deployment parameter for you on each call. That is the whole reason the split above exists in the SDK design.
Fusor Protocol Documentation@fusordotfun