本页目前只有英文版。

Build on the Pool Launch Protocol

On Pool Launch you build two kinds of contracts, alone or together:

  • a Launcher, your launch page on-chain: it raises the initial liquidity your own way (a presale, a mint, a contribution round, …) and then launches the token into its DEX pool;
  • a Vault, which receives a share of the token's tax revenue and can also hold the tokens reserved for a mechanism.

See Basic Mechanism for how the pieces fit together. This page covers the contracts and the UI.

Beta, permissioned. A Launcher Factory and a Vault Factory must pass Flap's audit and be enabled on the Portal before they are used: contact the Flap team. See Getting access.

The interfaces, base contracts and complete, tested examples are in FlapPoolLaunchVaultExample: copy the files you need from its src/flap directory. The Pool Launch Portal is 0x000042108465206185A077Edc3F3fd2b49060000 on BNB Chain and BNB Chain Testnet (all addresses).

1. Contracts

1.1 Launcher

Create a Launcher through the Portal

A Launcher is always created through the Portal, never by calling your factory directly. The app (or the user, from your launch page) calls:

/// Pool Launch Portal
function newLauncher(address launcherFactory, bytes calldata data) external payable returns (address launcher);
  1. The Portal checks that launcherFactory is enabled (LauncherFactoryNotEnabled otherwise).
  2. It calls launcherFactory.newLauncher{value: msg.value}(msg.sender, data). data is your own layout; the Portal does not read it.
  3. It checks that the returned Launcher is a contract and not already recorded, records launcher → launcherFactory, reads the Launcher's description(), and emits:
event FlapNewLauncherCreated(address launcher, address launcherFactory, address creator, string description);

Flap will show newly created Launchers on its website, from this event and the Launcher's description in it (see Describe the Launcher). The description must follow the Launcher description JSON spec. Terminals and bots can follow the same event to list them.

Users then interact with the Launcher. When its condition is met, the Launcher calls launchTaxTokenV2 from its own address, exactly once. A second launch reverts with LauncherAlreadyLaunched: deploy a new Launcher for each sale.

function getLauncher(address launcher) external view returns (LauncherRecord memory); // { launcherFactory }
function launcherFactoryMultiplier(address factory) external view returns (bool enabled, uint16 multiplierBps);

The factory's floor multiplier (0–10000 bps, 10000 = no reduction) is agreed during the audit and set on the Portal by Flap; the Portal reads it live at launch.

Launcher Factory interface

interface ILauncherFactory {
    /// Portal-only. `creator` is the wallet that called Portal.newLauncher; `msg.value` is forwarded.
    function newLauncher(address creator, bytes calldata data) external payable returns (address launcher);

    /// MUST return "lf-v1".
    function factorySpecVersion() external pure returns (string memory);

    /// The layout of `data`, for the creation form (same structs as vaultDataSchema).
    function launcherDataSchema() external pure returns (LauncherDataSchema memory);

    /// Guardian-only. Upgrades every Launcher of this factory (beacon proxies).
    function upgradeVaultImplementation(address newImplementation) external;
}

Build it on LauncherFactoryBase: Flap only enables factories that inherit it. The base makes newLauncher Portal-only, returns "lf-v1", and leaves you three functions:

You implementWhat it does
_newLauncher(creator, data)Decode data, deploy the Launcher (a beacon proxy is recommended), initialize it with the Portal address, creator and the config, and return it. msg.value is available.
launcherDataSchema()Must match the data layout _newLauncher decodes.
upgradeVaultImplementation(impl)onlyGuardian; forward to your UpgradeableBeacon. The Guardian is the only upgrade path.
contract MintLauncherFactory is LauncherFactoryBase {
    address public immutable beacon;

    constructor() {
        beacon = address(new UpgradeableBeacon(address(this), address(new MintLauncher())));
    }

    function _newLauncher(address creator, bytes calldata data) internal override returns (address launcher) {
        MintConfig memory config = abi.decode(data, (MintConfig));
        launcher = LibClone.deployDeterministicERC1967BeaconProxy(beacon, config.salt);
        MintLauncher(launcher).initialize{value: msg.value}(_getHub(), creator, config); // MintLauncher locks its salt here
    }

    function upgradeVaultImplementation(address impl) external override onlyGuardian {
        UpgradeableBeacon(beacon).upgradeTo(impl);
    }

    function launcherDataSchema() external pure override returns (LauncherDataSchema memory) { /* … */ }
}

Launcher

The Launcher holds your whole mechanism (contributions, pricing, caps, claims, refunds); the Portal never reads it. Build it on LauncherBase (optional, storage-free):

abstract contract LauncherBase is ILauncherDescription {
    modifier onlyHub();       // the Pool Launch Portal
    modifier onlyGuardian();  // the Flap Guardian

    /// Returns LauncherHelper.toJson(_description()); read by the Portal at creation.
    function description() external view returns (string memory);

    function _description() internal view virtual returns (LauncherDescription memory); // you implement
    function _getHub() internal view virtual returns (address);                          // you implement: the stored Portal

    /// Calls Portal.launchTaxTokenV2{value}(p, distribution, creatorBuy).
    function _launchOnHub(CreateTaxTokenParams calldata p, Distribution calldata distribution,
                          CreatorBuyParams calldata creatorBuy) internal returns (address token, address pool);
    function _launchOnHub(CreateTaxTokenParams calldata p, Distribution calldata distribution,
                          CreatorBuyParams calldata creatorBuy, uint256 value) internal returns (address token, address pool);
}

The launch parameters (CreateTaxTokenParams, Distribution, CreatorBuyParams) are described in Launch parameters below. The rules a Launcher must follow:

RuleWhy
If you lock the salt, lock it from the Launcher (recommended, not required). Locking protects the token address from being taken before the launch. Call Portal.lockSalt{value: SALT_LOCK_FEE()}(salt, 0) from the Launcher, typically in initialize, paid from the msg.value the factory forwards. A Launcher may also skip the lock and let the dev enter the salt in the Launcher UI.lockSalt records msg.sender as the locker, so a locked salt can only be launched by its locker. A lock taken by the factory makes the Launcher's launch revert (FlapSaltLockedByAnotherUser).
Send exactly the launch value. For a native quote, msg.value must equal poolQuote + creatorBuyQuote. For an ERC-20 quote, hold the quote, approve the Portal and send 0.The Portal rejects any other amount.
Launch once.LauncherAlreadyLaunched.
Show the real creator. Set (p.extensionID, p.extensionData) = LaunchExtensionHelper.encodeCreator(creator) with the creator you stored at creation.The Launcher calls the Portal, so the launch event would otherwise show the Launcher contract as the creator. Only a recorded Launcher may use it.
Give the Guardian recovery paths.Use onlyGuardian on emergency functions that must stay reachable if the creator's key is lost.

Launch parameters

When its condition is met, the Launcher calls:

function launchTaxTokenV2(CreateTaxTokenParams calldata p, Distribution calldata distribution,
                          CreatorBuyParams calldata creatorBuy) external payable returns (address token, address pool);

One transaction creates the tax token (and, with a VAULT slot, its vault), seeds the V2 pool with poolToken and poolQuote and burns the LP, hands the rest of the supply to distribution, then runs the optional creator first buy. The token trades from that block. The (kind, quoteToken) pair must be enabled by Flap (LaunchQuotePairNotEnabled otherwise; check with launchQuoteConfig).

struct CreateTaxTokenParams {
    string name;
    string symbol;
    string meta;               // metadata CID (see Flap IPFS Gateway)
    uint256 totalSupply;       // chosen by the creator: 100 to 10^15 tokens (18 decimals)
    bytes32 salt;              // the token address must end in 7777
    address quoteToken;        // address(0) = native
    uint8 dexId;               // 0
    uint256 poolQuote;         // quote put into the pool (> 0)
    uint256 poolToken;         // tokens put into the pool (> 0, <= totalSupply)
    uint16 buyTaxRate;         // bps, at most 1000
    uint16 sellTaxRate;        // bps, at most 1000
    uint32 taxDuration;        // seconds
    uint32 antiFarmerDuration; // seconds
    FeeConfig[] feeConfigs;    // where the tax goes, see below
    uint8 antiSniperMode;      // must be 0
    uint32 exemptWindow;       // pass 0
    address[] exemptAddrs;     // pass []
    uint256 creatorBuyQuote;   // quote for the creator's first buy, 0 = none
    bytes32 extensionID;       // bytes32(0), or the creator extension (see the rules above)
    bytes extensionData;       // 0x
    address commissionReceiver;// address(0) = no commission
}

The opening price is poolQuote / poolToken, and the launch must clear the creation floor.

Fee slots. feeConfigs splits the tax revenue; up to 4 slots, each type at most once, and the active slots' bps must add up to exactly 10000:

enum FeeSlotType { NONE, DEFLATION, LP_BPS, MARKETING, VAULT, DIVIDEND }
struct FeeConfig { FeeSlotType feeType; uint16 bps; bytes configData; }
SlotEffectconfigData
DEFLATION (1)Burnedempty
LP_BPS (2)Added to the pool's liquidityempty
MARKETING (3)Sent to a walletabi.encode(address marketingAddress)
VAULT (4)Sent to the launch's vault, created by the factoryabi.encode(address vaultFactory, bytes vaultData)
DIVIDEND (5)Paid to holdersabi.encode(address dividendToken, uint256 minimumShareBalance)
  • MARKETING and VAULT cannot be combined.
  • dividendToken: address(0) = the quote token; 0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd = the token itself; 0xC0Dec0dec0DeC0Dec0dEc0DEC0DEC0DEC0DEC0dE = computed by the launch's vault factory (needs a VAULT slot); or another supported ERC-20.
  • minimumShareBalance (the balance a holder needs to earn dividends) must be between 0.001% and 0.1% of totalSupply.

Distribution. Everything not in the pool (totalSupply - poolToken) must be handed out in the launch, through distribution (see Reserved tokens):

targetWho receives it
NONE (0)Nobody: amount must be 0, so the whole supply must be in the pool.
EXPLICIT (1)The strategy contract you name, for example the Launcher itself or the shared AllocationStrategy.
VAULT (2)The launch's vault. strategy is ignored.

A leftover above 1 token reverts ExcessiveRemainder; up to 1 token of rounding dust is burned.

Creator first buy. With creatorBuyQuote > 0, after the pool is seeded the Portal buys that much for the caller. CreatorBuyParams { uint256 minOutputAmount; bytes permitData; } is that buy's slippage bound and optional permit.

Value. Native quote: msg.value = poolQuote + creatorBuyQuote, exactly (IncorrectNativeValue otherwise). ERC-20 quote: msg.value = 0 and approve the Portal for poolQuote + creatorBuyQuote.

const p = {
  name: "My Token", symbol: "MTK", meta: metadataCid,
  totalSupply: parseEther("1000000000"), salt, quoteToken: zeroAddress, dexId: 0,
  poolQuote: parseEther("20"), poolToken: parseEther("500000000"),   // 50% of the supply in the pool
  buyTaxRate: 300, sellTaxRate: 300, taxDuration: 30 * 86400, antiFarmerDuration: 86400,
  feeConfigs: [
    { feeType: 4, bps: 8000, configData: encodeAbiParameters([{ type: "address" }, { type: "bytes" }], [VAULT_FACTORY, vaultData]) },
    { feeType: 2, bps: 2000, configData: "0x" },
  ],
  antiSniperMode: 0, exemptWindow: 0, exemptAddrs: [],
  creatorBuyQuote: 0n, extensionID: zeroHash, extensionData: "0x", commissionReceiver: zeroAddress,
};
const distribution = { target: 2, strategy: zeroAddress, amount: p.totalSupply - p.poolToken, configData: "0x" }; // VAULT
const creatorBuy = { minOutputAmount: 0n, permitData: "0x" };
// From a Launcher: IPortal(hub).launchTaxTokenV2{value: p.poolQuote + p.creatorBuyQuote}(p, distribution, creatorBuy)

Salt lock. lockSalt(bytes32 salt, uint8 kind) with kind = 0 reserves the token address; msg.value must equal SALT_LOCK_FEE() (0.01 BNB on BNB Chain, not refundable). A salt can be locked once, the lock has no expiry, and it is used up by the launch. Read it with getSaltLock(salt) → (locker, launchKind, isUsed); events FlapSaltLocked and FlapSaltUsed.

Errors

ErrorCause
LaunchQuotePairNotEnabled(kind, quoteToken)The quote token is not enabled for Pool Launch.
QuoteTokenCreationDisabled(quoteToken)Creation with this quote token is switched off.
CreationPaused()Creation is paused and the caller is not covered by an enabled factory (see Getting access).
LaunchLiquidityTooLow(quoteUsdWad, requiredUsdWad)Below the creation floor.
IncorrectNativeValue(sent, required)msg.value is not poolQuote + creatorBuyQuote (native) or 0 (ERC-20).
SupplyOutOfRange, ZeroPoolAmount, PoolTokenExceedsSupply, InvalidTaxBpsA parameter is out of range.
InvalidFeeConfigBps(), DuplicateFeeSlot(feeType), TooManyFeeConfigs, VaultAndMarketingMutuallyExclusive()The fee slots break a rule above.
ExcessiveRemainder(token, amount)More than 1 token of the supply was not handed out.
ExplicitStrategyRequired(), NoLaunchVault(token)EXPLICIT without a strategy, or VAULT without a VAULT slot.
AntiSniperDisabled()antiSniperMode is not 0.
FlapSaltLockedByAnotherUser(salt, locker)Someone else locked this salt.
LauncherAlreadyLaunched(launcher)The Launcher already launched.
CreatorOverrideUnauthorized(caller), ZeroCreatorAddress()The creator extension was used by a non-Launcher, or with the zero address.

Describe the Launcher: shown on Flap

Flap will show newly created Launchers on its website, and terminals can show them too. Each entry is built from the Launcher's description(), which the Portal reads once at creation and puts in FlapNewLauncherCreated, so no extra call is needed. What you return there is what users see for your launch on Flap: the token name and symbol, the raise target, the share of the supply that goes into the pool, and your title and summary.

description() must return JSON that follows the Launcher description spec (v1): the exact keys, value types and formats it defines. Flap and terminals parse it against that spec, so a description in any other shape cannot be displayed properly. The simplest way to comply is LauncherBase, which builds it with LauncherHelper.toJson; a Launcher that writes its own description() must produce exactly the same format.

Implement _description() and LauncherBase renders it as JSON with LauncherHelper.toJson:

struct LauncherDescription {
    string name;            // the token this Launcher will launch
    string symbol;
    address quoteToken;     // address(0) = native
    uint256 targetRaise;    // 18-decimal fixed point
    uint256 poolTokenRatio; // share of the supply that goes into the pool, 1e18 = 100%
    string titleZh;
    string titleEn;         // one line
    string summaryZh;
    string summaryEn;       // a few sentences: how the launch works
}
{"v":1,"name":"MyToken","symbol":"MTK",
 "quoteToken":"0x0000000000000000000000000000000000000000",
 "targetRaise":"1000000000000000000","poolTokenRatio":"500000000000000000",
 "titleZh":"募资发射","titleEn":"Contribution round",
 "summaryZh":"…","summaryEn":"BNB is collected until the target is met. …"}
  • v is the only number; every other value is a string. Amounts are decimal strings and addresses lowercase hex.
  • Only name and symbol come from users, so only they are escaped. Titles and summaries are your constants: keep quotes and backslashes out of them.
  • More languages add a suffix (titleJa, summaryJa); readers fall back to …En.
  • The read is best-effort (at most 200,000 gas and 4,096 bytes). A Launcher with no description, or one that reverts, is still created, with "" in the event. description() stays callable on the Launcher, so it can also be read later.
  • It is display data only: never use it for gating or accounting. It is not launcherDataSchema().description, which is the help text of the creation form.

The full format, with every key and the parsing rules readers apply: Launcher description spec.

1.2 Vault

A Pool Launch Vault works like a bonding curve vault: a factory creates one vault per token during the launch, the vault receives a share of the tax revenue, it describes itself, it accepts the Guardian, and it is upgraded only through a beacon held by the Guardian. Two things differ:

  • The interfaces and the entry point. The factory is called by the Pool Launch Portal, with the whole launch input, so it validates the launch and creates the vault in one call.
  • Reserved tokens. The vault can also receive the part of the supply that does not go into the pool, through the strategy interface, and hold it in the Token Dock.

Attach a vault to a launch

A launch uses a vault through a VAULT fee slot:

FeeConfig({
    feeType: FeeSlotType.VAULT,   // 4
    bps: 8000,                    // the vault's share of the tax revenue
    configData: abi.encode(FeeConfigVault({ vaultFactory: myFactory, vaultData: abi.encode(/* your config */) }))
})

The other bps go to DEFLATION, LP_BPS or DIVIDEND; a MARKETING slot cannot be combined with VAULT. During launchTaxTokenV2, the Portal calls your factory's newVault, puts the returned vault in the slot, and records it (getLaunchVault(token)).

Vault Factory interface

interface IDirectLaunchVaultFactory {
    /// Portal-only. Validate the launch and create its vault, in one call, BEFORE the token exists.
    /// `token` is the predicted address. `launchParams` = abi.encode(CreateTaxTokenParams, Distribution)
    /// for launchKind DIRECT_V2_POOL_TAX (0).
    function newVault(address token, address creator, uint8 launchKind, bytes calldata launchParams)
        external returns (address vault);

    function isQuoteTokenSupported(address quoteToken) external view returns (bool); // truthful, matches newVault
    function factorySpecVersion() external pure returns (string memory);             // "pl-v1"
    function vaultDataSchema() external pure returns (VaultDataSchema memory);       // same structs as bonding curve
    function tokenCreationPolicies() external view returns (FactoryPolicy[] memory); // UI hints, mirror newVault
    function upgradeVaultImplementation(address newImplementation) external;         // Guardian-only
}

Compared with a bonding curve factory: there is no separate onBeforeLaunch, because newVault itself rejects a launch by reverting; and factorySpecVersion() returns "pl-v1" (every Pool Launch factory's version starts with pl).

DirectLaunchVaultFactoryBase (recommended) gives onlyHub, onlyGuardian and two validation helpers:

HelperChecks
_validateLaunch(token, creator, launchKind, launchParams)token and creator are non-zero; exactly one VAULT slot names this factory, with bps above zero; the quote token passes isQuoteTokenSupported. Returns the decoded CreateTaxTokenParams, the Distribution and your vaultData.
_requireNoReserveDistribution(d)For a vault that does not take reserved tokens: the distribution must not target the vault.

In newVault: check onlyHub, call _validateLaunch, check everything your vault depends on (decode vaultData strictly, check the distribution, the slot's bps, the dividend slot, …) and revert otherwise, then deploy and initialize the vault and return it. Never call token: it has no code yet. Deploy the vault deterministically from token behind a beacon proxy.

Vault interface

interface IDirectLaunchVault {
    /// The vault's current state, and every fee, commission and party that receives revenue or reserved tokens.
    function description() external view returns (string memory);
    /// The vault's user-facing methods (the same VaultUISchema as bonding curve vaults).
    function vaultUISchema() external pure returns (VaultUISchema memory);
}

Build it on DirectLaunchVaultBase (recommended): onlyHub, onlyGuardian, and _getTokenDockRouter(). Revenue arrives in the quote token (native or ERC-20): recognize it by balance delta and keep receive() light and non-reverting, exactly as in the bonding curve accounting model. Every permissioned function must also accept the Guardian.

Reserved tokens: the strategy interface

A launch puts poolToken of the supply into the pool. The rest, totalSupply - poolToken, must be handed out in the same transaction through the launch's Distribution, otherwise the launch reverts (ExcessiveRemainder; up to 1 token of rounding dust is burned):

struct Distribution {
    DistributionTarget target; // NONE, EXPLICIT (a strategy contract), or VAULT (the launch's vault)
    address strategy;          // EXPLICIT only
    uint128 amount;            // totalSupply - poolToken
    bytes configData;          // passed to the strategy
}

Whoever receives the reserve implements IStrategy. The Portal approves it for exactly amount and calls:

interface IStrategy {
    event FlapDistributionInitialized(address token, uint256 amount);
    function initializeDistribution(address token, uint256 amount, bytes calldata configData, bytes32 salt) external;
}

To let your vault take the reserve, launch with distribution.target = VAULT: the Portal finds the launch's vault and calls initializeDistribution on it. In the vault:

  • restrict it to the Portal (onlyHub) and to token == taxToken, and allow it once;
  • pull exactly amount with transferFrom(msg.sender, …) and check the balance change;
  • emit FlapDistributionInitialized;
  • deposit the tokens in the Token Dock through _getTokenDockRouter(), so they are shown as a labelled reserve rather than as one large holder, and withdraw from it when your mechanism pays out.
function initializeDistribution(address token_, uint256 amount, bytes calldata, bytes32) external onlyHub {
    require(token_ == taxToken && !distributionInitialized, "bad distribution");
    distributionInitialized = true;
    token_.safeTransferFrom(msg.sender, address(this), amount);

    address dock = _getTokenDockRouter();       // never cache it
    token_.safeApprove(dock, amount);
    ITokenDock(dock).deposit(token_, amount);
    emit FlapDistributionInitialized(token_, amount);
}

// later, when the mechanism pays out:
ITokenDock(_getTokenDockRouter()).withdraw(taxToken, amount, recipient);

The reserve first sits in the open Token Dock; once the vault is audited and the Portal whitelists it, its balance moves to the verified dock by itself. Withdrawals are never blocked.

If your mechanism depends on the reserve (for example "80% of the supply is mined by stakers"), enforce it in newVault so a launch that does not match cannot happen: d.target == DistributionTarget.VAULT, d.amount is exactly what your economics expect, and p.poolToken + d.amount == p.totalSupply.

A Launcher can be the strategy too: MintLauncher launches with target = EXPLICIT, strategy = address(this), receives half of the supply in its own initializeDistribution, and lets contributors claim it.

To hand the reserve to recipients without writing a contract, use the shared AllocationStrategy: launch with target = EXPLICIT, strategy = ALLOCATION_STRATEGY and configData = abi.encode(creator). The tokens are held there and only that creator can hand them out, in one or more calls, until none are left: batchAllocate(token, recipients, amounts), with remainingOf(token) showing what is left. A vault in the VAULT slot still receives its share of the revenue; it just does not hold the reserve.

Token Dock interface

A vault only talks to the TokenDockRouter (from _getTokenDockRouter()), never to a dock directly, and never caches its address:

interface ITokenDock {
    function deposit(address token, uint256 amount) external;              // pulls with transferFrom, credits msg.sender
    function withdraw(address token, uint256 amount, address to) external; // never blocked
    function balanceOf(address token, address owner) external view returns (uint256);
    function totalReserved(address token) external view returns (uint256);
    function isWhitelisted(address vault, address token) external view returns (bool);

    event FlapTokenDockDeposited(address token, address owner, uint256 amount);
    event FlapTokenDockWithdrawn(address token, address owner, address to, uint256 amount);
}

balanceOf reads both docks, so it stays right when the balance moves to the verified dock.

Vault factory errors

Raised by DirectLaunchVaultFactoryBase's helpers:

ErrorCause
NotHub(caller)newVault was not called by the Portal.
ZeroAddress()token or creator is zero.
MissingVaultSlot(factory), DuplicateVaultSlot(factory), ZeroVaultSlotBps()Not exactly one VAULT slot naming this factory, or a zero share.
UnsupportedQuoteToken(quoteToken)isQuoteTokenSupported is false for the launch's quote.
UnsupportedLaunchKind(kind)Not DIRECT_V2_POOL_TAX.
UnexpectedDistributionTarget, UnexpectedDistributionAmountThe distribution is not the one the vault serves.
InvalidVaultData()vaultData is not the exact layout the factory decodes.

Checklist

  • newVault is onlyHub, rejects other launchKind values and calls _validateLaunch.
  • vaultData is decoded strictly: trailing or missing bytes and out-of-range values revert.
  • A vault that does not take the reserve calls _requireNoReserveDistribution; one that relies on it checks the exact distribution.
  • Everything is checked before the vault is deployed, and newVault never calls the token.
  • One vault per token, deployed deterministically behind a beacon; upgradeVaultImplementation is Guardian-only and the factory has no other owner.
  • isQuoteTokenSupported and vaultDataSchema match what newVault accepts; factorySpecVersion returns "pl-v1".
  • The vault initializes once (initializers disabled in its constructor) and re-checks its invariants in initialize.
  • Revenue is recognized by balance delta, and receive() cannot revert.
  • Privileged vault functions also accept the Guardian.
  • description() names every fee and every recipient; vaultUISchema() lists every user-facing method and its approvals.
  • If the vault takes the reserve, initializeDistribution is onlyHub, single-use, checks the received balance and deposits into the Token Dock.

1.3 Audit

Both factories go through Flap's audit before they are enabled, with the same process as on the bonding curve: submit your code to the Flap team, then a few review rounds until every finding is fixed (see Audit and the low-risk badge). The floor multiplier of your factory is agreed at this step. A factory is immutable: to change it, deploy a new one and have it audited.

1.4 Examples

You buildExampleWhat it shows
A Vault that takes the reserveStakeMineVaultDocks 80% of the supply, vests 1% to the dev and mines the rest to stakers.
A LauncherMintLauncherRaises a fixed amount, launches, lets contributors claim half of the supply; refunds if the target is missed.
A Launcher and a VaultCrowdLauncher + CrowdVaultA crowdfunded launch whose contributors then share the tax revenue. Each factory accepts only the other.

Each has a fork test against the live Portal on BNB Chain and BNB Testnet.

2. UI

A Pool Launch project shows up on Flap through a few UI components, each tied to one stage of the launch. This section explains what each one is for and how they connect. To build them, see the Pool Launch UI developer guide in the UI template.

2.1 Overview

UI componentPage on FlapPurpose
1. Launcher Config (for your Launcher Factory)/create/launcherThe creation form for your Launcher Factory. Users enter the fields your factory needs, and Flap prepares the transaction that creates a Launcher (a smart contract, with its launch page). A Mini App usually doesn't need it: it has its own creation page (see 2.3).
2. Launcher (the launch page)/launcher/{launcher address}The UI of one created Launcher. It shows the pre-launch stage: for example a mint or a fundraising round, with progress tracking and participation details.
3. Tax info/bnb/{token address}/taxinfoShown on the launched token. It shows the token's Vault: its tax revenue and the actions it offers its users, such as claims.
4. Mini App/apps/{mini app slug}/…A standalone app hosted by Flap, with its own subpages and routes (see Flap Apps). Use one when your project needs more than a single launch page, for example a launchpad of your own built on Pool Launch.

Each component shows data from its own contract. Flap prepares every transaction, and the user confirms it in their wallet.

There is no default Launcher Config or Launcher page yet. For your Launcher Factory, you must either implement both components, or build a Mini App and redirect from the launch page to it. The home page list is built from the Launcher description, so it works without a custom UI.

发射前发射后发射或跳转到你的 Mini App① Launcher 配置/create/launcher填写参数,创建 Launcher② 发射页/launcher/{address}铸造 · 募资 · 进度③ 税收信息/bnb/{token}/taxinfo代币页上的 Vault 面板④ Mini App/apps/{slug}/…你自己的子页面数据Launcher Factory数据Launcher数据代币 + Vault数据你的合约创建发射每个 UI 展示各自的合约 · 交易由 Flap 准备,用户在钱包中确认

2.2 Launching a single token

创建者所有用户参与者持有者/create/launcher名称 · 符号募资目标参与者份额创建/board?tab=launch-pages发射页MTKNEW/launcher/{address}MyToken募资 3 BNB · 剩 1 小时你的份额:0.2 BNB参与人数:128参与/bnb/{token}/taxinfo你的 Vault UI收益 · 领取1 · 创建 Launcher你的配置组件2 · 出现在首页列表新创建的 Launcher3 · 铸造 / 募资进度 · 参与详情4 · 代币上线交易 + 税收信息募资完成后,代币在同一笔交易中被创建并注入池子流动性
  1. Create the Launcher. On /create/launcher, the creator enters your Launcher Factory's address. Flap renders your Config components, the creator fills in the required fields, and the transaction creates the Launcher: a smart contract with its own launch page.
  2. Find it on the home page. The home page (/board?tab=launch-pages) lists newly created Launchers. Users open a Launcher's page from that list.
  3. Mint or contribute on the launch page. The launch page (/launcher/{launcher address}) works like a mint page: it shows the progress towards the target and the details of each participation, and users take part from there.
  4. The token goes live. When the fundraising completes, the token is created and its liquidity is added to the pool. The token then has its token page at /bnb/{token address}, and its tax info at /bnb/{token address}/taxinfo.

On BNB Chain Testnet, the token pages are under /bnb-testnet/ instead of /bnb/.

2.3 Building a Mini App

A Mini App, for example a launchpad of your own built on Pool Launch, lives on its own route on Flap and arranges its subpages under it:

  • the app's route is /apps/{mini app slug};
  • its subpages sit under that route: /apps/{mini app slug}/subpage1, /apps/{mini app slug}/subpage2, and so on.

Your Mini App is the main entrance, including creation. Users create launches on your Mini App's own creation page (for example /apps/{mini app slug}/create), so you usually don't need a Launcher Config component.

You still create a Launcher for each launch. Pool Launch accepts only the Launcher flow for now, so even when your Mini App runs all your subpages, each launch comes from a Launcher created by your Launcher Factory: your creation page creates it through the Portal. That Launcher still appears on the home page (/board?tab=launch-pages) and still has its launch page (/launcher/{launcher address}).

You don't rebuild your subpage on the launch page. Since there is no default launch page, the launch page needs your Launcher component, but it can be a small one: the SDK lets your Launcher component redirect from the launch page to the matching subpage of your Mini App. A typical user who doesn't yet use your Mini App:

  1. sees the new Launcher in the home page list;
  2. opens the Launcher's page, which shows "Redirecting…";
  3. is taken automatically to the corresponding subpage of your Mini App.
任意 Flap 用户你的 Mini App 用户/board?tab=launch-pages新 LauncherNEW① 看到新 Launcher/launcher/{address}正在跳转…host.redirect(path)② 打开发射页/apps/{slug}/subpage1/apps/{slug}/create/apps/{slug}/subpage1/apps/{slug}/subpage2③ 进入你的子页面打开自动/create 创建Launcher 合约仍然需要 · 由你的工厂创建无需在发射页复制子页面 · 返回键回到列表

The redirect replaces the launch page in the browser history, so Back returns to the page the user came from. It happens at most once per visit, and when the redirect is not available the launch page keeps showing your Launcher UI.