本页目前只有英文版。

Launch Tokens

This page shows how to launch a token on the bonding curve protocol: prepare its metadata, find a vanity salt, then call the right entry point for the kind of token you want.

1. Prepare the token metadata

A token's metadata (description, links and image) lives on IPFS. Upload the image and metadata together with Flap's upload API; it returns the metadata CID, which you pass as the meta field of the launch.

curl --request POST "https://funcs.flap.sh/api/upload" \
  -F 'operations={"query":"mutation Create($file: Upload!, $meta: MetadataInput!) { create(file: $file, meta: $meta) }","variables":{"file":null,"meta":{"description":"My token","website":"https://example.com","twitter":null,"telegram":null,"creator":"0x0000000000000000000000000000000000000000"}}}' \
  -F 'map={"0":["variables.file"]}' \
  -F '0=@./image.png;type=image/png'
# → {"data":{"create":"bafkrei…"}}   ← the metadata CID

The token's name and symbol are not part of the metadata; they are launch parameters. Always upload through Flap: the Flap indexer and many terminals only read files pinned on the Flap gateway. The full API (form parts, TypeScript example, pinning existing content) is in Flap IPFS Gateway.

2. Find the salt

The Portal deploys every token with CREATE2, so the token address is fixed by the salt you pass. It must end with Flap's vanity suffix:

TokenAddress must end withToken implementation used for the prediction
Tax token (TOKEN_TAXED_V3)7777Tax Token V3 Impl
Non-tax token (TOKEN_V3_PERMIT)8888Standard Token V3 Impl

These are the BNB Chain suffixes; some chains use a different one (for example 1111 for tax tokens on Monad), shown in the Notes column of Deployed Contract Addresses next to each implementation address. The Portal (from below) is the bonding curve Portal; this is true even when you launch through VaultPortal, which deploys through the Portal.

Find a salt by hashing until the predicted address has the suffix (a few tens of thousands of tries on average):

import { getContractAddress, keccak256, toBytes, toHex, type Address, type Hex } from "viem";
import { generatePrivateKey } from "viem/accounts";

function findVanitySalt(suffix: "7777" | "8888", tokenImpl: Address, portal: Address) {
  // EIP-1167 minimal-proxy init code for `tokenImpl`
  const bytecode = ("0x3d602d80600a3d3981f3363d3d373d3d3d363d73" +
    tokenImpl.slice(2).toLowerCase() +
    "5af43d82803e903d91602b57fd5bf3") as Hex;
  const predict = (salt: Hex) =>
    getContractAddress({ from: portal, salt: toBytes(salt), bytecode, opcode: "CREATE2" });

  let salt = keccak256(toHex(generatePrivateKey())); // any random 32-byte seed
  while (!predict(salt).toLowerCase().endsWith(suffix)) salt = keccak256(salt);
  return { salt, token: predict(salt) };
}

Lock the salt (optional)

A salt is public once your launch transaction is in the mempool, so someone could launch a different token at your address first. To prevent that, reserve it beforehand:

/// tokenVersion must be TOKEN_TAXED_V3 (6) or TOKEN_V3_PERMIT (7); msg.value must equal SALT_LOCK_FEE().
function lockSalt(bytes32 salt, TokenVersion tokenVersion) external payable;

Only the locker can then launch with that salt (FlapSaltLockedByAnotherUser otherwise). Lock on the Portal even when you launch through VaultPortal: it passes your address through. SALT_LOCK_FEE() is 0.01 BNB on BNB Chain.

3. Choose the entry point

Tax tokenNon-tax token
Without vaultPortal.newTokenV6 · tokenVersion = TOKEN_TAXED_V3 · examplePortal.newTokenV7 · tokenVersion = TOKEN_V3_PERMIT · example
With vaultVaultPortal.newTokenV6WithVault · tokenVersion = TOKEN_TAXED_V3 · exampleVaultPortal.newTokenV7WithVault · tokenVersion = TOKEN_V3_PERMIT · example
  • Portal or VaultPortal? Use VaultPortal when the token's revenue should go to a vault created in the same transaction; otherwise call the Portal directly.
  • V6 or V7? Tax tokens use the V6 entry points, non-tax tokens the V7 ones. See About newTokenV7 for why.
  • All four are payable. For a native quote, send the amount for the creator's first buy (quoteAmt) as msg.value, or 0 for none. For an ERC-20 quote, send no value and approve the contract you call instead (see ERC-20 quote tokens).

Current limits (BNB Chain, Portal v5.27.2):

  • Portal.newTokenV7 (non-tax, without vault): only a single DIVIDEND fee slot is accepted (100% of the revenue to holders, paid in the quote token). A MARKETING_OR_VAULT slot reverts NewTokenV7RuleViolation(8); DEFLATION and LP_BPS revert UnsupportedV7TokenV3PermitFeeType.
  • VaultPortal.newTokenV7WithVault (non-tax, with vault): only two whitelisted vault factories are accepted ("Only supports gift vaultFactory" otherwise). Both support only native BNB today.
  • VaultPortal.newTokenV6WithVault: mktBps > 0 (the vault's share), and the vault factory must support the quote token (see ERC-20 quote tokens).
  • Older entry points (newTokenV2–newTokenV5, newTaxTokenWithVault) and the legacy token versions (TOKEN_V2_PERMIT and Tax Token V1/V2 through newTokenV6) are disabled for new launches.

Common parameters

All four entry points share these fields:

FieldMeaning
name, symbolToken name and symbol.
metaThe metadata CID from step 1.
dexThreshWhen the token migrates to a DEX. Only 1 (FOUR_FIFTHS, 80% of the supply sold) is accepted today.
saltThe salt from step 2.
migratorTypeThe pool it migrates into: 1 V2 for tax tokens; 3 PancakeSwap Infinity CL for V7 non-tax tokens on BNB Chain.
quoteTokenThe curve's quote token; address(0) = native gas token.
quoteAmtQuote spent on the creator's first buy at launch (0 for none). For a native quote, send it as msg.value; for an ERC-20 quote, approve it to the contract you call.
permitDataAn optional EIP-2612 permit for an ERC-20 quoteAmt (spender = the contract you call), or 0x.
extensionID, extensionDataOptional extensions such as an Agent ID gate; bytes32(0) / 0x for none. See Extension encoding.
dexIdThe DEX it migrates to (chain-specific; 0 = PancakeSwap on BNB Chain).
buyTaxRate, sellTaxRateTax in bps (100–1000); 0 for non-tax tokens.
taxDurationHow long the tax applies, in seconds.
antiFarmerDurationSeconds after launch during which dividend farming is blocked (at most 365 days).
commissionReceiverYour address to earn a commission on a tax token (Commission receiver); address(0) for none. Must be 0 for non-tax tokens.

Tax token, without vault

Portal.newTokenV6 with tokenVersion = TOKEN_TAXED_V3. The tax is split between a beneficiary wallet, burning, liquidity and dividends to holders.

function newTokenV6(NewTokenV6Params calldata params) external payable returns (address token);

struct NewTokenV6Params {
    string name; string symbol; string meta;
    DexThreshType dexThresh; bytes32 salt; MigratorType migratorType;
    address quoteToken; uint256 quoteAmt; address beneficiary;
    bytes permitData; bytes32 extensionID; bytes extensionData;
    DEXId dexId; V3LPFeeProfile lpFeeProfile;
    uint16 buyTaxRate; uint16 sellTaxRate; uint64 taxDuration; uint64 antiFarmerDuration;
    uint16 mktBps; uint16 deflationBps; uint16 dividendBps; uint16 lpBps;   // must sum to 10000
    uint256 minimumShareBalance;    // ≥ 10,000 tokens when dividendBps > 0
    address dividendToken;
    address commissionReceiver;
    TokenVersion tokenVersion;      // TOKEN_TAXED_V3 (6)
}

Example: a 3% buy / 5% sell tax for one year; 40% of it to the beneficiary, 60% paid to holders as dividends in BNB.

const params = {
  name: "My Token", symbol: "MTK", meta: metadataCid,
  dexThresh: 1,                     // 80%
  salt,                             // 7777 suffix, Tax Token V3 Impl
  migratorType: 1,                  // V2_MIGRATOR (required for tax tokens)
  quoteToken: zeroAddress,          // BNB
  quoteAmt: parseEther("0.1"),      // creator's first buy
  beneficiary: myWallet,
  permitData: "0x", extensionID: zeroHash, extensionData: "0x",
  dexId: 0, lpFeeProfile: 0,
  buyTaxRate: 300, sellTaxRate: 500,
  taxDuration: 365n * 86400n, antiFarmerDuration: 3600n,
  mktBps: 4000, deflationBps: 0, dividendBps: 6000, lpBps: 0,
  minimumShareBalance: parseEther("10000"),
  dividendToken: zeroAddress,       // pay dividends in the quote token
  commissionReceiver: zeroAddress,
  tokenVersion: 6,                  // TOKEN_TAXED_V3
};
await portal.write.newTokenV6([params], { value: params.quoteAmt });
  • mktBps + deflationBps + dividendBps + lpBps must be 10000 (of the tax, after the protocol fee). mktBps goes to beneficiary.
  • dividendToken: address(0) or the quote token pays holders in the quote token; 0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd pays them in the token itself; any other ERC-20 with a swap path in SwapRegistry also works. Details: Dividend token.
  • Tax tokens always migrate to a V2 pool (migratorType = V2_MIGRATOR).

Tax token, with vault

VaultPortal.newTokenV6WithVault. Same fields as newTokenV6 without beneficiary, plus the vault factory and its init data. VaultPortal creates the vault, then launches the token with the vault as the beneficiary, so the mktBps share of the tax goes to the vault, whose code decides what happens to it.

function newTokenV6WithVault(NewTokenV6WithVaultParams calldata params) external payable returns (address token);
// NewTokenV6WithVaultParams = NewTokenV6Params − beneficiary + { address vaultFactory; bytes vaultData; }
const params = {
  ...taxTokenParams,                // as above, without `beneficiary`
  mktBps: 8000, dividendBps: 2000,  // 80% of the tax to the vault, 20% to holders
  vaultFactory: MY_VAULT_FACTORY,
  vaultData: encodeAbiParameters(   // the factory's own format, here (uint256 maxReward, uint256 cooldown)
    [{ type: "uint256" }, { type: "uint256" }],
    [parseEther("0.1"), 3600n],
  ),
};
await vaultPortal.write.newTokenV6WithVault([params], { value: params.quoteAmt });
  • vaultData is defined by each factory: read its vaultDataSchema(). The example uses the layout of the FreeCoin V3 example factory, (uint256 maxReward, uint256 cooldown).
  • mktBps must be greater than 0 (it is the vault's share).
  • The quote token can be native or an ERC-20, as long as the vault factory supports it: VaultPortal checks isQuoteTokenSupported(quoteToken) on the factory (UnsupportedQuoteToken otherwise) and, for newer factories, that the new vault's vaultQuoteToken() matches (VaultQuoteTokenMismatch). See ERC-20 quote tokens.
  • The factory can add its own launch rules (allowed tax rates, shares, dividend token, …), enforced before the vault is created; a launch that breaks them reverts with the factory's reason. Its tokenCreationPolicies() lists them for UIs.
  • VaultPortal emits FlapTaxVaultTokenCreated(token, vault, vaultFactory). Whether a factory is enabled and its risk level are set by Flap; see Customization Using Vault.

Non-tax token, without vault

Portal.newTokenV7 with tokenVersion = TOKEN_V3_PERMIT. The token has no tax; what it shares instead is the LP fee of its pool after migration (TOKEN_V3_PERMIT migrates to PancakeSwap Infinity CL on BNB Chain). feeConfigs decides where that revenue goes.

function newTokenV7(NewTokenV7Params calldata params) external payable returns (address token);

enum FeeType { NONE, MARKETING_OR_VAULT, DIVIDEND, DEFLATION, LP_BPS }

struct FeeConfig {
    FeeType feeType;
    uint16 bps;
    address marketingAddress;     // MARKETING_OR_VAULT only
    address dividendToken;        // DIVIDEND only
    uint256 minimumShareBalance;  // DIVIDEND only, ≥ 10,000 tokens
}

struct NewTokenV7Params {
    string name; string symbol; string meta;
    DexThreshType dexThresh; bytes32 salt; MigratorType migratorType;
    address quoteToken; uint256 quoteAmt; bytes permitData;
    bytes32 extensionID; bytes extensionData; DEXId dexId;
    uint16 buyTaxRate; uint16 sellTaxRate; uint64 taxDuration; uint64 antiFarmerDuration;
    address commissionReceiver;
    TokenVersion tokenVersion;      // TOKEN_V3_PERMIT (7)
    FeeConfig[4] feeConfigs;        // replaces beneficiary + mktBps/deflationBps/dividendBps/lpBps
}

Example: all of the revenue paid to holders as dividends in BNB (the only mode open today).

const none = { feeType: 0, bps: 0, marketingAddress: zeroAddress, dividendToken: zeroAddress, minimumShareBalance: 0n };
const params = {
  name: "My Token", symbol: "MTK", meta: metadataCid,
  dexThresh: 1, salt,               // 8888 suffix, Standard Token V3 Impl
  migratorType: 3,                  // PCS_INFINITY_CL_MIGRATOR
  quoteToken: zeroAddress, quoteAmt: parseEther("0.1"), permitData: "0x",
  extensionID: zeroHash, extensionData: "0x", dexId: 0,
  buyTaxRate: 0, sellTaxRate: 0, taxDuration: 0n, antiFarmerDuration: 0n,
  commissionReceiver: zeroAddress,
  tokenVersion: 7,                  // TOKEN_V3_PERMIT
  feeConfigs: [
    { feeType: 2, bps: 10000, marketingAddress: zeroAddress,
      dividendToken: zeroAddress,                   // must equal quoteToken
      minimumShareBalance: parseEther("10000") },
    none, none, none,
  ],
};
await portal.write.newTokenV7([params], { value: params.quoteAmt });
  • Active slots must sum to 10000 bps, each with bps > 0, and no fee type twice. Today only the single DIVIDEND slot above is accepted (see the limits above).
  • Tax rates, taxDuration and commissionReceiver must be 0.

Non-tax token, with vault

VaultPortal.newTokenV7WithVault. Same fields as newTokenV7, plus the vault factory and its init data. Put exactly one MARKETING_OR_VAULT slot in feeConfigs: its address is a placeholder that VaultPortal replaces with the new vault, so the vault receives that share of the revenue.

function newTokenV7WithVault(NewTokenV7WithVaultParams calldata params) external payable returns (address token);
// NewTokenV7WithVaultParams = NewTokenV7Params + { address vaultFactory; bytes vaultData; }
const params = {
  ...nonTaxParams,                  // as above
  feeConfigs: [
    { feeType: 1, bps: 10000, marketingAddress: myWallet,   // placeholder → replaced by the vault
      dividendToken: zeroAddress, minimumShareBalance: 0n },
    none, none, none,
  ],
  vaultFactory: VAULT_FACTORY,       // must be one of the accepted factories (see the limits above)
  vaultData,                         // that factory's format, from its vaultDataSchema()
};
await vaultPortal.write.newTokenV7WithVault([params], { value: params.quoteAmt });
  • Zero or several MARKETING_OR_VAULT slots revert InvalidFeeConfig().
  • Today only two factories are accepted (BNB Chain: 0x025549F52B03cF36f9e1a337c02d3AA7Af66ab32, 0x6909aD1822Ece349CDDAb98E6F62EeeD9fAa2e10). VaultPortal applies the same quote-token check as for V6, and both support only native BNB, so V7 vault launches use BNB for now.

ERC-20 quote tokens

A token can use an ERC-20 (for example USDT) as its quote token instead of the native gas token. Set quoteToken to the ERC-20, send no msg.value, and fund the creator's first buy (quoteAmt) with an allowance:

  • Without vault (Portal): approve the Portal for quoteAmt, or pass a permit with the Portal as the spender in permitData.
  • With vault (VaultPortal): approve the VaultPortal, not the Portal, or pass a permit with the VaultPortal as the spender. VaultPortal pulls quoteAmt from you, approves the Portal for exactly that amount, then sends you the bought tokens and refunds any quote left unused.

With a vault, the vault factory must support the quote: call isQuoteTokenSupported(quoteToken) on the factory before launching. Most factories deployed today accept native BNB only.

const quoteAmt = parseUnits("10", 18);                       // 10 USDT first buy
await usdt.write.approve([VAULT_PORTAL, quoteAmt]);          // the VaultPortal, not the Portal
await vaultPortal.write.newTokenV6WithVault([{
  ...params,
  quoteToken: USDT, quoteAmt, permitData: "0x",
  vaultFactory: MY_VAULT_FACTORY,                            // isQuoteTokenSupported(USDT) == true
  vaultData,
  dividendToken: USDT,                                       // pay holders in the quote token
}]); // no `value`

About newTokenV7

newTokenV7 is the next version of the launch interface, and a deliberate breaking change: it replaces the single beneficiary and the four mktBps / deflationBps / dividendBps / lpBps fields with a list of up to four feeConfigs. It is designed to cover everything newTokenV6 can do, tax tokens included.

For now, newTokenV7 launches non-tax tokens only (TOKEN_TAXED_V3 through it reverts NewTokenV7RuleViolation(4)). This is intentional: newTokenV6 stays the entry point for tax tokens, because integrators already know it and its flat fields are simple to understand. Use newTokenV6 for tax tokens and newTokenV7 for non-tax tokens.

Errors you may see

ErrorCause
VanityAddressRequirementNotMet(address)The salt does not give the 7777 / 8888 suffix for this token type (wrong implementation or Portal in the prediction).
FlapSaltLockedByAnotherUser(bytes32, address)Someone else locked this salt.
FeatureDisabled()Entry point or token version not open (for example TOKEN_V2_PERMIT through newTokenV6).
NewTokenV7RuleViolation(uint8)A newTokenV7 rule: 4 tax tokens not open on V7, 8 marketing/vault slot on a non-tax launch, 2 commission on a non-tax token, 7 wrong migrator.
InvalidTaxDistribution()mktBps + deflationBps + dividendBps + lpBps is not 10000.
MinimumShareBalanceTooLow()Dividends enabled with minimumShareBalance below 10,000 tokens.
InvalidDexThresholdType(uint8)dexThresh is not 1 (FOUR_FIFTHS).
UnsupportedQuoteToken(address)The vault factory does not support this quote token (isQuoteTokenSupported is false).
VaultQuoteTokenMismatch(address, address)The vault the factory created reports a different vaultQuoteToken() than the launch's quote.
PermitFailed(address, address, address)VaultPortal with an ERC-20 quote: after the permit, your allowance to VaultPortal is still below quoteAmt.