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 CIDThe 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:
| Token | Address must end with | Token implementation used for the prediction |
|---|---|---|
Tax token (TOKEN_TAXED_V3) | 7777 | Tax Token V3 Impl |
Non-tax token (TOKEN_V3_PERMIT) | 8888 | Standard 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 token | Non-tax token | |
|---|---|---|
| Without vault | Portal.newTokenV6 · tokenVersion = TOKEN_TAXED_V3 · example | Portal.newTokenV7 · tokenVersion = TOKEN_V3_PERMIT · example |
| With vault | VaultPortal.newTokenV6WithVault · tokenVersion = TOKEN_TAXED_V3 · example | VaultPortal.newTokenV7WithVault · tokenVersion = TOKEN_V3_PERMIT · example |
- Portal or VaultPortal? Use
VaultPortalwhen the token's revenue should go to a vault created in the same transaction; otherwise call thePortaldirectly. - V6 or V7? Tax tokens use the V6 entry points, non-tax tokens the V7 ones. See About
newTokenV7for why. - All four are
payable. For a native quote, send the amount for the creator's first buy (quoteAmt) asmsg.value, or0for 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 singleDIVIDENDfee slot is accepted (100% of the revenue to holders, paid in the quote token). AMARKETING_OR_VAULTslot revertsNewTokenV7RuleViolation(8);DEFLATIONandLP_BPSrevertUnsupportedV7TokenV3PermitFeeType.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_PERMITand Tax Token V1/V2 throughnewTokenV6) are disabled for new launches.
Common parameters
All four entry points share these fields:
| Field | Meaning |
|---|---|
name, symbol | Token name and symbol. |
meta | The metadata CID from step 1. |
dexThresh | When the token migrates to a DEX. Only 1 (FOUR_FIFTHS, 80% of the supply sold) is accepted today. |
salt | The salt from step 2. |
migratorType | The pool it migrates into: 1 V2 for tax tokens; 3 PancakeSwap Infinity CL for V7 non-tax tokens on BNB Chain. |
quoteToken | The curve's quote token; address(0) = native gas token. |
quoteAmt | Quote 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. |
permitData | An optional EIP-2612 permit for an ERC-20 quoteAmt (spender = the contract you call), or 0x. |
extensionID, extensionData | Optional extensions such as an Agent ID gate; bytes32(0) / 0x for none. See Extension encoding. |
dexId | The DEX it migrates to (chain-specific; 0 = PancakeSwap on BNB Chain). |
buyTaxRate, sellTaxRate | Tax in bps (100–1000); 0 for non-tax tokens. |
taxDuration | How long the tax applies, in seconds. |
antiFarmerDuration | Seconds after launch during which dividend farming is blocked (at most 365 days). |
commissionReceiver | Your 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 + lpBpsmust be10000(of the tax, after the protocol fee).mktBpsgoes tobeneficiary.dividendToken:address(0)or the quote token pays holders in the quote token;0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEdpays them in the token itself; any other ERC-20 with a swap path inSwapRegistryalso 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 });vaultDatais defined by each factory: read itsvaultDataSchema(). The example uses the layout of the FreeCoin V3 example factory,(uint256 maxReward, uint256 cooldown).mktBpsmust be greater than0(it is the vault's share).- The quote token can be native or an ERC-20, as long as the vault factory supports it:
VaultPortalchecksisQuoteTokenSupported(quoteToken)on the factory (UnsupportedQuoteTokenotherwise) and, for newer factories, that the new vault'svaultQuoteToken()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. VaultPortalemitsFlapTaxVaultTokenCreated(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
10000bps, each withbps > 0, and no fee type twice. Today only the singleDIVIDENDslot above is accepted (see the limits above). - Tax rates,
taxDurationandcommissionReceivermust be0.
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_VAULTslots revertInvalidFeeConfig(). - Today only two factories are accepted (BNB Chain:
0x025549F52B03cF36f9e1a337c02d3AA7Af66ab32,0x6909aD1822Ece349CDDAb98E6F62EeeD9fAa2e10).VaultPortalapplies 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 thePortalforquoteAmt, or pass a permit with thePortalas the spender inpermitData. - With vault (
VaultPortal): approve theVaultPortal, not thePortal, or pass a permit with theVaultPortalas the spender.VaultPortalpullsquoteAmtfrom you, approves thePortalfor 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
| Error | Cause |
|---|---|
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. |