Microsales
Microsales do not have their own contract. They share the one launchpad address with pools and Melt Pools — one address per chain, one ABI, all three launch styles.
| Network | Chain ID | Launchpad |
|---|---|---|
| BNB Chain (mainnet) | 56 | 0xD9CcE715619fA9082d21375B6B37A3a0d99138F4 |
| BNB Chain (testnet) | 97 | 0x0AA21d47b600B04527c09BbD9A30aF32cb64f6cF |
| Robinhood Chain | 4663 | 0x7E0752B42f9db92F12E221E10fFfa10bfE8668C6 |
An individual microsale is identified by its fundManager address, which you pass as an argument to
the calls below. That is a per-sale handle, not a contract you call.
A microsale is a fixed-price token sale. You pick a config template, the launchpad mints a fresh token, contributors send funds until the hard cap is hit, and on finalize the launchpad lists the token by adding liquidity to a DEX. Every microsale creates exactly one token, and a token can be launched through a microsale only once.
This page walks the whole lifecycle: choose a template, create, contribute, finalize, claim, and handle refunds. Vault-backed launches get their own page, vaults.md, but the create call is summarized here too.
Contents
- The pieces
- Sale states
- Config templates
- Allowed quote tokens
- Token types and create paths
- Creating a microsale
- Contributing
- Finalizing
- Claiming
- Refunds and exits
- Reading sale state
- Vanity token addresses
- Error reference
The pieces
- The config (template). The platform publishes reusable templates that fix the economics: how
many tokens go to the presale, how many to liquidity, the hard cap, and the sale duration. You
launch against a template by its
configId. - The sale. One instance created from a template. Identified by its
fundManageraddress. - The token. Minted at create time. Total supply is fixed at 1,000,000,000 (1e27 with 18 decimals) and split between presale and LP per the template.
- The fund manager. A per-sale address that holds the raise and the token until finalize. You never call it directly. It is the handle you pass to almost every microsale function, and the key events are indexed by it.
Sale states
A microsale is always in one of three states:
| State | Value | Meaning |
|---|---|---|
Active | 0 | Open for contributions |
Finalized | 1 | Hard cap hit, liquidity added, tokens claimable |
Cancelled | 2 | Called off; contributors can withdraw |
A key detail: a microsale finalizes only when the raise reaches the hard cap exactly.
Contributions are capped so totalRaised can never exceed hardCap, and the last contribution
that closes the gap sets filledAt. A sale that reaches its end time without filling stays
Active but becomes refundable (see Refunds and exits).
Config templates
You do not set the economics of a sale; you pick from templates the platform publishes. Discover them before showing a create form:
// Full template objects (recommended for discovery)
const configs = await launchpad.getActiveMicroSaleConfigs();
for (const c of configs) {
console.log({
configId: c.configId,
presaleTokens: c.presaleTokens,
lpTokens: c.lpTokens,
hardCap: c.hardCap, // in the template's raise currency
duration: c.duration, // seconds
active: c.active,
});
}
Related reads:
| Function | Returns |
|---|---|
getActiveMicroSaleConfigs() | array of active MicroSaleConfig |
getActiveMicroSaleConfigIds() | array of active configId values |
getMicroSaleConfig(configId) | one template |
getMicroSaleConfigsCount() | total templates ever created |
The rate is derived from the template
You do not set a price. The launchpad computes two rates from the template and hard cap:
- Presale rate =
presaleTokens * 1e18 / hardCap. This is how many token units a contributor gets per unit of raise currency:purchased = amount * rate / 1e18. - Listing rate =
lpTokens * 1e18 / (hardCap * 0.95). This sets the opening DEX price. The 0.95 is the 5% platform fee taken out of the raise before liquidity is added.
Raise currency
Most templates raise in the chain's native coin (BNB). Some templates are bound to an ERC20 quote token instead. Check the binding before you create or contribute:
const quote = await launchpad.getMicroSaleConfigQuoteToken(configId);
// address(0) => native (BNB) raise
// otherwise => contributors pay with that ERC20
The binding applies to no-tax, wallet-receiver, and vault launches. A native raise uses
contributeMicroSale; an ERC20 raise uses contributeMicroSaleQuote. Mixing them reverts.
Allowed quote tokens
Microsales and Melt Pool curves raise in the chain's native coin or any token on the
launchpad's shared quote-token allow-list — read it live with getAllowedQuoteTokens() and check one
with isQuoteTokenAllowed(token). See Supported currencies for the full
per-chain list.
Token types and create paths
There are three current create paths. Each returns the new sale's fundManager address.
| Function | Token | Tax | Raise currency | Fee receiver |
|---|---|---|---|---|
createMicroSaleNoTaxToken | CheeseToken | none | template-bound native or ERC20 | n/a |
createMicroSaleTaxTokenV2 | CheeseTaxTokenV2 | buy/sell tax | template-bound native or ERC20 | a wallet you name |
createMicroSaleTaxTokenWithVaultV2 | CheeseTaxTokenV2 | buy/sell tax | template-bound native or ERC20 | a vault deployed at launch |
The tax-token paths mint a token that taxes buys and sells and routes that revenue to a fee receiver. The plain path mints a normal ERC20 with no tax and no fee receiver. Pick the vault path when you want the revenue to flow into programmable payout logic; see vaults.md.
Verifying token source
Every token minted by a microsale is one of two contracts: the plain ERC20 (CheeseToken) or the
tax-collecting ERC20 (CheeseTaxTokenV2). Their source is published in
cheesepad-ai/cheesepad-contracts.
The downloadable ABIs and token-specific getters are covered in Minted tokens. This
applies to tokens minted by Melt Pools as well.
Creating a microsale
Shared parameters
Every create call takes these fields (inside CreateParams):
| Field | Type | Notes |
|---|---|---|
configId | uint256 | the template to launch against; must be active |
name | string | token name |
symbol | string | token symbol |
poolDetails | string | free-form metadata (JSON, a URL, whatever your UI stores) |
maxContribution | uint256 | per-address cap on cumulative contribution; 0 means unlimited |
salt | bytes32 | fresh CREATE2 salt returned by the CheesePad API |
lockDuration | uint256 | LP lock in seconds; 0 burns LP instead of locking |
lockDuration rules: it is either 0 (LP is burned at finalize) or between the chain minimum
(365 days on both mainnets, 5 minutes on BNB testnet) and a 10-year ceiling. Anything else reverts
with Invalid lock duration.
If the platform charges a creation fee, send it as msg.value on the create call (see
getting-started.md).
Generate the token salt
Request a fresh salt immediately before every create call. The response also contains the token address that the launchpad will deploy. Do not reuse a salt for another launch.
type SaltTokenType = "CheeseToken" | "CheeseTaxTokenV2";
async function generateTokenSalt(
tokenType: SaltTokenType,
name: string,
symbol: string,
router: string,
): Promise<{ salt: string; address: string }> {
const { chainId } = await provider.getNetwork();
const query = new URLSearchParams({
tokenType,
launchpadContract: LAUNCHPAD,
chainId: chainId.toString(),
name,
symbol,
router,
});
const response = await fetch(
`https://api.cheesepad.ai/microsale/generate-salt?${query}`,
);
if (!response.ok) {
throw new Error(`Salt generation failed: ${response.status} ${await response.text()}`);
}
return response.json();
}
Pass one of the launchpad's supported routers. For a V2 tax-token launch, use the same dexKey
that you pass to the create function. The API always returns a 32-byte salt and its corresponding
address.
Choose a DEX for a tax token
The two V2 tax-token calls take a dexKey after their other arguments. Use a supported router whose
DEX entry is enabled:
const [dexKey] = await launchpad.getSupportedRouters();
const dex = await launchpad.getDex(dexKey);
if (!dex.enabled) throw new Error("DEX is not enabled");
The launchpad supplies that DEX's venue configuration to the token. Callers do not need to discover or calculate pair hashes or fee tiers.
No-tax token
The simplest launch. The selected template decides whether contributors pay in native currency or an ERC20 quote token.
const name = "My Token";
const symbol = "MTK";
const [router] = await launchpad.getSupportedRouters();
if (!router) throw new Error("No supported router");
const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseToken",
name,
symbol,
router,
);
const params = {
configId: 0n,
name,
symbol,
poolDetails: "",
maxContribution: 0n, // unlimited per wallet
salt,
lockDuration: 0n, // burn LP at finalize
};
const creationFee = (await launchpad.getMasterConfig()).microSaleCreationFee;
const tx = await launchpadRW.createMicroSaleNoTaxToken(params, { value: creationFee });
const receipt = await tx.wait();
// The fundManager and token come from the MicroSaleCreated event
const ev = receipt.logs.find((l) => l.eventName === "MicroSaleCreated");
const fundManager = ev.args.fundManager;
const token = ev.args.token;
if (token.toLowerCase() !== predictedToken.toLowerCase()) {
throw new Error("Created token does not match the predicted address");
}
Tax token (wallet fee receiver)
Same shape plus a taxConfig describing the tax and where it goes. Revenue is sent to a plain
wallet you name.
const name = "Taxed";
const symbol = "TAX";
const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseTaxTokenV2",
name,
symbol,
dexKey,
);
const taxConfig = {
buyTaxBps: 300n, // 3% on buys; each side max 1000 (10%)
sellTaxBps: 500n, // 5% on sells; at least one side must be > 0
taxDuration: 365n * 24n * 60n * 60n, // required; seconds after listing
antiFarmerDuration: 15n * 60n, // taxes every registered venue during this window
allocation: { // must sum to 10000
feeReceiver: 10000, // 100% to the fee receiver in this example
holders: 0,
burn: 0,
liquidity: 0,
},
feeReceiver: "0xYourRevenueWallet",
quoteToken: ethers.ZeroAddress, // launchpad fills WETH for native templates
minHolderShare: 0n, // required > 0 only when holders > 0
v3Factory: ethers.ZeroAddress, // DEX-derived fields are filled by the launchpad
v2PairInitCodeHash: ethers.ZeroHash,
v3PoolInitCodeHash: ethers.ZeroHash,
v3FeeTiers: [],
};
const params = {
configId: 0n, name, symbol, poolDetails: "",
maxContribution: 0n, salt, lockDuration: 0n,
taxConfig,
};
const tx = await launchpadRW.createMicroSaleTaxTokenV2(params, dexKey, {
value: creationFee,
});
Tax rules enforced at create time:
- Each of
buyTaxBpsandsellTaxBpsis at most 1000 (10%), and at least one must be above zero. taxDurationmust be above zero and at leastantiFarmerDuration; both are capped at 150 years.allocation.feeReceiver + holders + burn + liquiditymust equal 10000.- The fee receiver only earns if
allocation.feeReceiver > 0, so the create reverts if you set a tax but leave that slice at zero (nothing would ever fund the receiver). - If
allocation.holders > 0, you must setminHolderShare > 0(the minimum balance a wallet needs to earn holder dividends). - Tax liquidation thresholds are token constants and are not create parameters.
The allocation splits let one token pay a wallet, reward holders as dividends, burn a slice, and
top up liquidity, all from the same tax. For the plain-wallet path most people use
feeReceiver: 10000. The holder/burn/liquidity slices are covered in vaults.md.
Tax token with a vault
The fee receiver is a vault deployed for you by a factory you name. Set taxConfig.feeReceiver to
the zero address; the launchpad binds the deployed vault. Because the extra arguments and the
factory handshake need their own explanation, the full walkthrough lives in
vaults.md. The signature is:
function createMicroSaleTaxTokenWithVaultV2(
CreateParamsV2 calldata params,
address vaultFactory,
bytes calldata vaultData,
address dexKey
) external payable returns (address fundManager);
Generate a separate salt for the vault launch and set the fee receiver to the zero address:
const { salt, address: predictedToken } = await generateTokenSalt(
"CheeseTaxTokenV2",
params.name,
params.symbol,
dexKey,
);
const vaultParams = {
...params,
salt,
taxConfig: { ...params.taxConfig, feeReceiver: ethers.ZeroAddress },
};
await launchpadRW.createMicroSaleTaxTokenWithVaultV2(
vaultParams,
VAULT_FACTORY,
vaultData,
dexKey,
{ value: creationFee },
);
Reading back what you created
Every create emits MicroSaleCreated(index, fundManager, owner, token, configId, timestamp). Pull
fundManager and token from it, or look the sale up afterward:
const sale = await launchpad.getMicroSaleByFundManager(fundManager);
// or by the token address, or by index
const sameByToken = await launchpad.getMicroSaleByToken(token);
Contributing
Which function to call depends on the sale's raise currency. Read sale.currency: address(0)
means a native raise, anything else is an ERC20 raise.
Native (BNB) raises
await launchpadRW.contributeMicroSale(fundManager, {
value: ethers.parseEther("1"), // 1 BNB
});
ERC20 quote raises
Approve the quote token first, then contribute an explicit amount. No msg.value.
const quote = new ethers.Contract(sale.currency, ERC20_ABI, signer);
await quote.approve(LAUNCHPAD, amount);
await launchpadRW.contributeMicroSaleQuote(fundManager, amount);
Fee-on-transfer quote tokens are rejected: the launchpad checks that the amount it received exactly
matches amount, and reverts Non-standard quote transfer otherwise.
What the launchpad checks on every contribution
- The sale is
Active. - Now is within
[startTime, endTime). totalRaised + amount <= hardCap(you cannot overshoot the cap).- If
maxContribution > 0, your cumulative contribution stays within it. - The purchase rounds to more than zero tokens (
amount * rate / 1e18 > 0), so dust reverts withContribution too small.
Because contributions cannot exceed the hard cap, the amount that fills the last slot is exactly
hardCap - totalRaised. Quote that to your users so their transaction does not revert.
const sale = await launchpad.getMicroSaleByFundManager(fundManager);
const remaining = sale.hardCap - sale.totalRaised;
let maxForUser = remaining;
if (sale.maxContribution > 0n) {
const me = await launchpad.getMicroSaleContribution(fundManager, myAddress);
const headroom = sale.maxContribution - me.contribution;
maxForUser = headroom < remaining ? headroom : remaining;
}
Finalizing
When the hard cap is reached, the sale owner finalizes it (a platform operator can also do it, which
is how the UI offers to finalize on the owner's behalf). Finalizing takes the 5% platform fee, adds
the rest of the raise plus LP tokens to the DEX, and then locks the LP (if lockDuration > 0) or
burns it.
const sale = await launchpad.getMicroSaleByFundManager(fundManager);
if (sale.totalRaised === sale.hardCap && sale.state === 0n) {
await launchpadRW.finalizeMicroSale(fundManager); // sale owner
}
After finalize, sale.state is 1 (Finalized), sale.listedAt is the listing timestamp, and the
DEX pair is live. This is the moment a token starts trading, so it is the event to watch if you
track listings (MicroSaleFinalized).
Claiming
Once a sale is finalized, each contributor claims their purchased tokens. Claiming is one-shot per address.
await launchpadRW.claimMicroSale(fundManager); // sends msg.sender their tokens
const me = await launchpad.getMicroSaleContribution(fundManager, myAddress);
console.log("claimed:", me.claimed); // equals `purchased` after a successful claim
Claim reverts if the sale is not finalized (MicroSale not finalized), if you bought nothing
(Nothing to claim), or if you already claimed (Already claimed).
Refunds and exits
There are two ways to get money back, depending on the situation.
Full refund: failed or cancelled sale
withdrawMicroSale returns 100% of a contribution when either:
- the sale is still
Activebut itsendTimehas passed and the hard cap was never reached (a failed sale), or - the sale was
Cancelledby its owner.
await launchpadRW.withdrawMicroSale(fundManager); // refunds msg.sender in full
This is the normal path for a sale that did not fill. There is no fee.
Early exit while live: emergency withdraw
emergencyWithdrawMicroSale lets a contributor leave a sale that is still open and not yet filled,
for a 10% penalty (the penalty goes to the platform). It is blocked in the last 15 minutes before
endTime.
await launchpadRW.emergencyWithdrawMicroSale(fundManager); // returns 90% to msg.sender
Conditions: the sale is Active, the hard cap has not been reached, the sale has not ended, and
there are at least 15 minutes left. Unlike a full withdraw, this one reduces the sale's
totalRaised, freeing that room for others.
Cancelling a sale
The owner can cancel an Active sale, which flips it to Cancelled and opens full withdrawals for
every contributor.
await launchpadRW.cancelMicroSale(fundManager); // sale owner
Editing a sale
Two fields stay editable after creation by the sale owner (or a platform operator):
| Function | Updates |
|---|---|
updateMicroSaleDetails(fundManager, poolDetails) | free-form metadata; the owner can only call this while the sale is Active, an operator can call it any time |
setMicroSaleMaxContribution(fundManager, maxContribution) | the per-address cap; 0 means unlimited |
Both emit MicroSaleUpdated(index, fundManager, timestamp).
Reading sale state
Everything you need for a dashboard is a view call. All are open to anyone.
Look up a sale
| Function | Use |
|---|---|
getMicroSale(index) | by creation index |
getMicroSaleByFundManager(fundManager) | by the sale handle |
getMicroSaleByToken(token) | by token address (1:1 with the sale) |
getMicroSaleByVault(vault) | by attached vault, for vault launches |
getMicroSalesCount() | total sales ever created |
isQuoteTokenAllowed(token) | whether an ERC20 is accepted as a vault-launch quote token |
getAllowedQuoteTokens() | every ERC20 the platform accepts as a quote token |
The returned MicroSale struct has the full state: state, totalRaised, hardCap, startTime,
endTime, filledAt, listedAt, rate, maxContribution, token, currency, vault, and
more. See events-and-data.md for every field.
Look up contributors
// One user's ledger
const info = await launchpad.getMicroSaleContribution(fundManager, user);
// { contribution, purchased, claimed, excessiveRefunded }
// How many contributors
const n = await launchpad.getMicroSaleContributorCount(fundManager);
// A page of contributors (inclusive indexes; endIndex is clamped to the last one)
const page = await launchpad.getMicroSaleContributionInfos(fundManager, 0, 49);
// [{ user, contribution, purchased, claimed }, ...]
A small progress helper
async function progress(fundManager: string) {
const s = await launchpad.getMicroSaleByFundManager(fundManager);
const pct = (Number(s.totalRaised) / Number(s.hardCap)) * 100;
return {
state: ["Active", "Finalized", "Cancelled"][Number(s.state)],
raised: ethers.formatEther(s.totalRaised),
hardCap: ethers.formatEther(s.hardCap),
percent: pct.toFixed(2),
endsIn: Number(s.endTime) - Math.floor(Date.now() / 1000),
};
}
Vanity token addresses
Every create path deploys the token with CREATE2. Use the
generate-salt API helper before each launch; it searches against the
current on-chain token template and returns both a unique salt and the predicted token address. The
returned address ends in 6666. Pass the returned salt unchanged to the create function and use
the returned address for a pre-transaction preview or post-transaction verification.
Error reference
Revert strings you are most likely to hit, grouped by call.
Create
| Message | Cause |
|---|---|
Config not found / Config not active | bad or deactivated configId |
Invalid lock duration | lockDuration not 0 and outside the allowed window |
Invalid total supply | template's presale + LP tokens do not sum to 1e9 |
Insufficient creation fee | msg.value below the platform creation fee |
Buy tax exceeds max / Sell tax exceeds max | a tax side above 1000 BPS |
Tax must be > 0 | both tax sides zero |
Tax duration required / Tax duration too short | duration is zero or shorter than antiFarmerDuration |
Invalid allocation | tax allocation slices do not sum to 10000 |
Fee receiver required | wallet-receiver path with a zero feeReceiver |
Config not bound to this quote | taxConfig.quoteToken does not match the template binding |
Dex not enabled | dexKey is not an enabled DEX entry |
Contribute
| Message | Cause |
|---|---|
Invalid fund manager | the address is not a microsale fund manager |
Quote sale: use contributeMicroSaleQuote | native call on an ERC20 raise |
Native sale: use contributeMicroSale | ERC20 call on a native raise |
MicroSale not active | sale finalized or cancelled |
Contribution outside of start and end time | before start or after end |
Buying amount exceeds hard cap | would push totalRaised over the cap |
Contribute more than allowed | exceeds this sale's maxContribution |
Contribution too small | rounds to zero tokens |
Non-standard quote transfer | fee-on-transfer quote token |
Finalize / claim / withdraw
| Message | Cause |
|---|---|
Unauthorized | not the sale owner or a platform operator |
Hard cap not reached | finalize before the cap is exactly filled |
MicroSale not finalized | claim before finalize |
Nothing to claim / Already claimed | claim with no allocation, or twice |
Hard cap reached | full withdraw attempted on a sale that actually filled |
Pool is still in progress | full withdraw before the sale ended |
Hard cap already reached | emergency withdraw after the cap filled |
Too close to end time | emergency withdraw inside the last 15 minutes |
No contribution | withdraw with nothing contributed |