Launch Types

Launch Pool

Last updated September 18, 2026

Launch Pools provide organic price discovery for fair token launches on Solana. Users deposit SOL during a window and receive SPL tokens proportional to their share of total deposits. No sniping, no front-running, fair distribution for everyone.

What You'll Learn

This guide covers:

  • How Launch Pool pricing and distribution works
  • Setting up deposit and claim windows
  • Configuring end behaviors for fund collection
  • User operations: Deposit, withdraw, and claim

Summary

Launch Pools are a crowdsale-style token launch mechanism that accepts deposits during a defined window, then distributes tokens proportionally. The final token price is determined by total deposits divided by token allocation — enabling transparent, on-chain price discovery for your token generation event (TGE).

  • Users deposit SOL during the deposit window (0% fee applies)
  • Withdrawals allowed during deposit period (0% fee)
  • Token distribution is proportional to deposit share
  • An optional soft cap bounds how much the launch keeps, refunding the excess pro-rata
  • End behaviors route collected SOL to treasury buckets

Launch Pools discover price from deposits. For a fixed price set upfront, use Presale; for bid-based clearing, use Uniform Price Auction. Liquidity pool creation is handled by the Raydium graduation buckets, not by the Launch Pool bucket itself.

Quick Start

How It Works

  1. A specific quantity of tokens is allocated to the Launch Pool bucket
  2. Users deposit SOL during the deposit window (withdrawals allowed with fee)
  3. When the window closes, tokens distribute proportionally based on deposit share

Price Discovery

The token price emerges from total deposits:

tokenPrice = totalDeposits / tokenAllocation
userTokens = (userDeposit / totalDeposits) * tokenAllocation

Example: 1,000,000 tokens allocated, 100 SOL total deposits = 0.0001 SOL per token

Price discovery is unbounded by default — the more the pool is subscribed, the higher the implied price. Set a soft cap to put a ceiling on it.

Lifecycle

  1. Deposit Period - Users deposit SOL during a defined window
  2. triggerBehaviorsV2 - End behaviors execute (e.g., send collected SOL to another bucket)
  3. Claim Period - Users claim tokens proportional to their deposit weight
  4. Refund Period (conditional) - If a soft cap was exceeded or a minimum quote token threshold was missed, depositors call refundLaunchPoolV2

Launch Pool Soft Cap

A soft cap is a ceiling on the quote tokens a Launch Pool keeps, configured with the softCap extension on addLaunchPoolBucketV2. Deposits above the cap are still accepted during the deposit window, the launch still succeeds, and the excess quote tokens are refunded pro-rata once the window closes.

Without a soft cap, price discovery is unbounded: every deposit is kept and the implied token price rises with subscription. A soft cap fixes the maximum the launch raises, so the effective price is softCap / baseTokenAllocation no matter how far deposits overshoot.

PropertyBehaviour when a soft cap is configured
Deposits above the capAccepted during the deposit window — deposits are never rejected at the cap
Launch outcomeSucceeds — a soft cap is a ceiling, never a failure condition
Base token distributionFull baseTokenAllocation distributed pro-rata across all deposits
Excess quote tokensRefunded pro-rata via refundLaunchPoolV2 after the deposit window ends
Proceeds seen by end behaviorsClamped to the soft cap, so SendQuoteTokenPercentage forwards at most softCap
Raydium graduation start priceDerived from the capped proceeds, not the raw deposit total

A soft cap is a ceiling on capital raised, not a floor. The floor is a separate extension, minimumQuoteTokenThreshold — see Soft Cap and Minimum Quote Token Threshold Together.

Configuring a Soft Cap on a Launch Pool Bucket

Pass a softCap value to addLaunchPoolBucketV2 when you add the bucket. The amount is denominated in quote token quantum units (lamports for wSOL).

Add a Launch Pool bucket with a 100 SOL soft cap
import { sol } from '@metaplex-foundation/umi';
await addLaunchPoolBucketV2(umi, {
genesisAccount,
baseMint: baseMint.publicKey,
baseTokenAllocation: TOTAL_SUPPLY,
// ...time conditions and end behaviors...
// Floor: the launch fails below this and everyone can take a full refund.
minimumQuoteTokenThreshold: { amount: sol(10).basisPoints },
// Ceiling: the launch keeps at most this much; the rest is refunded pro-rata.
softCap: { amount: sol(100).basisPoints },
}).sendAndConfirm(umi);

softCap is a required argument on addLaunchPoolBucketV2 in @metaplex-foundation/genesis 0.42.0. Unlike the other Launch Pool extensions it has no default, so pass softCap: null explicitly when you do not want a cap.

Soft caps are validated when the extension is set and again at finalizeV2:

RuleError if violated
softCap.amount must be greater than zeroInvalidSoftCap (221)
softCap.amount must be greater than or equal to minimumQuoteTokenThreshold.amountSoftCapBelowThreshold (222)
Extensions can only be added or removed before finalizeV2Account is rejected as already finalized

A soft cap can also be set or cleared on an existing bucket with addLaunchPoolBucketV2Extensions and removeLaunchPoolBucketV2Extensions using the SoftCap member of LaunchPoolV2ExtensionType — but only while the Genesis Account is unfinalized.

Oversubscription and Pro-Rata Refund Math

A Launch Pool is oversubscribed when quoteTokenDepositTotal is strictly greater than softCap.amount. Each deposit is then split into a filled portion that counts toward the cap and an excess portion that is refundable:

Per-depositor split in an oversubscribed Launch Pool
filled_i = ceil(deposit_i * softCap / totalDeposits)
excess_i = deposit_i - filled_i
tokens_i = (weighted_i / weightedQuoteTokenTotal) * baseTokenAllocation

filled rounds up so that the sum of all filled portions is always at least the soft cap. This keeps the bucket solvent against the capped graduation transfer regardless of whether refunds or graduation are cranked first; the rounding leaves at most a few quantum units of dust in the bucket.

Token allocation is unaffected by the cap. Refunding excess does not remove a depositor's weighted contribution, so everyone still receives tokens proportional to their full deposit.

Worked example — 1,000,000 tokens allocated, a 100 SOL soft cap, and 150 SOL deposited:

DepositorDepositedFilled (kept)RefundedTokens received
Alice50 SOL~33.33 SOL~16.67 SOL333,333 (1/3)
Bob100 SOL~66.67 SOL~33.33 SOL666,667 (2/3)
Total150 SOL100 SOL50 SOL1,000,000

The effective price is 0.0001 SOL per token (100 SOL / 1,000,000), not the 0.00015 SOL per token the uncapped deposit total would have implied. On-chain values are computed in lamports with filled rounded up, so real figures differ from the rounded SOL amounts above by a few lamports.

Refunding Excess Deposits with refundLaunchPoolV2

refundLaunchPoolV2 returns a depositor's excess quote tokens after an oversubscribed deposit window closes. It takes no amount argument — the program computes the refundable amount from the deposit, the soft cap, and the bucket's deposit total.

refundLaunchPool.ts
1import {
2 genesis,
3 refundLaunchPoolV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi('https://api.mainnet-beta.solana.com')
9 .use(mplToolbox())
10 .use(genesis())
11
12// umi.use(keypairIdentity(yourKeypair));
13
14// Assumes genesisAccount, launchPoolBucket, and baseMint from previous steps.
15// Only valid after the deposit window closed AND either the
16// minimumQuoteTokenThreshold was missed or the softCap was exceeded.
17
18// The program computes the refundable amount, so there is no amount argument.
19// A missed threshold refunds the full deposit; an exceeded soft cap refunds
20// only the excess, leaving the depositor's token allocation intact.
21await refundLaunchPoolV2(umi, {
22 genesisAccount,
23 bucket: launchPoolBucket,
24 baseMint: baseMint.publicKey,
25 recipient: umi.identity.publicKey,
26}).sendAndConfirm(umi)

Key properties of the refund path:

  • Cranking is permissionless. Only payer must sign. If the depositor also signs, their empty base token account is closed for them.
  • No fee and no penalty are applied to a refund; deposit and withdraw penalty schedules do not affect it.
  • Claim order does not matter. An excess refund is allowed before or after claimLaunchPoolV2; both orderings converge on the same final state.
  • One refund per deposit. A second call returns DepositAlreadyRefunded.
  • Refunds are gated on the deposit window ending. Calling earlier returns LaunchPoolNotEnded.
  • Refunds require a failed floor or an exceeded cap. If neither applies, the call returns LaunchPoolThresholdMet.

Soft Cap and Minimum Quote Token Threshold Together

softCap and minimumQuoteTokenThreshold are independent extensions that bound a Launch Pool from opposite directions, and refundLaunchPoolV2 serves both. When the floor fails, that takes precedence and the refund is a full one.

ConfigurationDeposits below the floorDeposits between floor and capDeposits above the cap
Neither setLaunch succeeds, no refundsLaunch succeeds, no refundsLaunch succeeds, no refunds
Floor onlyLaunch fails, full refundsLaunch succeeds, no refundsLaunch succeeds, no refunds
Cap onlyLaunch succeeds, no refundsLaunch succeeds, no refundsLaunch succeeds, excess refunded pro-rata
Floor and capLaunch fails, full refundsLaunch succeeds, no refundsLaunch succeeds, excess refunded pro-rata

A full refund removes the depositor's weighted contribution from the bucket and cannot follow a claim — a failed floor means no claim was possible. An excess refund leaves the weighted contribution intact so the pro-rata claim formula stays correct.

Fees

InstructionSolana
User deposit fee0%
User withdraw fee0%
Creator withdraw fee5%*

* This fee only applies when creators withdraw liquidity

Each deposit increases your credited balance by the SOL left after the user deposit fee (0%) is withheld from the deposit.

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.

initializeV2.ts
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 Launch Pool Bucket

The Launch Pool bucket collects deposits and distributes tokens proportionally. Configure timing here.

addLaunchPoolBucket.ts
1import {
2 addLaunchPoolBucketV2,
3 findLaunchPoolBucketV2Pda,
4 findUnlockedBucketV2Pda,
5 genesis,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { publicKey } 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 [launchPoolBucket] = findLaunchPoolBucketV2Pda(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 addLaunchPoolBucketV2(umi, {
29 genesisAccount,
30 baseMint: baseMint.publicKey,
31 baseTokenAllocation: TOTAL_SUPPLY,
32
33 // Timing
34 depositStartCondition: {
35 __kind: 'TimeAbsolute',
36 padding: Array(47).fill(0),
37 time: depositStart,
38 triggeredTimestamp: null,
39 },
40 depositEndCondition: {
41 __kind: 'TimeAbsolute',
42 padding: Array(47).fill(0),
43 time: depositEnd,
44 triggeredTimestamp: null,
45 },
46 claimStartCondition: {
47 __kind: 'TimeAbsolute',
48 padding: Array(47).fill(0),
49 time: claimStart,
50 triggeredTimestamp: null,
51 },
52 claimEndCondition: {
53 __kind: 'TimeAbsolute',
54 padding: Array(47).fill(0),
55 time: claimEnd,
56 triggeredTimestamp: null,
57 },
58
59 // Optional: Minimum deposit
60 minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }
61
62 // Where collected SOL goes after transition
63 endBehaviors: [
64 {
65 __kind: 'SendQuoteTokenPercentage',
66 padding: Array(4).fill(0),
67 destinationBucket: publicKey(unlockedBucket),
68 percentageBps: 10000, // 100%
69 processed: false,
70 },
71 ],
72}).sendAndConfirm(umi)

3. Add the Unlocked Bucket

The Unlocked bucket receives SOL from the Launch Pool after triggerBehaviorsV2 executes.

addUnlockedBucket.ts
1import {
2 addUnlockedBucketV2,
3 genesis,
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, 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 launch. This is irreversible.

finalize.ts
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.

wrapSol.ts
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

depositLaunchPool.ts
1import {
2 genesis,
3 depositLaunchPoolV2,
4 findLaunchPoolDepositV2Pda,
5 fetchLaunchPoolDepositV2,
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, launchPoolBucket, and baseMint from previous steps.
18
19await depositLaunchPoolV2(umi, {
20 genesisAccount,
21 bucket: launchPoolBucket,
22 baseMint: baseMint.publicKey,
23 amountQuoteToken: sol(10).basisPoints,
24}).sendAndConfirm(umi)
25
26// Verify
27const [depositPda] = findLaunchPoolDepositV2Pda(umi, {
28 bucket: launchPoolBucket,
29 recipient: umi.identity.publicKey,
30})
31const deposit = await fetchLaunchPoolDepositV2(umi, depositPda)
32
33console.log('Deposited (after fee):', deposit.amountQuoteToken)

Multiple deposits from the same user accumulate into a single deposit account.

Withdrawing

Users can withdraw during the deposit period. A 0% fee applies.

withdrawLaunchPool.ts
1import {
2 genesis,
3 withdrawLaunchPoolV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity, sol } 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, launchPoolBucket, and baseMint from previous steps.
16
17await withdrawLaunchPoolV2(umi, {
18 genesisAccount,
19 bucket: launchPoolBucket,
20 baseMint: baseMint.publicKey,
21 amountQuoteToken: sol(3).basisPoints,
22}).sendAndConfirm(umi)

If a user withdraws their entire balance, the deposit PDA is closed.

Claiming Tokens

After the deposit period ends and claims open:

claimLaunchPool.ts
1import {
2 claimLaunchPoolV2,
3 genesis,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi('https://api.mainnet-beta.solana.com')
9 .use(mplToolbox())
10 .use(genesis())
11
12// umi.use(keypairIdentity(yourKeypair));
13
14// Assumes genesisAccount, launchPoolBucket, and baseMint from previous steps.
15
16await claimLaunchPoolV2(umi, {
17 genesisAccount,
18 bucket: launchPoolBucket,
19 baseMint: baseMint.publicKey,
20 recipient: umi.identity.publicKey,
21}).sendAndConfirm(umi)

Token allocation: userTokens = (userDeposit / totalDeposits) * bucketTokenAllocation

Refunding a Deposit

Refunds are available in two cases: the launch missed its minimumQuoteTokenThreshold (full refund), or it exceeded its softCap (excess-only refund). Both use the same instruction — see Refunding Excess Deposits with refundLaunchPoolV2.

Admin Operations

Executing triggerBehaviorsV2

After deposits close, run triggerBehaviorsV2 to move collected SOL to the unlocked bucket.

triggerBehaviors.ts
1import {
2 genesis,
3 triggerBehaviorsV2,
4 WRAPPED_SOL_MINT,
5} from '@metaplex-foundation/genesis'
6import {
7 findAssociatedTokenPda,
8 mplToolbox,
9} from '@metaplex-foundation/mpl-toolbox'
10import { publicKey } 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, launchPoolBucket, unlockedBucket, and baseMint from previous steps.
20
21const unlockedBucketQuoteTokenAccount = findAssociatedTokenPda(umi, {
22 owner: unlockedBucket,
23 mint: WRAPPED_SOL_MINT,
24})
25
26await triggerBehaviorsV2(umi, {
27 genesisAccount,
28 primaryBucket: launchPoolBucket,
29 baseMint,
30})
31 .addRemainingAccounts([
32 { pubkey: publicKey(unlockedBucket), isSigner: false, isWritable: true },
33 { pubkey: publicKey(unlockedBucketQuoteTokenAccount), isSigner: false, isWritable: true },
34 ])
35 .sendAndConfirm(umi)

Why this matters: Without running triggerBehaviorsV2, collected SOL stays locked in the Launch Pool bucket. Users can still claim tokens, but the team cannot access the raised funds.

Reference

Time Conditions

Four conditions control Launch Pool timing:

ConditionPurpose
depositStartConditionWhen deposits open
depositEndConditionWhen deposits close
claimStartConditionWhen claims open
claimEndConditionWhen 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,
},
]

You can split funds across multiple buckets:

endBehaviors: [
{
__kind: 'SendQuoteTokenPercentage',
padding: Array(4).fill(0),
destinationBucket: publicKey(treasuryBucket),
percentageBps: 2000, // 20%
processed: false,
},
{
__kind: 'SendQuoteTokenPercentage',
padding: Array(4).fill(0),
destinationBucket: publicKey(liquidityBucket),
percentageBps: 8000, // 80%
processed: false,
},
]

Launch Pool Extensions

Extensions are optional guards configured on the Launch Pool bucket. All are set through addLaunchPoolBucketV2, or added and removed individually with addLaunchPoolBucketV2Extensions and removeLaunchPoolBucketV2Extensions before finalizeV2.

ExtensionTypePurpose
softCap{ amount: bigint }Ceiling on quote tokens kept; excess refunded pro-rata
minimumQuoteTokenThreshold{ amount: bigint }Floor below which the launch fails and full refunds open
minimumDepositAmount{ amount: bigint }Minimum quote tokens per deposit
depositLimit{ limit: bigint }Maximum quote tokens per account
allowlistAllowlistGates deposits to an allowlisted set of wallets
claimScheduleClaimScheduleVests claimed base tokens over time
bonusScheduleLinearBpsScheduleV2Time-weighted deposit bonus
depositPenaltyLinearBpsScheduleV2Time-weighted deposit penalty
withdrawPenaltyLinearBpsScheduleV2Time-weighted withdrawal penalty
backendSignerBackendSignerRequires a backend co-signer on user actions

Common Errors

ErrorCodeCause
InvalidSoftCap221softCap.amount is zero — omit the extension instead of setting it to 0
SoftCapBelowThreshold222softCap.amount is below minimumQuoteTokenThreshold.amount
LaunchPoolNotEnded—refundLaunchPoolV2 called before the deposit window closed
LaunchPoolThresholdMet173Refund requested when the floor was met and the cap was not exceeded
DepositAlreadyRefunded—refundLaunchPoolV2 called twice for the same deposit
DepositAlreadyClaimed—Full refund requested after the depositor already claimed tokens

Fetching State

Bucket state:

import { fetchLaunchPoolBucketV2 } from '@metaplex-foundation/genesis';
const bucket = await fetchLaunchPoolBucketV2(umi, launchPoolBucket);
console.log('Total deposits:', bucket.quoteTokenDepositTotal);
console.log('Deposit count:', bucket.depositCount);
console.log('Claim count:', bucket.claimCount);
console.log('Token allocation:', bucket.bucket.baseTokenAllocation);
// Soft cap state (Option<SoftCap>)
console.log('Soft cap:', bucket.extensions.softCap);
console.log('Floor:', bucket.extensions.minimumQuoteTokenThreshold);

Deposit state:

import { fetchLaunchPoolDepositV2, safeFetchLaunchPoolDepositV2 } from '@metaplex-foundation/genesis';
const deposit = await fetchLaunchPoolDepositV2(umi, depositPda); // throws if not found
const maybeDeposit = await safeFetchLaunchPoolDepositV2(umi, depositPda); // returns null
if (deposit) {
console.log('Amount deposited:', deposit.amountQuoteToken);
console.log('Claimed:', deposit.claimed);
console.log('Refunded:', deposit.refunded);
}

Notes

  • User deposit and withdraw fees for Launch Pool are shown in Fees above.
  • Multiple deposits from the same user accumulate in one deposit account
  • If a user withdraws their entire balance, the deposit PDA closes
  • triggerBehaviorsV2 must be executed after deposits close for end behaviors to process
  • Users must have wSOL (wrapped SOL) to deposit
  • softCap is a required argument on addLaunchPoolBucketV2 in @metaplex-foundation/genesis 0.42.0 — pass softCap: null when no cap is wanted
  • Launch Pool extensions, including softCap, can only be added or removed before finalizeV2
  • Soft caps are supported by the Genesis program and JavaScript SDK; the mplx CLI does not expose a soft cap flag yet
  • An oversubscribed Launch Pool leaves a few quantum units of rounding dust in the bucket, because each depositor's filled portion rounds up
  • quoteTokenDepositTotal and depositCount are preserved as historical records after refunds; refundCount tracks refunds processed

FAQ

How is the token price determined in a Launch Pool?

The price is discovered organically based on total deposits. Final price equals total SOL deposited divided by tokens allocated. More deposits means higher implied price per token.

Can users withdraw their deposits?

Yes, users can withdraw during the deposit period. A 0% withdrawal fee applies to discourage gaming the system.

What happens if I deposit multiple times?

Multiple deposits from the same wallet accumulate into a single deposit account. Your total share is based on your combined deposits.

When can users claim their tokens?

After the deposit period ends and the claim window opens (defined by claimStartCondition). triggerBehaviorsV2 must be executed first to process end behaviors.

What's the difference between Launch Pool and Presale?

Launch Pool discovers price organically based on deposits with proportional distribution. Presale has a fixed price set upfront with first-come-first-served allocation up to the cap.

What is a Launch Pool soft cap?

A soft cap is a ceiling on the quote tokens a Launch Pool keeps, set with the softCap extension. Deposits above the cap are still accepted, the launch still succeeds, and the excess is refunded pro-rata after the deposit window closes.

What is the difference between a soft cap and a minimum quote token threshold?

A soft cap is a ceiling on capital raised and never causes a launch to fail. A minimumQuoteTokenThreshold is a floor — if total deposits fall below it, the launch fails and every depositor can take a full refund. They are separate extensions and can be used together.

Do depositors receive fewer tokens when a Launch Pool is oversubscribed?

No. The full base token allocation is still distributed pro-rata across all deposits. Oversubscription refunds excess quote tokens instead of cutting token allocations, so the effective price is capped at softCap / baseTokenAllocation.

Does a soft cap change the Raydium graduation start price?

Yes. When a Launch Pool is oversubscribed, the graduation start price is derived from the capped proceeds rather than the raw deposit total, so it matches the amount actually forwarded by SendQuoteTokenPercentage.

Glossary

TermDefinition
Launch PoolDeposit-based distribution where price is discovered at close
Deposit WindowTime period when users can deposit and withdraw SOL
Claim WindowTime period when users can claim their proportional tokens
End BehaviorAutomated action executed after deposit period ends
triggerBehaviorsV2Instruction that processes end behaviors and routes funds
Proportional DistributionToken allocation based on user's share of total deposits
Quote TokenThe token users deposit (usually wSOL)
Base TokenThe token being distributed
Soft CapCeiling on the quote tokens a Launch Pool keeps; excess is refunded pro-rata
Minimum Quote Token ThresholdFloor below which a Launch Pool fails and full refunds open
OversubscriptionState where total deposits exceed the configured soft cap
Filled PortionThe part of a deposit that counts toward the soft cap and is kept by the launch
Excess RefundReturn of the portion of a deposit above the soft cap, leaving token allocation intact

Next Steps