I watched pump.fun pull in millions of dollars in fees. Millions. For what? Letting people create tokens and add liquidity. That’s it. I sat there thinking “I can build this myself.” So I did.

The whole thing took me about a week. The core infrastructure is shockingly simple. Create a token, slap some metadata on it, spin up a liquidity pool, collect a fee. That’s the entire business model.

Here’s how I built it from scratch.


The Token Creation Flow

Every token launch on Solana follows the same steps:

  1. Create a mint — this is the token itself
  2. Set metadata — name, symbol, image so wallets recognize it
  3. Mint the supply — generate the actual tokens
  4. Create a liquidity pool — pair it with SOL on Raydium
  5. Add liquidity — seed the pool so people can trade

I used to think this required writing a Solana program. It doesn’t. You can do everything with existing programs and client-side code.


Setting Up the Project

You need three main packages:

npm install @solana/web3.js @solana/spl-token @metaplex-foundation/mpl-token-metadata

Here’s the basic setup I use for every script:

import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createMint, getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";

const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY)));

I keep the private key in an environment variable. Never hardcode it. I’ve seen people push keys to GitHub and lose everything within minutes.


Creating the SPL Token

This is the foundation. You’re creating a new mint account on Solana that represents your token.

async function createToken(decimals: number = 9) {
  const mintKeypair = Keypair.generate();

  const mint = await createMint(
    connection,
    payer,
    payer.publicKey,  // mint authority
    payer.publicKey,  // freeze authority
    decimals,
    mintKeypair
  );

  console.log("Token created:", mint.toBase58());
  return { mint, mintKeypair };
}

I set decimals to 9 because that’s what SOL uses. Keeps things consistent. The mint authority controls who can create new tokens, and the freeze authority can freeze any token account. Both matter a lot later.


Minting the Supply

Once the mint exists, you need to actually create the tokens:

async function mintSupply(mint: PublicKey, amount: number) {
  const tokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    payer,
    mint,
    payer.publicKey
  );

  await mintTo(
    connection,
    payer,
    mint,
    tokenAccount.address,
    payer,
    amount * Math.pow(10, 9) // adjust for decimals
  );

  console.log(`Minted ${amount} tokens to ${tokenAccount.address.toBase58()}`);
  return tokenAccount;
}

I usually mint 1 billion tokens. That’s the standard for meme tokens. The number doesn’t really matter though — it’s all about the ratio in the liquidity pool.


Adding Metaplex Metadata

Without metadata, your token shows up as “Unknown Token” in wallets. Nobody’s buying that.

import { createCreateMetadataAccountV3Instruction } from "@metaplex-foundation/mpl-token-metadata";

async function addMetadata(mint: PublicKey, name: string, symbol: string, uri: string) {
  const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
    "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
  );

  const [metadataAccount] = PublicKey.findProgramAddressSync(
    [
      Buffer.from("metadata"),
      TOKEN_METADATA_PROGRAM_ID.toBuffer(),
      mint.toBuffer(),
    ],
    TOKEN_METADATA_PROGRAM_ID
  );

  const instruction = createCreateMetadataAccountV3Instruction(
    {
      metadata: metadataAccount,
      mint: mint,
      mintAuthority: payer.publicKey,
      payer: payer.publicKey,
      updateAuthority: payer.publicKey,
    },
    {
      createMetadataAccountArgsV3: {
        data: {
          name: name,
          symbol: symbol,
          uri: uri, // points to a JSON file with image, description
          sellerFeeBasisPoints: 0,
          creators: null,
          collection: null,
          uses: null,
        },
        isMutable: true,
        collectionDetails: null,
      },
    }
  );

  const transaction = new Transaction().add(instruction);
  await sendAndConfirmTransaction(connection, transaction, [payer]);

  console.log("Metadata added for", name);
}

The uri field points to a JSON file (usually on Arweave or IPFS) that contains the token image, description, and other details. I host mine on Arweave because it’s permanent. IPFS links break all the time.


Creating the Raydium Liquidity Pool

This is where it gets interesting. You need a liquidity pool so people can actually buy and sell the token.

Raydium’s AMM uses OpenBook (formerly Serum) for the order book. You need to create an OpenBook market first, then initialize the Raydium pool on top of it.

import { Liquidity, Token, TokenAmount, Percent } from "@raydium-io/raydium-sdk";

async function createPool(mintAddress: PublicKey, initialTokenAmount: number, initialSolAmount: number) {
  const baseToken = new Token(mintAddress, 9, "MYTOKEN", "MYTOKEN");
  const quoteToken = Token.WSOL;

  // Create the OpenBook market first
  const marketId = await createOpenBookMarket(connection, payer, mintAddress);

  // Initialize Raydium AMM pool
  const { transaction, signers } = await Liquidity.makeCreatePoolV4InstructionV2Simple({
    connection,
    programId: Liquidity.getProgramId(4),
    marketInfo: {
      marketId: marketId,
      programId: new PublicKey("srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX"),
    },
    baseMintInfo: { mint: mintAddress, decimals: 9 },
    quoteMintInfo: { mint: Token.WSOL.mint, decimals: 9 },
    baseAmount: new TokenAmount(baseToken, initialTokenAmount, false),
    quoteAmount: new TokenAmount(quoteToken, initialSolAmount, false),
    startTime: Math.floor(Date.now() / 1000),
    ownerInfo: {
      feePayer: payer.publicKey,
      wallet: payer.publicKey,
    },
    makeTxVersion: 0,
  });

  for (const tx of transaction) {
    await sendAndConfirmTransaction(connection, tx, [payer, ...signers]);
  }

  console.log("Pool created with", initialSolAmount, "SOL liquidity");
  return marketId;
}

I messed this up the first three times. The order matters — market first, then pool. And you have to wrap SOL into WSOL before adding it as liquidity. Raydium’s SDK handles most of it, but the errors are cryptic when something goes wrong.


The Full Launch Flow

Here’s where it all comes together. One function that does everything:

async function launchToken(
  name: string,
  symbol: string,
  metadataUri: string,
  totalSupply: number,
  liquidityTokens: number,
  liquiditySol: number,
  launchFeeSol: number
) {
  // Step 1: Collect the launch fee
  console.log(`Collecting ${launchFeeSol} SOL launch fee...`);
  // Fee transfer happens before anything else

  // Step 2: Create the token
  const { mint } = await createToken(9);

  // Step 3: Add metadata
  await addMetadata(mint, name, symbol, metadataUri);

  // Step 4: Mint supply
  const tokenAccount = await mintSupply(mint, totalSupply);

  // Step 5: Create pool and add liquidity
  await createPool(mint, liquidityTokens, liquiditySol);

  // Step 6: Revoke authorities (critical for trust)
  await revokeAuthorities(mint);

  console.log("Token launched successfully!");
  return mint;
}

That’s it. Six steps. The whole thing runs in about 30 seconds on mainnet.


Revoking Authorities — Don’t Skip This

This is the part most people get wrong. If you don’t revoke mint and freeze authorities, nobody will trust the token. They shouldn’t.

import { setAuthority, AuthorityType } from "@solana/spl-token";

async function revokeAuthorities(mint: PublicKey) {
  // Revoke mint authority — no one can create more tokens
  await setAuthority(
    connection,
    payer,
    mint,
    payer,
    AuthorityType.MintTokens,
    null
  );

  // Revoke freeze authority — no one can freeze token accounts
  await setAuthority(
    connection,
    payer,
    mint,
    payer,
    AuthorityType.FreezeAccount,
    null
  );

  console.log("Authorities revoked. Supply is fixed, accounts can't be frozen.");
}

Setting authority to null means nobody can ever mint more tokens or freeze accounts. It’s irreversible. That’s the point. Users check this on Solscan before buying, and if authorities aren’t revoked, they move on.


Monetization: Taking a Fee

The business model is dead simple. Charge a fee on every token launch.

import { SystemProgram } from "@solana/web3.js";

function createFeeInstruction(userWallet: PublicKey, feeLamports: number) {
  const platformWallet = new PublicKey("YourPlatformWalletAddressHere");

  return SystemProgram.transfer({
    fromPubkey: userWallet,
    toPubkey: platformWallet,
    lamports: feeLamports,
  });
}

// Add to the launch transaction
// 0.1 SOL fee = 100_000_000 lamports
const feeIx = createFeeInstruction(userWallet, 100_000_000);

I charge 0.1 SOL per launch. pump.fun charges around 0.02 SOL but makes it up on volume and trading fees. Start with a flat fee. You can always add percentage-based fees on the supply later.


A Simple API Endpoint

For the frontend, I expose a single endpoint that handles the entire launch:

// Express route handler
app.post("/api/launch", async (req, res) => {
  const { name, symbol, metadataUri, totalSupply, liquiditySol, userPublicKey } = req.body;

  try {
    // Build the transaction server-side
    const transaction = await buildLaunchTransaction({
      name,
      symbol,
      metadataUri,
      totalSupply,
      liquidityTokens: totalSupply * 0.8, // 80% to liquidity
      liquiditySol,
      userPublicKey: new PublicKey(userPublicKey),
    });

    // Return serialized transaction for wallet signing
    const serialized = transaction.serialize({
      requireAllSignatures: false,
    });

    res.json({
      transaction: Buffer.from(serialized).toString("base64"),
    });
  } catch (error) {
    res.status(500).json({ error: "Launch failed" });
  }
});

The user’s wallet signs the transaction on the frontend. You never touch their private key. I build the transaction server-side, serialize it, send it to the frontend, and let Phantom or Solflare handle the signing.

I put 80% of supply into the liquidity pool by default. The remaining 20% goes to the creator’s wallet. You can adjust this — some platforms do 100% to liquidity and charge higher fees instead.


Security Considerations

A few things I learned the hard way:

Always revoke authorities before marketing the token. I already covered this, but it’s worth repeating. It’s the single biggest trust signal.

Validate metadata URIs. Don’t let users point to arbitrary URLs. Require Arweave or IPFS hashes. Someone will try to link to a phishing site.

Rate limit launches. Without rate limiting, someone will spin up thousands of tokens to spam your platform. I cap it at 5 launches per wallet per hour.

Lock initial liquidity. If the creator can pull liquidity right after launch, that’s a rug pull waiting to happen. Use a time-lock or burn the LP tokens entirely.


I’m not going to pretend this is all sunshine. Most tokens launched on platforms like this go to zero. Many are outright scams. You’re building infrastructure that enables both legitimate projects and garbage.

I think there’s a real difference between building the tool and using it maliciously. But you should talk to a lawyer before launching something like this commercially. Securities laws vary by jurisdiction, and regulators are paying attention to token launch platforms specifically.

What I do: I require metadata, I auto-revoke authorities, and I burn LP tokens by default. It doesn’t prevent scams entirely, but it raises the floor. Bad actors will find other tools. At least mine makes some of the right things automatic.


What I’d Build Differently Now

If I started over, I’d skip Raydium V4 and go straight to Raydium concentrated liquidity (CLMM). The capital efficiency is way better, and the SDK has improved a lot since I first built this.

I’d also add token vesting for the creator’s allocation. Right now the creator gets their 20% immediately. A vesting schedule where tokens unlock over 30-90 days would add more legitimacy.

And I’d build the frontend with Next.js and the Solana wallet adapter from day one. I wasted time building a separate API when the wallet adapter handles transaction signing so cleanly on the client side.


The infrastructure for a token launch platform is surprisingly simple. A few hundred lines of code and you’ve got the core of a business that generates real revenue. The hard part isn’t the code. It’s building something people actually trust. Anyone can spin up a token factory. Getting users to choose yours over the dozen others — that takes reputation, transparency, and defaults that protect buyers. Focus on that, and the code will be the easy part.