Skip to content
exile
Raydium LaunchLab · StonkFunreads pump.funTypeScript · bigint

exile-sdk

Everything this app does, as a library. Read a pump.fun coin, snapshot its holders, zero the dev and insiders, shape a curve that locks the holders' bags for free (or quote their first buy on StonkFun), and build the one-transaction launch, the vesting math and the claims.

at a glance
const coin = await exile.getPumpCoin(pumpMint);
const snapshot = await exile.snapshot(coin.mint);
const insiders = await exile.detectInsiders(coin, snapshot);

// Free airdrop: lock the holders' 1:1 bags, capped at 20% of supply
const { targetTotal } = exile.planAllocation(snapshot, insiders, { tokensAvailable: 0n });
const lockedAmount = lockedAmountFor({ targetTotal, supply, lockBps: DEFAULT_LOCK_BPS, maxLocked });
const plan = exile.planAllocation(snapshot, insiders, {
  tokensAvailable: lockedAmount, // nobody buys it, the dev gets 0
});
Start

Install

One package, two entry points. Raw token amounts are bigint everywhere, addresses are base58 strings at module boundaries, fees are basis points (LaunchLab's own rates are parts per million).

terminal
pnpm add exile-sdk @solana/web3.js
exile-sdkNode / server

Everything: pump reader, snapshot, insider scan, allocation, anti-vamp seal, both launch builders, LaunchLab platform, vesting and fee instructions, curve rules, the StonkFun API client, claim builders, curve-state decoding and ExileClient.

exile-sdk/liteBrowser-safe

Types, constants, errors, the LaunchLab curve math (quoteFirstBuy, budgetForTokens…), the airdrop shape math (airdropCurveShape, lockedAmountFor, maxLockedAmount, curveFromShape), both vesting clocks and normalizeKey. No network calls.

Start

Launch modes

Two ways to hand the old holders their bags. Both zero the dev and insiders, seal the origin coin, and vest the bags linearly after graduation.

Free airdrop (default)"airdrop"
  • The holders' share is LaunchLab's totalLockedAmount: minted locked at launch, nobody buys it. 5%–30% of supply, 20% by default.
  • Curve buyers pay for it through a higher opening price. The launcher pays rent and fees, plus an optional dev buy.
  • The launcher is the pool creator and earns the creator fee. Runs on Exile's LaunchLab platform, or StonkFun's once its curve rule allows a lock.
  • Bags unlock on LaunchLab's clock, from graduation. No graduation, no unlock.
List on StonkFun"stonkfun"
  • StonkFun's exact standard curve, listed on its board. Its curve rule allows no lock, so the bags are bought.
  • The launcher's first buy lands in the launch transaction, at the very bottom of the curve, into Exile's vault.
  • The launcher keeps a cut of what they buy (20% by default, up to 50%), vested like the holders.
  • Bags unlock on Exile's clock: from graduation, or after a fallback if the curve never graduates.
Start

Quickstart

From a pump.fun mint to a signed free-airdrop launch: the holders' bags are locked into the coin itself. Snapshots read mainnet and the launch runs on mainnet LaunchLab, so this spends real SOL (rent and fees).

relaunch.ts
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import {
  DEFAULT_LAUNCH_FEE_LAMPORTS,
  DEFAULT_LOCK_BPS,
  DEFAULT_NEW_SUPPLY,
  ExileClient,
  buildAirdropLaunchTransaction,
  deriveExilePlatformConfig,
  fetchLookupTables,
  lockedAmountFor,
  resolveAirdropVenue,
} from "exile-sdk";

const authority = Keypair.fromSecretKey(platformSecretKey); // seal base, vesting beneficiary, vault owner
const launcher = new PublicKey(launcherWallet); // fee payer and pool creator: pays rent, earns the creator fee

const mainnet = new Connection(MAINNET_RPC_URL);
const exile = new ExileClient({
  snapshotConnection: mainnet, // pump.fun lives on mainnet
  launchConnection: mainnet, // and so does Raydium LaunchLab
  authority: authority.publicKey,
  heliusApiKey: process.env.HELIUS_API_KEY,
});

// 1. Read the pump.fun coin. A coin whose seal exists was exiled before.
const coin = await exile.getPumpCoin(pumpMint);
if (await exile.isAlreadyExiled(coin.mint)) throw new Error("This coin already had its exile");

// 2. Snapshot every holder, then flag the dev, snipers, bundles and dev-linked wallets.
const snapshot = await exile.snapshot(coin.mint);
const insiders = await exile.detectInsiders(coin, snapshot);

// 3. The holders' share: their 1:1 bags, capped at 20% of the supply.
const { targetTotal } = exile.planAllocation(snapshot, insiders, { tokensAvailable: 0n });
const wanted = lockedAmountFor({
  targetTotal,
  supply: DEFAULT_NEW_SUPPLY,
  lockBps: DEFAULT_LOCK_BPS,
  maxLocked: DEFAULT_NEW_SUPPLY, // the venue's own cap applies below
});

// 4. Where it launches: StonkFun's platform once its curve rule allows a lock, else Exile's own.
const unlockPeriod = 86_400n; // linear unlock from graduation
const { venue, reason } = await resolveAirdropVenue({
  connection: mainnet,
  exilePlatformId: deriveExilePlatformConfig(authority.publicKey),
  wants: { lockedAmount: wanted, unlockPeriod },
});
if (!venue) throw new Error(reason);
const lockedAmount = wanted < venue.maxLocked ? wanted : venue.maxLocked;
const plan = exile.planAllocation(snapshot, insiders, { tokensAvailable: lockedAmount });

// 5. One v0 transaction: seal + create on LaunchLab with the lock + the holders' vesting record
//    + the launch fee (which carries the origin mint, so getSignaturesForAddress(origin) finds it).
const mint = Keypair.generate();
const launch = await buildAirdropLaunchTransaction({
  connection: mainnet,
  venue,
  launcher,
  authority: authority.publicKey,
  treasury,
  launchFeeLamports: DEFAULT_LAUNCH_FEE_LAMPORTS,
  originMint: coin.mint,
  name: coin.name,
  symbol: coin.symbol,
  uri: `https://your.app/api/meta/${mint.publicKey.toBase58()}`,
  lockedAmount,
  unlockPeriod,
  mintKeypair: mint,
  lookupTables: await fetchLookupTables(mainnet, [EXILE_LOOKUP_TABLE]),
});
launch.tx.sign([authority, mint]); // the launcher's wallet signs as fee payer and creator, then sends
console.log(`opens at ${launch.shape.startMarketCapQuote} SOL, costs at most ${launch.launcherCostLamports} lamports`);

List on StonkFun instead

Same snapshot and insiders; the holders' bags come from a first buy the launcher funds, and the launcher's cut joins the plan as a leaf labelled "launcher".

relaunch-stonkfun.ts
import { addLauncherShare, DEFAULT_LAUNCHER_CUT_BPS } from "exile-sdk";

// StonkFun's exact standard curve, and what the launcher's budget buys at its very bottom.
const pricing = await exile.fetchPricing(); // validated, cached ~60s
const firstBuyLamports = 5_000_000_000n; // 5 SOL
const quote = await exile.quoteFirstBuy(pricing, firstBuyLamports);

// The launcher keeps a cut of the buy; holders split the rest pro rata, capped at 1:1.
const cut = (quote.tokensOut * BigInt(DEFAULT_LAUNCHER_CUT_BPS)) / 10_000n;
const holders = exile.planAllocation(snapshot, insiders, { tokensAvailable: quote.tokensOut - cut });
const plan = addLauncherShare(holders, launcher.toBase58(), cut); // one leaf per owner, new merkle root

// Seal + create on StonkFun + the holders' first buy into the vault + the launch fee.
const launch = await exile.buildLaunchTransaction({
  pricing,
  launcher, // fee payer, and the pool creator StonkFun forwards its creator fee to
  originMint: coin.mint,
  name: coin.name,
  symbol: coin.symbol,
  uri,
  firstBuyLamports,
  mintKeypair: mint,
});
launch.tx.sign([authority, mint]);
Public RPC endpoints often refuse getProgramAccounts for large coins. Pass heliusApiKey and snapshots switch to Helius DAS getTokenAccounts, paginated.
Reference

ExileClient

A thin facade over the modules below. It holds the two connections and the platform authority so call sites stay short; every module function is also exported on its own.

ExileConfig
FieldTypeDefaultMeaning
snapshotConnectionConnection–Mainnet connection used to read pump.fun coins, holders and insider history.
launchConnectionConnection–Where StonkFun / LaunchLab live: mainnet-beta, or a localnet with the programs cloned.
authorityPublicKey–Platform authority: LaunchLab creator, seal base and owner of the vault the first buy lands in.
treasuryPublicKey?authorityLaunch fee recipient.
launchFeeLamportsbigint?0.1 SOLDefault launch fee; a single launch can override it.
lookupTablesPublicKey[]?–Exile's address lookup table(s), fetched once by the client. The launch is a v0 transaction that does not fit without one.
heliusApiKeystring?–Enables Helius DAS snapshots.
  • getPumpCoin(mint): Promise<PumpCoin>
    On-chain first (bonding curve PDA, metadata, creator); the pump.fun API is best-effort enrichment. Throws NOT_PUMP_COIN when there is no bonding curve.
  • snapshot(mint, opts?): Promise<Snapshot>
    Every holder at one slot, aggregated by owner, sorted descending. SPL Token and Token-2022. Program-owned holders are kept and flagged.
  • detectInsiders(coin, snapshot, opts?): Promise<InsiderReport>
    Dev, snipers, bundle clusters and dev-linked wallets, each with reasons and a confidence.
  • planAllocation(snapshot, report, { tokensAvailable, ...opts }): AllocationPlan
    Pure and deterministic: the first buy's tokens split pro rata by old bag, capped at 1:1, plus the merkle root.
  • fetchPricing(): Promise<StonkfunPricing> · fetchFeeRate(pricing): Promise<bigint>
    StonkFun's standard SOL curve (validated, cached ~60s), and the total trade fee in ppm read from LaunchLab.
  • quoteFirstBuy(pricing, budgetIn, feeRate?): Promise<FirstBuyQuote>
    Exact first-buy quote on a fresh curve; reads the fee rate from chain unless given.
  • deriveSeal(originMint) · isAlreadyExiled(originMint)
    The one seal address this coin can have under this authority, and whether it exists.
  • checkVamp(candidate, registry, opts?): VampCheckResult
    Lookalike check against the registry.
  • buildLaunchTransaction(params): Promise<BuiltStonkfunLaunch>
    The one-transaction launch. Treasury, fee, pricing and lookup tables default to the client's.
  • fetchCurveState(pool) · fetchCurveStates(pools) · fetchRecentTrades(pool, limit?)
    Live curve state decoded from LaunchLab, and recent trades parsed from the pool's transactions.
  • buildClaimTransaction(params) · buildAirdropBatches(params)
    Pull claims signed by the holder, or push batches signed by the authority.

Every method wraps a module function of the same job (fetchStonkfunPricing, buildStonkfunLaunchTransaction, fetchLaunchCurveState…), documented below with the pure curve and vesting math.

Reference

Insider detection

Every flag carries its reasons and a 0..1 confidence. Exclusion is the default decision; the launcher can override any wallet before launch.

"dev"

The coin creator, read from the bonding curve or the pump.fun API.

"sniper"

First buy landed within creationSlot + 2 slots, from pre/post token balances.

"bundle"

Part of a cluster of at least 3 wallets holding identical balances. Program-owned holders are ignored.

"dev_linked"

Received tokens directly from the dev wallet.

"manual"

Excluded by the launcher through AllocationOptions.exclude.

InsiderDetectOptions
FieldTypeDefaultMeaning
sniperSlotWindownumber2Slots after creation that still count as sniping.
maxSignaturePagesnumber100Pages of 1000 signatures to walk back looking for the creation tx.
maxEarlyTransactionsnumber300Early transactions fetched and parsed once the creation slot is found.
bundleMinClusterSizenumber3Minimum wallets sharing an identical balance to count as a bundle.
onProgressProgressFn?–Stage callbacks for long scans.
report.reachedCreation === false means the history walk stopped before the creation transaction, so sniper detection is partial. Raise maxSignaturePages or review early wallets by hand.
Reference

Allocation

planAllocation splits whatever the launch gives the holders: the locked amount in airdrop mode, or what the first buy bought on StonkFun. Pure, deterministic and bigint-only.

  • The 1:1 target

    Each eligible holder's target is their old bag mapped onto the new supply: originAmount × newSupply / originSupply. targetTotal is the sum.

  • Split by old bag, capped at 1:1

    airdropTotal = min(tokensAvailable, targetTotal), split pro rata by old bag and floored per holder. Nobody ever gets more than their 1:1 target.

  • Coverage

    scaleBps = airdropTotal / targetTotal. 10000 means every holder keeps exactly their bag; a bigger lock (or first-buy budget) moves it toward 10000. budgetForTokens(curve, targetTotal) prices full bags on StonkFun.

  • Insiders, programs and dust get zero

    Excluded owners land in plan.excluded with their reasons; off-curve owners (bonding curve, pool vaults) and holders below minOriginSharePpm of the origin supply are skipped.

PlanAllocationOptions
FieldTypeDefaultMeaning
tokensAvailablebigint–Tokens the launch gives the holders: the locked amount (airdrop) or the first buy's tokensOut minus the launcher's cut (StonkFun). Never more than this is allocated.
newSupplybigint1,000,000,000 × 10^6Raw supply of the new coin (1B, both modes).
newDecimalsnumber6Decimals of the new coin.
minOriginSharePpmnumber1 ppmDust floor as parts per million of the origin supply.
includeBase58[]?–Force-include owners even if flagged.
excludeBase58[]?–Force-exclude owners (reason "manual").

plan.entries is sorted by amount descending and each entry's index equals its array position, which is also its merkle leaf index. The plan also reports where the origin supply went: excludedOriginTotal, programOwnedOriginTotal and dustOriginTotal.

  • addLauncherShare(plan, launcher, amount): AllocationPlan
    StonkFun mode: adds the launcher's cut as a leaf labelled "launcher", merged into their own leaf if they held the old coin. Entries are re-indexed and the merkle root recomputed; airdropTotal stays the holders' total and the cut goes to launcherShare.
Reference

Merkle leaves

Every plan commits to its entries with a sha256 merkle root, so anyone can verify an allocation without trusting the server.

allocationLeaf(entry) = sha256 of a 44-byte preimageoffsets in bytes
  • Nodes hash the sorted pair: sha256(min(a, b) ‖ max(a, b)).
  • An odd node at the end of a level is promoted unchanged.
  • An empty tree has a root of 64 zero hex characters.
verify.ts
import { allocationLeaf, computeMerkleRoot, merkleProof, verifyMerkleProof } from "exile-sdk";

const leaves = plan.entries.map((e) => allocationLeaf(e));
computeMerkleRoot(leaves) === plan.merkleRoot; // true

const entry = plan.entries[42];
const proof = merkleProof(leaves, entry.index); // hex sibling hashes
verifyMerkleProof(allocationLeaf(entry), proof, plan.merkleRoot); // true
LaunchLab

Airdrop curve

LaunchLab mints the whole supply into the pool. The curve sells totalSellA, the locked amount waits in the vault for the vesting records, and the migrate amount seeds Raydium at graduation. Keeping the migrate amount and the raise at StonkFun's values keeps the graduation price; the lock only raises the opening price.

Locked for holders
5%–30%
of supply, 20% by default, capped at their 1:1 bags
Migrate amount
206.9M
seeds the Raydium CPMM pool, same as StonkFun
Graduation
85 SOL raised
about 411 SOL market cap, whatever the lock
  • maxLockedAmount({ supply, migrateAmount, raise?, maxLockRate?, minSellRate? }): bigint
    Largest lock the SOL GlobalConfig accepts: the max lock rate, the min sell rate and the constant-product bound (totalSellA must exceed the migrate amount).
  • lockedAmountFor({ targetTotal, supply, lockBps, maxLocked }): bigint
    The holders' locked amount: their 1:1 target, capped at lockBps of supply and at maxLocked.
  • airdropCurveShape({ supply, migrateAmount, lockedAmount, raise }): AirdropCurveShape
    totalSellA, the virtual reserves LaunchLab will derive, and the start and graduation market caps. Throws INVALID_CONFIG when LaunchLab would refuse the shape.
  • curveFromShape(shape, feeRate): CurveParams
    The fresh curve, for quoteFirstBuy (the optional dev buy) or any trade quote.
airdrop-curve.ts
import {
  AIRDROP_MIGRATE_AMOUNT,
  AIRDROP_RAISE_LAMPORTS,
  DEFAULT_NEW_SUPPLY,
  airdropCurveShape,
  curveFromShape,
  lockedAmountFor,
  maxLockedAmount,
  quoteFirstBuy,
} from "exile-sdk/lite";

const supply = DEFAULT_NEW_SUPPLY;
const migrateAmount = AIRDROP_MIGRATE_AMOUNT; // 206.9M, like StonkFun: same graduation price
const raise = AIRDROP_RAISE_LAMPORTS; // 85 SOL

const maxLocked = maxLockedAmount({ supply, migrateAmount, raise }); // ~586M, LaunchLab's limit
const lockedAmount = lockedAmountFor({ targetTotal: plan.targetTotal, supply, lockBps: 2000, maxLocked });

const shape = airdropCurveShape({ supply, migrateAmount, lockedAmount, raise });
shape.totalSellA; // supply - migrateAmount - lockedAmount, sold on the curve
shape.startMarketCapQuote; // ~50 SOL at a 20% lock (StonkFun's standard opens at ~28)
shape.graduationMarketCapQuote; // ~411 SOL, whatever the lock

// Quote an optional dev buy on the fresh curve, at the venue's on-chain fee rate.
const devBuy = quoteFirstBuy(curveFromShape(shape, venue.feeRate), 1_000_000_000n);
Virtual reserves for a constant curve with no migrate fee, with TF the raise, S totalSellA and M the migrate amount: x0 = TF·S²/M / (TF·S/M − TF), y0 = TF² / (TF·S/M − TF). They match real locked pools on mainnet exactly.
StonkFun

StonkFun curve

StonkFun only lists coins launched with its exact standard curve, so stonkfun mode copies it from the pricing API and does LaunchLab's constant-product math itself, bit for bit, fees included.

Supply
1,000,000,000
6 decimals, Token-2022
Sold on the curve
793.1M
the rest + the SOL raised seed Raydium CPMM
Graduation
85 SOL raised
1.25% fee on the quote side (Raydium 0.25% + StonkFun 1%)

Those are the standard SOL-pair values at the time of writing. Treat fetchStonkfunPricing() as the source of truth: it reads https://www.stonkfun.xyz/api/public/v1/launchlab/pricing, validates every field a launch depends on (config, CPMM migration, supply, sale, raise, no vesting, platform, allow-config and curve-rule accounts) and caches the answer for about a minute.

  • fetchStonkfunPricing(quoteMint = SOL, fetchImpl?): Promise<StonkfunPricing>
    The exact create values. Throws STONKFUN_API when the API is down or answers a shape a launch can't rely on.
  • fetchLaunchLabFees(connection, configId, platformId): Promise<LaunchLabFees>
    Reads LaunchLab's GlobalConfig and StonkFun's PlatformConfig; feeRate is protocol + platform + creator, in ppm.
  • curveFromPricing(pricing, feeRate) · curveFromState(state, feeRate): CurveParams
    The untouched curve a launch opens with, or the live curve of a pool that already trades.
  • quoteFirstBuy(curve, budgetIn): FirstBuyQuote
    A buy_exact_in: tokensOut is exactly what the program pays, plus fee, price and market cap after, and curveProgressBps.
  • budgetForTokens(curve, tokensOut): bigint
    Smallest budget whose buy returns at least tokensOut (capped at what is left on the curve).
  • quoteSell(curve, tokensIn): SellQuote
    A sell_exact_in: SOL out after the fee.
  • curveProgressBps(realB, raise) · priceFromState(s) · marketCapFromState(s)
    Progress toward graduation (capped at 10000), quote per whole token, fully diluted market cap.
curve.ts
import { budgetForTokens, curveFromPricing, curveProgressBps, quoteFirstBuy } from "exile-sdk/lite";

const feeRate = await exile.fetchFeeRate(pricing); // ppm, read once from LaunchLab + StonkFun configs
const curve = curveFromPricing(pricing, feeRate); // realA = realB = 0: the launch's opening state

const quote = quoteFirstBuy(curve, 2_000_000_000n); // 2 SOL in
quote.tokensOut; // exactly what buy_exact_in pays out, safe as its minimum out
quote.fee; // the curve fee inside the budget (1.25% on StonkFun's SOL curve, read from chain)
quote.curveProgressBps; // how much of the raise the first buy already fills

// Budget that gives every eligible holder exactly 1:1 (what the wizard offers as "full bags")
const fullBags = budgetForTokens(curve, plan.targetTotal);

curveProgressBps(state.realB, state.totalFundRaisingB); // 0..10000
Buy exact in: fee = ceil(in × rate / 1e6), out = floor((in − fee) × (vA − rA) / (vB + rB + in − fee)), capped at totalSellA − rA. The tests prove these numbers against @raydium-io/raydium-sdk-v2.
StonkFun

Curve rules

StonkFun enforces its curve on-chain with a PlatformCurveRule: groups of constraints over the launch's parameters, OR'ed between groups and AND'ed within one. Today every group pins TotalLockedAmount = 0, which is why airdrop mode runs on Exile's own platform.

  • decodePlatformCurveRule(data): PlatformCurveRule
    Decodes the rule account into groups of { field, op, value } constraints.
  • checkLaunchAgainstCurveRule(rule, launch): { ok, groupId?, unsatisfied }
    Whether a launch shape passes any group, and which constraints fail otherwise.
  • findAirdropShape(rule, { supply, raise, lockedAmount, unlockPeriod, migrateAmount? })
    The airdrop shape the rule accepts with the biggest lock up to lockedAmount, with its groupId, or null when no group allows a lock.
  • resolveAirdropVenue({ connection, exilePlatformId?, stonkfunPricing?, wants })
    { venue, reason? }: StonkFun's platform as soon as its rule accepts a lock, else Exile's. The venue carries the platform, the fee rate read from chain, maxLocked and the accounts to append.
curve-rule.ts
import { checkLaunchAgainstCurveRule, decodePlatformCurveRule, derivePlatformCurveRule, findAirdropShape } from "exile-sdk";

// StonkFun pins every curve with an on-chain rule: groups are OR'ed, constraints within a group AND'ed.
const info = await connection.getAccountInfo(derivePlatformCurveRule(stonkfunPlatform, configId));
const rule = decodePlatformCurveRule(info!.data);

checkLaunchAgainstCurveRule(rule, launchShape); // { ok, groupId?, unsatisfied }

// The airdrop shape the rule accepts with the biggest lock, or null while StonkFun allows none.
const found = findAirdropShape(rule, { supply, raise, lockedAmount, unlockPeriod: 86_400n });
if (found) console.log(`StonkFun group ${found.groupId} accepts a lock of ${found.shape.totalLockedAmount}`);
LaunchLab

Launch transactions

Each builder returns one unsigned v0 transaction. Whatever the holders get is created inside the transaction that creates the coin, so no sniper ever gets in ahead of the community.

Airdrop modesigners: launcher + authority + mint
buildAirdropLaunchTransaction
  1. 1Allocate the anti-vamp seal at createWithSeed(authority, seed(originMint), SystemProgram)
  2. 2initializeWithToken2022 on the venue's platform with the airdrop shape; creator = the launcher
  3. 3create_vesting_account: the whole locked amount to the Exile authority, whose Token-2022 vault is created in the same tx
  4. 4Optional dev buy (buy_exact_in) straight to the launcher's wallet
  5. 5Launch fee to the treasury (0.1 SOL by default), carrying the origin mint as a reference key
StonkFun modesigners: launcher + authority + mint
buildStonkfunLaunchTransaction
  1. 1Allocate the anti-vamp seal at createWithSeed(authority, seed(originMint), SystemProgram)
  2. 2initializeWithToken2022 on LaunchLab with StonkFun's standard config; creator = the launcher
  3. 3buy_exact_in for the holders with the launcher's SOL; tokens land in the authority's vault
  4. 4Launch fee to the treasury (0.1 SOL by default), carrying the origin mint as a read-only reference key so getSignaturesForAddress(originMint) finds the launch
LaunchLab
LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj
SOL GlobalConfig
6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX
Raydium CPMM
CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C
StonkFun platform
4E876qZTE9FJMrBzgVtBrSrzz2TLivB5Y5QXPjB4gZL7
BuildAirdropLaunchParams
FieldTypeDefaultMeaning
connection · venueConnection · AirdropVenue–Launch cluster, and the venue from resolveAirdropVenue().
launcherPublicKey–Fee payer and pool creator: rent, fees, any dev buy and the launch fee; earns the creator fee. Signs.
authorityPublicKey–Seal base, vesting beneficiary and owner of the holders' vault. Signs.
treasury · launchFeeLamportsPublicKey · bigint–Where the launch fee goes, and how much.
originMint · name · symbol · uriBase58 · string–The pump.fun coin (seeds the seal) and the new coin's Token-2022 metadata.
lockedAmount · unlockPeriodbigint · bigint–The holders' locked share and its linear unlock from graduation; the cliff is always 0.
devBuyLamportsbigint?noneOptional launcher buy in the same transaction.
mintKeypair · lookupTablesKeypair? · ALT[]?–The new mint (signs) and Exile's lookup table(s).

The result carries tx, mint, pool, seal, vault, vestingRecord, the shape it launched, the devBuy quote, launcherCostLamports (an upper bound: about 0.015 SOL of rent and fees, plus the launch fee and any dev buy) and the blockhash with its lastValidBlockHeight. verifyAirdropLaunch checks the pool's platform, creator and vesting parameters, the record's beneficiary and share, and the seal.

BuildStonkfunLaunchParams
FieldTypeDefaultMeaning
connectionConnection–Launch cluster (blockhash, rent, fee configs).
pricingStonkfunPricing–From fetchStonkfunPricing(); copied into initialize exactly.
launcherPublicKey–Fee payer: the first-buy SOL, rent and the launch fee. Signs.
creatorPublicKey?launcherLaunchLab pool creator; StonkFun forwards its creator fee to it.
authorityPublicKey–Seal base and owner of the vault the first buy lands in. Signs.
treasury · launchFeeLamportsPublicKey · bigint–Where the launch fee goes, and how much.
originMintBase58–The pump.fun coin: seeds the seal and rides as a reference key on the launch fee.
name · symbol · uristring–Token-2022 metadata for the new coin.
firstBuyLamportsbigint–The holders' budget.
mintKeypairKeypair?generatedThe new mint. Signs.
lookupTablesAddressLookupTableAccount[]?–Required in practice for the transaction to fit.

buildStonkfunLaunchTransaction appends the allow-config and curve-rule accounts from the pricing response read-only to initialize; StonkFun adopts the pool within a minute or two. Its result carries tx, mint, pool, seal, vault, the firstBuy quote it was built from, its serialized size, launcherCostLamports (an upper bound on what the launcher spends) and the blockhash with its lastValidBlockHeight. Name, symbol and URI are capped at 32, 10 and 200 bytes.

Address lookup table

About 27 accounts plus the coin's metadata don't fit in 1232 bytes inline. The accounts that are the same for every launch (the global config, both platforms and their fee vaults, StonkFun's allow-config and curve rule, the CPMM config, the treasury) live in one table the operator creates once; signers and programs always stay inline.

setup-lookup-table.ts
import { buildExileLookupTableTxs } from "exile-sdk";

// Once per operator: the launch-invariant accounts go into one address lookup table.
const { txs } = await buildExileLookupTableTxs({ connection, authority: authority.publicKey, pricing, treasury });
for (const tx of txs) {
  tx.sign(authority);
  await connection.sendRawTransaction(tx.serialize()); // in order; usable from the next slot
}
StonkFun

Curve state & trades

Decoded straight from the LaunchLab pool account, so the board never waits on an indexer.

"trading"

On the curve: buys add SOL, sells take it out.

"migrating"

Raise reached; waiting for Raydium's migration crank.

"graduated"

Liquidity is in a Raydium CPMM pool (cpmmPool), LP locked. Locked bags start vesting.

  • deriveLaunchPool(mint, quoteMint): PublicKey
    LaunchLab pool PDA (and the other PDAs in stonkfun/pda).
  • fetchLaunchCurveState(connection, pool) · fetchLaunchCurveStates(connection, pools)
    LaunchCurveState or null; the plural form is one getMultipleAccountsInfo. Carries the pool's vesting schedule and migrated.
  • fetchRecentTrades(connection, pool, limit = 50): Promise<TradeEvent[]>
    Newest first, from the pool's base-vault and quote-vault deltas (fees included on buys, deducted on sells).
  • fetchStonkfunToken(mint): Promise<{ listed, raw } | null>
    Whether StonkFun's API reports the coin (launchpad: launchlab once adopted).
board.ts
// One getMultipleAccountsInfo for the whole board.
const states = await exile.fetchCurveStates(pools);
for (const s of states) {
  if (!s) continue; // not created yet, or not a LaunchLab pool
  console.log(s.mint, s.status, s.progressBps / 100, "%", s.marketCapQuote, "SOL");
  if (s.status === "graduated") console.log("Raydium CPMM pool", s.cpmmPool);
}

const trades = await exile.fetchRecentTrades(pool, 50); // newest first
Holders

Vesting

Holders' bags unlock linearly after graduation, so the community's head start can't drain the curve before the Raydium pool exists. Two clocks, both pure and browser-safe: LaunchLab's own for airdrop mode, Exile's for StonkFun listings.

Default schedule (configurable per launch)StonkFun listings only: vesting starts 7 days after launch anyway
  • vestingWindow({ launchedAt, graduatedAt?, durationSeconds, fallbackUnlockSeconds, nowSeconds? })
    { startAt, endAt }: start = min(graduatedAt, launchedAt + fallback). A late graduation never restarts the clock.
  • vestedAmount(total, window, nowSeconds): bigint
    Linear, floored: 0 before the start, total from the end on.
  • vestedBps(window, nowSeconds): number
    Share of every allocation vested right now.
  • launchLabVestedAmount({ share, startTime, unlockPeriod, cliffPeriod?, now, migrated }): bigint
    Airdrop mode: LaunchLab's rule, share × min(now − start, unlock) / unlock, and 0 until the pool migrated. No fallback.

StonkFun listings: Exile's clock

vesting.ts
import { vestedAmount, vestedBps, vestingWindow } from "exile-sdk/lite";

const window = vestingWindow({
  launchedAt, // unix seconds
  graduatedAt, // null while the coin is still on its curve
  durationSeconds: 86_400, // linear unlock after graduation
  fallbackUnlockSeconds: 604_800, // vesting starts anyway 7 days after launch
});

const now = Math.floor(Date.now() / 1000);
vestedBps(window, now); // share of every bag unlocked right now
const claimable = vestedAmount(entry.amount, window, now) - alreadyClaimed;

Free airdrops: LaunchLab's clock

launchlab-vesting.ts
import { launchLabVestedAmount } from "exile-sdk/lite";

// Airdrop mode runs on LaunchLab's own clock: it starts at the buy that completes the curve,
// and nothing unlocks before the pool migrates. No fallback.
const state = await exile.fetchCurveState(pool);
const vested = launchLabVestedAmount({
  share: entry.amount,
  startTime: Number(state.vesting.startTime),
  unlockPeriod: Number(state.vesting.unlockPeriod),
  now: Math.floor(Date.now() / 1000),
  migrated: state.migrated,
});
Protections

Anti-vamp

LaunchLab creates the mint from a fresh keypair, so the on-chain guard is a seal: an account at an address derived from the origin coin, created by the launch. It can exist once, so a second Exile relaunch of the same coin fails on-chain.

Deterministic seal

The seed is base58(sha256("exile:v1:" + originMint)).slice(0, 32) and the seal lives at PublicKey.createWithSeed(authority, seed, SystemProgram.programId). Only the authority can create it, and only once. Lamports alone don't count: anyone can send SOL to an address, so isSealTaken checks for data or a non-system owner.

Lookalike registry

Names and tickers are folded by normalizeKey (NFKC, lowercase, 0→o, 1/l/|→i, 3→e, 4→a, 5/$→s, 7→t, Cyrillic and Greek twins, leading "the", trailing coin/token/inu). Logos match by exact sha256 or a 64-bit dHash within 6 bits.

  • deriveExileSeed(originMint): string
    Exactly 32 characters.
  • deriveExileSeal(authority, originMint): Promise<PublicKey>
    The one possible seal address.
  • isAlreadyExiled(connection, authority, originMint): Promise<boolean>
    Whether the seal exists (data or a non-system owner).
  • normalizeKey(s): string
    Comparison key for names and tickers. Also in /lite.
  • fingerprintCoin({ originMint, name, symbol, imageSha256?, imageDHash? }): CoinFingerprint
    Fingerprint stored in the registry.
  • dHashFromGrayscale(pixels, 9, 8): string · hammingHex(a, b): number
    16-hex-char difference hash of a 9×8 grayscale image, and the bit distance between two.
  • checkVamp(candidate, registry, { dHashThreshold? }): VampCheckResult
    ok plus every match: origin_mint, exile_mint, symbol, name, image_exact, image_similar.
antivamp.ts
import { checkVamp, deriveExileSeal, deriveExileSeed, fingerprintCoin, isAlreadyExiled } from "exile-sdk";

deriveExileSeed(originMint); // 32 chars, identical on every call
const seal = await deriveExileSeal(authority, originMint); // createWithSeed(authority, seed, SystemProgram)
await isAlreadyExiled(connection, authority, originMint); // true once a launch created the seal

// registry: RegistryEntry[] for every existing relaunch
const result = checkVamp(fingerprintCoin({ originMint, name: "PR0JECT K1MCH1", symbol: "$K1MCH1" }), registry);
if (!result.ok) {
  for (const m of result.matches) console.log(m.kind, m.existing.symbol, m.detail);
}
Holders

Claims & airdrops

Holders claim whatever has vested so far, as often as they like, or an operator pushes vested amounts in batches. A unique memo on every claim makes double claims detectable. In airdrop mode the same transaction first pulls what LaunchLab vested into Exile's vault.

claim.ts
import { PublicKey } from "@solana/web3.js";
import { buildClaimTransaction, claimMemo, findSignatureByMemo } from "exile-sdk";

export async function prepareClaim(claimant: PublicKey, amount: bigint, nonce: string) {
  // amount: what has vested and is still unclaimed; partial claims are fine
  const memo = claimMemo(mint.toBase58(), claimant.toBase58(), nonce);

  // A landed claim is findable by its memo, so a retry never pays out twice.
  const landed = await findSignatureByMemo(connection, claimant, memo);
  if (landed) return { landed };

  const { tx, lastValidBlockHeight } = await buildClaimTransaction({
    connection,
    authority: authority.publicKey,
    mint,
    claimant,
    amount,
    decimals,
    memo,
    // Airdrop mode: pull newly vested tokens from LaunchLab into the vault first (claim_vested_token).
    pullVestedFrom: newlyVested ? pool : undefined,
  });
  tx.partialSign(authority); // the holder's wallet signs as fee payer and sends
  return { tx: tx.serialize({ requireAllSignatures: false }).toString("base64"), lastValidBlockHeight };
}
  • buildClaimTransaction({ connection, authority, mint, claimant, amount, decimals, memo, pullVestedFrom? })
    Vault transfer of any amount (partial claims work) that the server partially signs. The claimant is fee payer, signs, and pays their own ATA rent. pullVestedFrom (the pool) prepends claim_vested_token when the record has newly vested tokens.
  • deriveVestingRecord(pool, beneficiary) · decodeVestingRecord(data)
    The ["pool_vesting", pool, beneficiary] record and its claimedAmount / tokenShareAmount.
  • claimVestedTokenInstruction({ beneficiary, pool, mint, beneficiaryAta?, tokenProgram? })
    Moves everything vested so far from the pool into the beneficiary's account. Fails until the pool migrated.
  • claimMemo(mint, owner, nonce): string
    exile-claim:<mint>:<owner>:<nonce>; nonce is 1 to 64 characters of [A-Za-z0-9_-].
  • findSignatureByMemo(connection, address, memo, limit?): Promise<string | null>
    Finds a landed claim from getSignaturesForAddress memos (returned as "[len] memo").
  • buildAirdropBatches({ connection, authority, mint, decimals, recipients, maxPerTx = 8 })
    Authority-signed push transfers, packed under the 1232-byte transaction limit.
Launchers

Creator fees

The launcher is the LaunchLab pool creator, so they earn the creator fee on every curve trade (0.5% on Exile's platform, the program's cap). LaunchLab keeps one vault per creator and quote mint, so one claim covers every coin they launched.

creator-fees.ts
import { Transaction } from "@solana/web3.js";
import { NATIVE_MINT, createCloseAccountInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { claimCreatorFeeInstruction, fetchCreatorFeeClaimable } from "exile-sdk";

// LaunchLab keeps one creator fee vault per (creator, quote mint): one claim covers every coin.
const claimable = await fetchCreatorFeeClaimable(connection, launcher); // lamports
if (claimable > 0n) {
  const wsol = getAssociatedTokenAddressSync(NATIVE_MINT, launcher);
  const tx = new Transaction().add(
    claimCreatorFeeInstruction({ creator: launcher }), // vault -> the launcher's WSOL account, created if missing
    createCloseAccountInstruction(wsol, launcher, launcher), // unwrap to SOL
  );
  // The launcher's wallet is the fee payer and the only signer.
}
  • fetchCreatorFeeClaimable(connection, creator, quoteMint?): Promise<bigint>
    Lamports in the creator fee vault; 0 when it doesn't exist yet.
  • claimCreatorFeeInstruction({ creator, quoteMint?, creatorAta? })
    Vault to the creator's quote account (WSOL, created if missing). The creator is the only signer.
  • fetchPlatformFeeClaimable(connection, platformId) · claimPlatformFeeFromVaultInstruction({ platformFeeWallet, platformId })
    The same for the platform's own fee, claimed by its fee wallet.
  • deriveExilePlatformConfig(admin) · buildCreateExilePlatformTx({ connection, admin, treasury, web })
    Exile's platform PDA ["platform_config", admin], and the one-time transaction that creates it (about 0.0054 SOL).

StonkFun-listed coins earn StonkFun's creator share instead, which StonkFun forwards to the pool creator off-chain.

Reference

Errors

Everything the SDK throws on purpose is an ExileError with a stable code. Narrow with isExileError(e).

ExileErrorCode
FieldTypeMeaning
NOT_PUMP_COINcodeNo pump.fun bonding curve exists for this mint.
MINT_NOT_FOUNDcodeThe mint account does not exist.
SNAPSHOT_FAILEDcodeThe RPC refused or failed the holder scan (public RPC often blocks getProgramAccounts).
VAMP_BLOCKEDcodeAlready exiled, or a lookalike of an existing relaunch.
ALREADY_LAUNCHEDcodeThe origin coin's seal already exists on-chain.
STONKFUN_APIcodeStonkFun's API failed or answered something a launch can't rely on.
INVALID_CONFIGcodeOptions or curve parameters failed validation.
NOTHING_TO_AIRDROPcodeNo eligible holders after exclusions and the dust floor.
RPC_ERRORcodeAn RPC call failed.
errors.ts
import { isExileError } from "exile-sdk";

try {
  await exile.fetchPricing();
} catch (e) {
  if (isExileError(e) && e.code === "STONKFUN_API") {
    // StonkFun is down or answered something a launch can't rely on: don't launch now
  } else {
    throw e;
  }
}