Overview
Every Morph token is an SPL token with a fixed supply of 1,000,000,000 (6 decimals). It launches on a Meteora Dynamic Bonding Curve (DBC) pool that uses Morph’s config, and trades against SOL.
- Starts at about a $4K market cap and graduates to a Meteora DAMM v2 pool at about $35K.
- After graduation the pool’s liquidity is permanently locked.
- Every trade pays 1.3% in SOL, before and after graduation. After Meteora’s 20% protocol share, 70% goes to the creator and 30% to Morph.
- The creator holds the token’s Metaplex update authority, so they can change the name, ticker and logo at any time, or set the metadata immutable (“lock forever”).
Addresses
| Morph DBC config | CyPtWki83XjLeSEyjuMTbwx7DFiyaz9ABxo5mR69Yrp9 |
| Meteora DBC program | dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN |
| Meteora DAMM v2 program | cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG |
| Metaplex Token Metadata | metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s |
| Quote mint (wrapped SOL) | So11111111111111111111111111111111111111112 |
A token is a Morph token if and only if its DBC pool’s config equals the config above.
How a launch works
- The creator signs one transaction that creates a new mint and a DBC pool with Morph’s config.
- DBC mints the full supply into the pool’s base vault and creates Metaplex metadata (name, symbol, URI).
- An optional first buy runs in the same transaction, so nobody can buy before the creator.
- Trades move the price along the curve. When the quote vault reaches the config’s migration threshold (about 82.6 SOL), the pool migrates to DAMM v2 and the LP is locked 70% creator / 30% Morph.
Fees
| Fee | Amount | Paid out by |
|---|---|---|
| Launch | None (Solana network fees and rent, ≈ 0.02 SOL) | — |
| Trading fee | 1.3% of every swap, in SOL | Accrues in the pool |
| Meteora protocol | 20% of the trading fee | Meteora |
| Creator | 70% of the remainder | Creator claims (claimCreatorTradingFee) |
| Morph | 30% of the remainder | Fee claimer claims (claimPartnerTradingFee) |
Example: a 1 SOL buy pays 0.013 SOL. Meteora takes 0.0026, the creator gets 0.00728 and Morph 0.00312. The fee rate is part of the config and can never change for a launched token.
Setup
npm i @meteora-ag/dynamic-bonding-curve-sdk @solana/web3.js bn.js \
@metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults \
@metaplex-foundation/mpl-token-metadata @metaplex-foundation/umi-web3js-adaptersimport { Connection, PublicKey } from "@solana/web3.js";
import { DynamicBondingCurveClient } from "@meteora-ag/dynamic-bonding-curve-sdk";
export const MORPH_CONFIG = new PublicKey("CyPtWki83XjLeSEyjuMTbwx7DFiyaz9ABxo5mR69Yrp9");
export const connection = new Connection("<your RPC>", "confirmed");
export const dbc = new DynamicBondingCurveClient(connection, "confirmed");The public Solana RPC rate-limits heavily; use a private RPC for anything beyond testing.
Launch a token
Upload a metadata JSON first (Metaplex standard: name, symbol, image, description, external_url, extensions). Name and symbol are capped at 32 and 10 bytes by Metaplex.
import { Keypair } from "@solana/web3.js";
import BN from "bn.js";
const mint = Keypair.generate();
const tx = await dbc.creator.createPoolWithFirstBuy({
createPoolParam: {
name: "Moon Rocket",
symbol: "MOON",
uri: "ipfs://bafy...", // metadata JSON
payer: wallet.publicKey,
poolCreator: wallet.publicKey, // becomes the creator: fee share + update authority
config: MORPH_CONFIG,
baseMint: mint.publicKey,
},
firstBuyParam: { // optional: omit for no first buy
buyer: wallet.publicKey,
buyAmount: new BN(0.5 * 1e9), // 0.5 SOL in lamports
minimumAmountOut: new BN(0), // same transaction as creation, nothing to front-run
referralTokenAccount: null,
},
});
// the new mint keypair must co-sign
const sig = await wallet.sendTransaction(tx, connection, { signers: [mint] });Buy & sell
Exact-in swaps against the curve. swapBaseForQuote: false buys (SOL in), true sells (tokens in). The SDK wraps and unwraps SOL for you.
const pool = await dbc.state.getPoolByBaseMint(mint); // { publicKey, account }
const state = await dbc.state.getPool(pool.publicKey); // pass this object to swapQuote as-is
const config = await dbc.state.getPoolConfig(state.poolState.config);
const amountIn = new BN(0.1 * 1e9); // 0.1 SOL
const quote = dbc.pool.swapQuote({
virtualPool: state,
config,
swapBaseForQuote: false,
amountIn,
slippageBps: 500,
hasReferral: false,
eligibleForFirstSwapWithMinFee: false,
currentPoint: new BN(Math.floor(Date.now() / 1000)), // Morph's config uses timestamps
});
// quote.outputAmount (tokens, 6 decimals), quote.minimumAmountOut, quote.tradingFee
const tx = await dbc.pool.swap({
owner: wallet.publicKey,
payer: wallet.publicKey,
pool: pool.publicKey,
amountIn,
minimumAmountOut: quote.minimumAmountOut,
swapBaseForQuote: false,
referralTokenAccount: null,
});After graduation (poolState.isMigrated is 1) trade through the DAMM v2 pool, for example via Jupiter.
Read tokens & prices
import { getPriceFromSqrtPrice } from "@meteora-ag/dynamic-bonding-curve-sdk";
// every Morph token (getProgramAccounts filtered by config)
const pools = await dbc.state.getPoolsByConfig(MORPH_CONFIG);
const config = await dbc.state.getPoolConfig(MORPH_CONFIG);
for (const { publicKey, account } of pools) {
const p = account.poolState; // accounts are wrapped in poolState
const priceSol = Number(getPriceFromSqrtPrice(p.sqrtPrice, 6, 9)); // SOL per token
const marketCapSol = priceSol * 1_000_000_000;
const progress = Number(p.quoteReserve) / Number(config.migrationQuoteThreshold);
const graduated = p.isMigrated === 1;
const unclaimedCreatorFeeSol = Number(p.creatorQuoteFee) / 1e9;
}The live name, ticker and logo come from the Metaplex metadata account (PDA of ["metadata", metaqb…, mint]). Read it every time; don’t cache it forever.
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { fetchMetadataFromSeeds, mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata";
import { publicKey } from "@metaplex-foundation/umi";
const umi = createUmi("<your RPC>").use(mplTokenMetadata());
const md = await fetchMetadataFromSeeds(umi, { mint: publicKey(mint) });
// md.name, md.symbol, md.uri (JSON with the image), md.isMutable, md.updateAuthorityRename & change logo
The metadata’s update authority (the creator) signs a Metaplex updateV1. To change the logo, upload a new JSON and point uri at it. Re-publish the JSON with the new name and symbol too, since some apps read them from there.
import { updateV1 } from "@metaplex-foundation/mpl-token-metadata";
import { createNoopSigner, some } from "@metaplex-foundation/umi";
import { toWeb3JsInstruction } from "@metaplex-foundation/umi-web3js-adapters";
import { Transaction } from "@solana/web3.js";
const authority = createNoopSigner(publicKey(wallet.publicKey.toBase58()));
const current = await fetchMetadataFromSeeds(umi, { mint: publicKey(mint) });
const ixs = updateV1(umi, {
mint: publicKey(mint),
authority,
payer: authority,
data: some({
name: "Moon Rocket 2",
symbol: "MOON2",
uri: "ipfs://new-json",
sellerFeeBasisPoints: current.sellerFeeBasisPoints,
creators: current.creators,
}),
}).getInstructions().map(toWeb3JsInstruction);
await wallet.sendTransaction(new Transaction().add(...ixs), connection);Lock forever
Setting isMutable to false makes the name, ticker and logo permanent. Rug checkers then stop flagging “mutable metadata”. It can’t be undone.
const ixs = updateV1(umi, { mint: publicKey(mint), authority, payer: authority, isMutable: some(false) })
.getInstructions()
.map(toWeb3JsInstruction);Claim fees
const U64_MAX = new BN("18446744073709551615");
const tx = await dbc.creator.claimCreatorTradingFee({
creator: wallet.publicKey, // the pool creator
payer: wallet.publicKey,
pool: poolAddress,
maxBaseAmount: U64_MAX,
maxQuoteAmount: U64_MAX, // fees are collected in SOL
});Unclaimed amounts are on the pool: poolState.creatorQuoteFee and poolState.partnerQuoteFee (lamports).
Indexing trades
Every launch and swap of a Morph token includes the config account, so a single signature feed covers the whole launchpad, whichever app or bot routed the trade:
const sigs = await connection.getSignaturesForAddress(MORPH_CONFIG, { limit: 1000, until: lastSeen });
for (const { signature, err } of sigs.reverse()) {
if (err) continue;
const tx = await connection.getParsedTransaction(signature, { maxSupportedTransactionVersion: 1 });
// For the pool touched: compare pre/post token balances of its quoteVault (SOL) and baseVault.
// quote up + base down -> buy (SOL in, tokens out)
// quote down + base up -> sell
// anything else -> fee claim, migration, ...
// In the launch transaction the base vault is created: tokens bought = supply - post balance.
}Gotchas
- DBC pool accounts come back wrapped as
{ poolState: {...} }, even though the SDK’s types don’t show it.swapQuoteexpects that wrapper unchanged. isMigratedis a number (0 or 1), not a boolean.- Solana now has version 1 transactions; pass
maxSupportedTransactionVersion: 1when fetching. - SDK amounts are
BNs;JSON.stringifyprints them as hex. Use.toString(). - The SPL mint has no mint authority, so the supply can’t grow.