What the Flap Protocol Docs Don't Tell You
I built a non-custodial token launch and trading console for the Flap protocol on BNB Chain. Launch tokens through Flap's Portal contract from your own UI: metadata upload to IPFS, CREATE2 vanity-salted deployment, bonding-curve quoting, and slippage-protected trades.
The interesting part was never the UI. It was everything the docs got wrong. This post is the field report: five findings verified against live BNB mainnet and testnet state, with the code that worked around them. The full source is public at github.com/adriel-oloko/flap-launchpad.
How I verify (no wallet required)
Before writing any UI, I built probe scripts that read live chain state with plain RPC calls, no wallet, no gas:
- ABI probes against the Portal contract
-
eth_getLogsevent scans forTokenCreated - Live
quoteExactInputcalls against the newest token - Transaction replays of recent launches
- Gas measurement of real launch transactions
That discipline is why the findings below exist. Every claim in this post was reproduced on-chain, not taken from a README.
Finding 1: The docs version table is stale
Flap's developer docs list Portal versions v5.8.5 (mainnet) and v5.14.16 (testnet). The live portals report something else entirely via version():
| Network | Docs say | Live Portal reports |
|---|---|---|
| BNB mainnet | v5.8.5 | v5.16.1 |
| BNB testnet | v5.14.16 | v5.15.3 |
The version skew is not cosmetic. The documented read path, getTokenV8Safe, reverts on the testnet portal with selector 0xde6137d1. A brand new integrator following the docs would hit a hard revert on testnet and assume their setup was wrong.
The fix is a silent fallback: try V8, fall back to V6, and normalize both shapes into one internal model:
export async function getTokenInfo(client: PublicClient, token: Address): Promise<TokenInfo | null> {
try {
const state = await client.readContract({
address: FLAP_CONFIG.portalAddress,
abi: PORTAL_ABI,
functionName: 'getTokenV8Safe',
args: [token],
});
return normalizeV8Safe(state as unknown as TokenStateV8Safe);
} catch {
try {
const state = await client.readContract({
address: FLAP_CONFIG.portalAddress,
abi: PORTAL_ABI,
functionName: 'getTokenV6',
args: [token],
});
return normalizeV6(state as unknown as TokenStateV6);
} catch {
return null;
}
}
}
One shape difference to handle: V6 only exposes a single symmetric tax rate, V8 exposes separate buy and sell rates. The normalizer maps taxRate to both sides so downstream code never cares which version answered.
Finding 2: Standard launches are disabled on mainnet
The protocol's standard launch path (V2_PERMIT) reverts with FeatureDisabled (selector 0xac5f6092) on both live portals. newTokenV7 is also unusable, reverting with NewTokenV7RuleViolation.
Only the TAXED_V3 path works today. That is a big deal for a token launcher: every launch goes through the taxed implementation, so every token gets per-side tax, and your UI cannot pretend otherwise. My console builds newTokenV6 params exclusively for the taxed path and enforces the constraint before signing.
Finding 3: The dex threshold gate
Even within TAXED_V3, newTokenV6 only accepts DexThreshType.FOUR_FIFTHS (value 1). Any other threshold reverts InvalidDexThresholdType (selector 0x77146b42).
The lesson for integrators: enums with exact Solidity values are part of the ABI contract. I mirrored them in TypeScript with the raw values, so the UI cannot construct an invalid call:
export const DexThreshType = {
ONE_HALF: 0,
FOUR_FIFTHS: 1, // the only value the live portals accept
ONE_AND_A_HALF: 2,
} as const;
Finding 4: CREATE2 vanity salts
Token addresses are CREATE2-predictable via EIP-1167 minimal-proxy init code. Flap's docs show the standard 3d602d80600a3d3981f3... proxy prefix with the implementation address embedded, and the launcher searches locally for a salt whose predicted address ends in a vanity suffix (8888 standard, 7777 tax):
const EIP1167_PREFIX = '0x3d602d80600a3d3981f3363d3d373d3d3d363d73';
const EIP1167_SUFFIX = '5af43d82803e903d91602b57fd5bf3';
export function predictTokenAddress(salt: Hex, tokenImpl: Address, portal: Address): Address {
const bytecode = `${EIP1167_PREFIX}${tokenImpl.slice(2).toLowerCase()}${EIP1167_SUFFIX}` as Hex;
return getContractAddress({
from: portal,
salt: toBytes(salt),
bytecode,
opcode: 'CREATE2',
});
}
export function findVanityTokenSalt(suffix: string, tokenImpl: Address, portal: Address): VanitySalt {
const seed = generatePrivateKey();
let salt = keccak256(toHex(seed));
let iterations = 0;
while (!predictTokenAddress(salt, tokenImpl, portal).endsWith(suffix)) {
salt = keccak256(salt);
iterations++;
}
return { salt, address: predictTokenAddress(salt, tokenImpl, portal), iterations };
}
A 4-character hex suffix averages ~65k keccak iterations. That runs sub-second locally, which means the vanity search happens client-side before any transaction, no server needed.
Finding 5: RPC log limits and the gas clamp
Two operational traps that only show up under real load:
Log limits. The testnet is spammy, and wide eth_getLogs ranges exceed provider caps. Event scans run in 1000-block chunks over a 400k-block lookback, and the app pins RPCs that support log-heavy reads.
Gas clamp. Public BNB gateways reject raw transactions above 16,777,216 gas, and some return wildly inflated estimates: I observed ~34.8M for launches that really use ~2M. Every estimate is clamped to maxTxGas (15M), which keeps broadcasts inside what the gateways accept.
Bonus: gasless sells via permit
Sells on the bonding curve can skip the approve transaction entirely. swapExactInput accepts ERC-2612 permitData (an EIP-712 typed signature), so the sell is one transaction: sign the permit off-chain, include it in the swap call. Tokens without permit support fall back to the approve path. One less transaction, one less point of failure.
What I learned
- Docs drift on live protocols. Version tables, function availability, and enum ranges all change while documentation lags. Treat docs as a starting point and the chain as the source of truth.
-
Revert selectors are a debugging gift.
0xde6137d1,0xac5f6092,0x77146b42each point to a specific gate in the contract. 4byte.directory and the ABI itself decode them faster than reading docs. -
Build verification into the workflow. The no-wallet probe scripts (in
scripts/of the repo) made every finding reproducible in seconds. That is the difference between "I think this works" and "I verified this works."
The complete, reproducible tooling is in the repo: github.com/adriel-oloko/flap-launchpad, including verify-contracts.mjs, measure-gas.mjs, and the deeper protocol probes. If you are building on Flap, Uniswap v4 launchpads, or Robinhood Chain, I am open to contract work and full-time roles. DMs open.
Top comments (0)