Launch Types
Presale
Last updated January 31, 2026
Presales offer fixed-price token distribution on Solana — set your SPL token price upfront based on allocation and SOL cap. Users know exactly what they're getting, and you know exactly what you'll raise. In Genesis, a "presale" means tokens are sold immediately before initial trading — buyers receive the tokens directly, not a future right to receive them.
What You'll Learn
This guide covers:
- How Presale pricing works (allocation + cap = price)
- Setting up deposit windows and claim periods
- Configuring deposit limits and cooldowns
- User operations: wrap SOL, deposit, and claim
Summary
Presales sell tokens at a predetermined price. The price is calculated from the token allocation and SOL cap you configure, making it ideal for token launches with a known valuation.
- Fixed price = SOL cap / token allocation
- Users deposit SOL during the deposit window (2% fee applies)
- First-come-first-served up to the SOL cap
- Optional: minimum/maximum deposit limits and cooldowns.
Out of Scope
Organic price discovery (see Launch Pool), bid-based auctions (see Uniform Price Auction), and vesting schedules.
How It Works
- You allocate tokens to the Presale with a SOL cap that determines the fixed price
- Users deposit SOL during the deposit window at the fixed rate
- After the deposit period ends, you execute the transition to move funds
- Users claim their tokens based on their deposit amount
Price Calculation
The token price is determined by the ratio of allocated tokens to the SOL cap:
price = allocationQuoteTokenCap / baseTokenAllocation
tokens = deposit / price
For example, if you allocate 1,000,000 tokens with a 100 SOL cap:
- Price = 100 SOL / 1,000,000 tokens = 0.0001 SOL per token
- A 10 SOL deposit receives 100,000 tokens
Fees
| Instruction | Solana |
|---|---|
| Deposit | 2% |
| Graduation | 5% |
Quick Start
Setup Guide
Prerequisites
npm install @metaplex-foundation/genesis @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-toolbox
1. Initialize the Genesis Account
The Genesis Account creates your token and coordinates all distribution buckets.
1import {
2 findGenesisAccountV2Pda,
3 genesis,
4 initializeV2,
5} from '@metaplex-foundation/genesis'
6import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
7import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
9
10const umi = createUmi('https://api.mainnet-beta.solana.com')
11 .use(mplToolbox())
12 .use(genesis())
13
14// umi.use(keypairIdentity(yourKeypair));
15
16const baseMint = generateSigner(umi)
17const TOTAL_SUPPLY = 1_000_000_000_000_000n // 1 million tokens (9 decimals)
18
19// Store this account address for later or recreate it when needed.
20const [genesisAccount] = findGenesisAccountV2Pda(umi, {
21 baseMint: baseMint.publicKey,
22 genesisIndex: 0,
23})
24
25await initializeV2(umi, {
26 baseMint,
27 fundingMode: 0,
28 totalSupplyBaseToken: TOTAL_SUPPLY,
29 name: 'My Token',
30 symbol: 'MTK',
31 uri: 'https://example.com/metadata.json',
32}).sendAndConfirm(umi)
The totalSupplyBaseToken should equal the sum of all bucket allocations.
2. Add the Presale Bucket
The Presale bucket collects deposits and distributes tokens. Configure timing and optional limits here.
1import {
2 genesis,
3 addPresaleBucketV2,
4 findPresaleBucketV2Pda,
5 findUnlockedBucketV2Pda,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.
18
19const [presaleBucket] = findPresaleBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
20const [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
21
22const now = BigInt(Math.floor(Date.now() / 1000))
23const depositStart = now
24const depositEnd = now + 86400n // 24 hours
25const claimStart = depositEnd + 1n
26const claimEnd = claimStart + 604800n // 1 week
27
28await addPresaleBucketV2(umi, {
29 genesisAccount,
30 baseMint: baseMint.publicKey,
31 baseTokenAllocation: TOTAL_SUPPLY,
32 allocationQuoteTokenCap: 100_000_000_000n, // 100 SOL cap (sets price)
33
34 // Timing
35 depositStartCondition: {
36 __kind: 'TimeAbsolute',
37 padding: Array(47).fill(0),
38 time: depositStart,
39 triggeredTimestamp: null,
40 },
41 depositEndCondition: {
42 __kind: 'TimeAbsolute',
43 padding: Array(47).fill(0),
44 time: depositEnd,
45 triggeredTimestamp: null,
46 },
47 claimStartCondition: {
48 __kind: 'TimeAbsolute',
49 padding: Array(47).fill(0),
50 time: claimStart,
51 triggeredTimestamp: null,
52 },
53 claimEndCondition: {
54 __kind: 'TimeAbsolute',
55 padding: Array(47).fill(0),
56 time: claimEnd,
57 triggeredTimestamp: null,
58 },
59
60 // Optional: Deposit limits
61 minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }
62 depositLimit: null, // or { limit: sol(10).basisPoints }
63
64 // Where collected SOL goes after transition
65 endBehaviors: [
66 {
67 __kind: 'SendQuoteTokenPercentage',
68 padding: Array(4).fill(0),
69 destinationBucket: publicKey(unlockedBucket),
70 percentageBps: 10000, // 100%
71 processed: false,
72 },
73 ],
74}).sendAndConfirm(umi)
3. Add the Unlocked Bucket
The Unlocked bucket receives SOL from the Presale after the transition.
1import {
2 addUnlockedBucketV2,
3 genesis,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, baseMint, claimStart, and claimEnd from previous steps.
16
17await addUnlockedBucketV2(umi, {
18 genesisAccount,
19 baseMint: baseMint.publicKey,
20 baseTokenAllocation: 0n,
21 recipient: umi.identity.publicKey,
22 claimStartCondition: {
23 __kind: 'TimeAbsolute',
24 padding: Array(47).fill(0),
25 time: claimStart,
26 triggeredTimestamp: null,
27 },
28 claimEndCondition: {
29 __kind: 'TimeAbsolute',
30 padding: Array(47).fill(0),
31 time: claimEnd,
32 triggeredTimestamp: null,
33 },
34 backendSigner: null,
35}).sendAndConfirm(umi)
4. Finalize
Once all buckets are configured, finalize to activate the presale. This is irreversible.
1import {
2 genesis,
3 finalizeV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount and baseMint from the Initialize step.
16
17await finalizeV2(umi, {
18 baseMint: baseMint.publicKey,
19 genesisAccount,
20}).sendAndConfirm(umi)
User Operations
Wrapping SOL
Users must wrap SOL to wSOL before depositing.
1import {
2 findAssociatedTokenPda,
3 createTokenIfMissing,
4 transferSol,
5 syncNative,
6 mplToolbox,
7} from '@metaplex-foundation/mpl-toolbox'
8import { WRAPPED_SOL_MINT, genesis } from '@metaplex-foundation/genesis'
9import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
10import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
11
12const umi = createUmi('https://api.mainnet-beta.solana.com')
13 .use(mplToolbox())
14 .use(genesis())
15
16// umi.use(keypairIdentity(yourKeypair));
17
18const userWsolAccount = findAssociatedTokenPda(umi, {
19 owner: umi.identity.publicKey,
20 mint: WRAPPED_SOL_MINT,
21})
22
23await createTokenIfMissing(umi, {
24 mint: WRAPPED_SOL_MINT,
25 owner: umi.identity.publicKey,
26 token: userWsolAccount,
27})
28 .add(
29 transferSol(umi, {
30 destination: publicKey(userWsolAccount),
31 amount: sol(10),
32 })
33 )
34 .add(syncNative(umi, { account: userWsolAccount }))
35 .sendAndConfirm(umi)
Depositing
1import {
2 genesis,
3 depositPresaleV2,
4 findPresaleDepositV2Pda,
5 fetchPresaleDepositV2,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
18
19await depositPresaleV2(umi, {
20 genesisAccount,
21 bucket: presaleBucket,
22 baseMint: baseMint.publicKey,
23 amountQuoteToken: sol(1).basisPoints,
24}).sendAndConfirm(umi)
25
26// Verify
27const [depositPda] = findPresaleDepositV2Pda(umi, {
28 bucket: presaleBucket,
29 recipient: umi.identity.publicKey,
30})
31const deposit = await fetchPresaleDepositV2(umi, depositPda)
32
33console.log('Deposited (after fee):', deposit.amountQuoteToken)
Multiple deposits from the same user accumulate into a single deposit account.
Claiming Tokens
After the deposit period ends and claims open:
1import {
2 genesis,
3 claimPresaleV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
16
17await claimPresaleV2(umi, {
18 genesisAccount,
19 bucket: presaleBucket,
20 baseMint: baseMint.publicKey,
21 recipient: umi.identity.publicKey,
22}).sendAndConfirm(umi)
Token allocation: userTokens = (userDeposit / allocationQuoteTokenCap) * baseTokenAllocation
Admin Operations
Executing the Transition
After deposits close, execute the transition to move collected SOL to the unlocked bucket.
1import {
2 genesis,
3 transitionV2,
4 WRAPPED_SOL_MINT,
5} from '@metaplex-foundation/genesis'
6import {
7 findAssociatedTokenPda,
8 mplToolbox,
9} from '@metaplex-foundation/mpl-toolbox'
10import { keypairIdentity } from '@metaplex-foundation/umi'
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
12
13const umi = createUmi('https://api.mainnet-beta.solana.com')
14 .use(mplToolbox())
15 .use(genesis())
16
17// umi.use(keypairIdentity(yourKeypair));
18
19// Assumes genesisAccount, presaleBucket, unlockedBucket, and baseMint from previous steps.
20
21const [unlockedBucketQuoteTokenAccount] = findAssociatedTokenPda(umi, {
22 owner: unlockedBucket,
23 mint: WRAPPED_SOL_MINT,
24})
25
26await transitionV2(umi, {
27 genesisAccount,
28 primaryBucket: presaleBucket,
29 baseMint: baseMint.publicKey,
30})
31 .addRemainingAccounts([
32 { pubkey: unlockedBucket, isSigner: false, isWritable: true },
33 { pubkey: unlockedBucketQuoteTokenAccount, isSigner: false, isWritable: true },
34 ])
35 .sendAndConfirm(umi)
Why this matters: Without transition, collected SOL stays locked in the Presale bucket. Users can still claim tokens, but the team cannot access the raised funds.
Reference
Configuration Options
These options are set when creating the Presale bucket:
| Option | Description | Example |
|---|---|---|
minimumDepositAmount | Minimum deposit per transaction | { amount: sol(0.1).basisPoints } |
depositLimit | Maximum total deposit per user | { limit: sol(10).basisPoints } |
depositCooldown | Time between deposits | { seconds: 60n } |
perCooldownDepositLimit | Max deposit per cooldown period | { amount: sol(1).basisPoints } |
Time Conditions
Four conditions control presale timing:
| Condition | Purpose |
|---|---|
depositStartCondition | When deposits open |
depositEndCondition | When deposits close |
claimStartCondition | When claims open |
claimEndCondition | When claims close |
Use TimeAbsolute with a Unix timestamp:
const condition = {
__kind: 'TimeAbsolute',
padding: Array(47).fill(0),
time: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now
triggeredTimestamp: null,
};
End Behaviors
Define what happens to collected SOL after the deposit period:
endBehaviors: [
{
__kind: 'SendQuoteTokenPercentage',
padding: Array(4).fill(0),
destinationBucket: publicKey(unlockedBucket),
percentageBps: 10000, // 100% = 10000 basis points
processed: false,
},
]
Fetching State
Bucket state:
import { fetchPresaleBucketV2 } from '@metaplex-foundation/genesis';
const bucket = await fetchPresaleBucketV2(umi, presaleBucket);
console.log('Total deposits:', bucket.quoteTokenDepositTotal);
console.log('Deposit count:', bucket.depositCount);
console.log('Token allocation:', bucket.bucket.baseTokenAllocation);
console.log('SOL cap:', bucket.allocationQuoteTokenCap);
Deposit state:
import { fetchPresaleDepositV2, safeFetchPresaleDepositV2 } from '@metaplex-foundation/genesis';
const deposit = await fetchPresaleDepositV2(umi, depositPda); // throws if not found
const maybeDeposit = await safeFetchPresaleDepositV2(umi, depositPda); // returns null
if (deposit) {
console.log('Amount deposited:', deposit.amountQuoteToken);
console.log('Amount claimed:', deposit.amountClaimed);
console.log('Fully claimed:', deposit.claimed);
}
Notes
- The 2% protocol fee applies to deposits
- Users must wrap SOL to wSOL before depositing
- Multiple deposits from the same user accumulate in one deposit account
- The transition must be executed after deposits close for the team to access funds
- Finalization is permanent—double-check all configuration before calling
finalizeV2
FAQ
How is the token price calculated in a Presale?
Price equals SOL cap divided by token allocation. For 1,000,000 tokens with a 100 SOL cap, the price is 0.0001 SOL per token.
What happens if the SOL cap isn't reached?
Users still receive tokens proportional to their deposits. If only 50 SOL is deposited against a 100 SOL cap, depositors receive 50% of allocated tokens.
Can I set deposit limits per user?
Yes. Use minimumDepositAmount for minimum per-transaction limits and depositLimit for maximum total deposit per user.
What's the difference between Presale and Launch Pool?
Presale has a fixed price determined by token allocation and SOL cap. Launch Pool discovers price organically based on total deposits.
When should I use Presale vs Launch Pool?
Use Presale when you want predictable pricing and know exactly how much you want to raise. Use Launch Pool for organic price discovery.
Glossary
| Term | Definition |
|---|---|
| Presale | Fixed-price token sale with predetermined rate |
| SOL Cap | Maximum SOL the presale will accept (determines price) |
| Token Allocation | Number of tokens available in the presale |
| Deposit Limit | Maximum total deposit allowed per user |
| Minimum Deposit | Minimum amount required per deposit transaction |
| Cooldown | Time users must wait between deposits |
| End Behavior | Automated action after deposit period ends |
| Transition | Instruction that processes end behaviors |
Next Steps
- Launch Pool - Fair launch with organic price discovery
- Uniform Price Auction - Auction-style bid-based allocation
- Launch a Token - End-to-end token launch guide
- Getting Started - Genesis launchpad fundamentals
