Developer docs · Solana

Build on Morph

Everything a bot, wallet, frontend or indexer needs to launch tokens, trade them, rename or lock them and pay out fees. Morph runs on Meteora’s Dynamic Bonding Curve with Metaplex metadata, so you only need public programs and an RPC.

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 configCyPtWki83XjLeSEyjuMTbwx7DFiyaz9ABxo5mR69Yrp9
Meteora DBC programdbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN
Meteora DAMM v2 programcpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG
Metaplex Token MetadatametaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
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

  1. The creator signs one transaction that creates a new mint and a DBC pool with Morph’s config.
  2. DBC mints the full supply into the pool’s base vault and creates Metaplex metadata (name, symbol, URI).
  3. An optional first buy runs in the same transaction, so nobody can buy before the creator.
  4. 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

FeeAmountPaid out by
LaunchNone (Solana network fees and rent, ≈ 0.02 SOL)—
Trading fee1.3% of every swap, in SOLAccrues in the pool
Meteora protocol20% of the trading feeMeteora
Creator70% of the remainderCreator claims (claimCreatorTradingFee)
Morph30% of the remainderFee 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

bash
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-adapters
client.ts
import { 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.

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

ts
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

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

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

Rename & 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.

ts
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);
For indexers, wallets and screeners
Morph tokens can change their name, ticker and logo. Re-read the Metaplex metadata periodically, or whenever you see a transaction to the Token Metadata program that writes to a Morph token’s metadata account.

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.

ts
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:

ts
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. swapQuote expects that wrapper unchanged.
  • isMigrated is a number (0 or 1), not a boolean.
  • Solana now has version 1 transactions; pass maxSupportedTransactionVersion: 1 when fetching.
  • SDK amounts are BNs; JSON.stringify prints them as hex. Use .toString().
  • The SPL mint has no mint authority, so the supply can’t grow.