Skip to main content

Microsales

Contract address

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.

NetworkChain IDLaunchpad
BNB Chain (mainnet)560xD9CcE715619fA9082d21375B6B37A3a0d99138F4
BNB Chain (testnet)970x0AA21d47b600B04527c09BbD9A30aF32cb64f6cF
Robinhood Chain46630x7E0752B42f9db92F12E221E10fFfa10bfE8668C6

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​

  • 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 fundManager address.
  • 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:

StateValueMeaning
Active0Open for contributions
Finalized1Hard cap hit, liquidity added, tokens claimable
Cancelled2Called 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:

FunctionReturns
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.

FunctionTokenTaxRaise currencyFee receiver
createMicroSaleNoTaxTokenCheeseTokennonetemplate-bound native or ERC20n/a
createMicroSaleTaxTokenV2CheeseTaxTokenV2buy/sell taxtemplate-bound native or ERC20a wallet you name
createMicroSaleTaxTokenWithVaultV2CheeseTaxTokenV2buy/sell taxtemplate-bound native or ERC20a 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):

FieldTypeNotes
configIduint256the template to launch against; must be active
namestringtoken name
symbolstringtoken symbol
poolDetailsstringfree-form metadata (JSON, a URL, whatever your UI stores)
maxContributionuint256per-address cap on cumulative contribution; 0 means unlimited
saltbytes32fresh CREATE2 salt returned by the CheesePad API
lockDurationuint256LP 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 buyTaxBps and sellTaxBps is at most 1000 (10%), and at least one must be above zero.
  • taxDuration must be above zero and at least antiFarmerDuration; both are capped at 150 years.
  • allocation.feeReceiver + holders + burn + liquidity must 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 set minHolderShare > 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 with Contribution 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 Active but its endTime has passed and the hard cap was never reached (a failed sale), or
  • the sale was Cancelled by 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):

FunctionUpdates
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​

FunctionUse
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

MessageCause
Config not found / Config not activebad or deactivated configId
Invalid lock durationlockDuration not 0 and outside the allowed window
Invalid total supplytemplate's presale + LP tokens do not sum to 1e9
Insufficient creation feemsg.value below the platform creation fee
Buy tax exceeds max / Sell tax exceeds maxa tax side above 1000 BPS
Tax must be > 0both tax sides zero
Tax duration required / Tax duration too shortduration is zero or shorter than antiFarmerDuration
Invalid allocationtax allocation slices do not sum to 10000
Fee receiver requiredwallet-receiver path with a zero feeReceiver
Config not bound to this quotetaxConfig.quoteToken does not match the template binding
Dex not enableddexKey is not an enabled DEX entry

Contribute

MessageCause
Invalid fund managerthe address is not a microsale fund manager
Quote sale: use contributeMicroSaleQuotenative call on an ERC20 raise
Native sale: use contributeMicroSaleERC20 call on a native raise
MicroSale not activesale finalized or cancelled
Contribution outside of start and end timebefore start or after end
Buying amount exceeds hard capwould push totalRaised over the cap
Contribute more than allowedexceeds this sale's maxContribution
Contribution too smallrounds to zero tokens
Non-standard quote transferfee-on-transfer quote token

Finalize / claim / withdraw

MessageCause
Unauthorizednot the sale owner or a platform operator
Hard cap not reachedfinalize before the cap is exactly filled
MicroSale not finalizedclaim before finalize
Nothing to claim / Already claimedclaim with no allocation, or twice
Hard cap reachedfull withdraw attempted on a sale that actually filled
Pool is still in progressfull withdraw before the sale ended
Hard cap already reachedemergency withdraw after the cap filled
Too close to end timeemergency withdraw inside the last 15 minutes
No contributionwithdraw with nothing contributed