Create and launch a token

Create and launch a token

This recipe walks the full path from "I have an idea and a wallet" to "my token is live on a DBC bonding curve with the first buy already in the book." Everything happens in one Jito bundle so either the entire launch lands or nothing does — no half-launched tokens, no orphaned mints.

The flow is identical whether you use the SDK or raw HTTP. The SDK collapses it into a single call; the HTTP version is shown side-by-side so you can see exactly what's happening under the hood.

What gets created

A single POST /v2/pools call builds two transactions that go into a single Jito bundle:

  1. Tx 1 — create the SPL mint, attach Metaplex metadata, initialize the DBC pool config, and create the DBC pool.
  2. Tx 2 — the optional dev-buy. If you pass buyAmount, this swaps SOL for GOLD and buys your own token through the curve in the same bundle.

The new pool is quoted in GOLD (GoLDDqDRHcGZBiGPeXAYi5ougndqBNQSNXdNeT3re6gr), starts at ~$1k market cap, and is set up to migrate to a Meteora DAMM v2 pool when it crosses ~$100k. See Pools for the lifecycle details.

Prerequisites

  • An API key with the * scope (or whatever your account ships with by default). Generate one at developer.piratecrew.fun.
  • A Solana wallet with enough SOL for rent + the optional dev-buy. Roughly 0.05 SOL for account creation, plus whatever you want to dev-buy with.
  • A PNG/JPEG image, base64-encoded as a data URI. It is run through an NSFW moderator before any transaction is built.
export PIRATE_API_KEY="pk_live_..."
export WALLET_SECRET_KEY="..."   # base58, hex, or JSON array

Track A — SDK one-liner

The SDK does the whole flow in one call. Install:

npm install @piratecrewfun/pirate-sdk @solana/web3.js
import { PirateSDK } from "@piratecrewfun/pirate-sdk";
import { readFileSync } from "node:fs";

const sdk = new PirateSDK({
  apiKey: process.env.PIRATE_API_KEY!,
  secretKey: process.env.WALLET_SECRET_KEY!,
  network: "mainnet-beta",
  rpcUrl: process.env.HELIUS_RPC_URL,
});

try {
  const image = readFileSync("./pearl.png").toString("base64");

  const { poolAddress, mintAddress, signatures, verification } =
    await sdk.createCoin({
      name: "Black Pearl",
      symbol: "PEARL",
      image: `data:image/png;base64,${image}`,
      description: "Captain Jack approved",
      buyAmount: 0.1,
      website: "https://blackpearl.xyz",
      x: "https://x.com/blackpearl",
      telegram: "https://t.me/blackpearl",
    });

  console.log("Pool:", poolAddress);
  console.log("Mint:", mintAddress);
  console.log("Signatures:", signatures);
  console.log("Verified:", verification.message);
} finally {
  sdk.destroy();
}

sdk.createCoin() does all of the following internally:

  1. POST /v2/pools to build the two unsigned transactions.
  2. Signs locally with your wallet plus the ephemeral base_mint_secret_key and config_secret_key returned by the API.
  3. POST /v2/transactions with bundle: true to land the bundle atomically.
  4. Polls GET /v2/pools/{address} until the resource flips to status: "live".

That is the entire launch. The rest of this page is what happens when you do it by hand.

Track B — Raw HTTP, step by step

1. Build the launch bundle

curl -X POST https://api.piratecrew.fun/v2/pools \
  -H "Authorization: Bearer $PIRATE_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "creator": "YOUR_WALLET_PUBKEY",
    "pool_params": {
      "name": "Black Pearl",
      "symbol": "PEARL",
      "image": "data:image/png;base64,iVBORw0KGgo...",
      "description": "Captain Jack approved",
      "buy_amount": 0.1,
      "website": "https://blackpearl.xyz",
      "x": "https://x.com/blackpearl",
      "telegram": "https://t.me/blackpearl"
    }
  }'

Response:

{
  "data": {
    "transactions": ["AQABAo...", "AQEDB..."],
    "base_mint": "MINT_ADDRESS",
    "config_address": "CONFIG_ADDRESS",
    "pool_address": "POOL_ADDRESS",
    "jito_tip_account": "TIP_ACCOUNT",
    "rpc_endpoint": "https://mainnet.helius-rpc.com/...",
    "required_signers": {
      "base_mint_secret_key": [/* 64 bytes */],
      "config_secret_key":    [/* 64 bytes */]
    }
  },
  "meta": { "request_id": "req_…" }
}

Two things to lock in:

  • transactions[] — base64 VersionedTransactions. Both must land in the same Jito bundle.
  • required_signers.*_secret_key — ephemeral 64-byte keypairs the server generated for the new mint and the pool config. You must co-sign with them; otherwise the bundle will fail with signature verification failed. They have no value after the pool is created.

2. Sign locally with three keypairs

Reconstruct both ephemeral keypairs and co-sign each transaction:

import { Keypair, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

const wallet = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_SECRET_KEY!));
const baseMint = Keypair.fromSecretKey(
  Uint8Array.from(response.data.required_signers.base_mint_secret_key),
);
const config = Keypair.fromSecretKey(
  Uint8Array.from(response.data.required_signers.config_secret_key),
);

const signed = response.data.transactions.map((b64: string) => {
  const tx = VersionedTransaction.deserialize(Buffer.from(b64, "base64"));
  tx.sign([wallet, baseMint, config]);
  return Buffer.from(tx.serialize()).toString("base64");
});

tx.sign() only adds signatures for keys that appear in the message's account list — extras are ignored — so passing all three to both transactions is safe even when one of them doesn't need the mint signer.

3. Submit the Jito bundle

curl -X POST https://api.piratecrew.fun/v2/transactions \
  -H "Authorization: Bearer $PIRATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "bundle": true, "signed_transactions": ["<tx1>", "<tx2>"] }'
{
  "data": {
    "bundle_id": "BUNDLE_ID",
    "signatures": ["sig1", "sig2"]
  },
  "meta": { "request_id": "req_…" }
}

Both signatures land in the same slot. If the bundle is dropped (e.g. tip too low), neither transaction is on-chain — retry the whole step with the same idempotency key on POST /v2/pools and you'll get the same pre-built bundle back.

4. Wait for the pool to flip to live

The pool resource progresses through its lifecycle automatically once the bundle confirms. Poll the resource (or subscribe to the pool.live webhook):

curl https://api.piratecrew.fun/v2/pools/POOL_ADDRESS \
  -H "Authorization: Bearer $PIRATE_API_KEY"

When the response shows "status": "live", the pool is listed in directory feeds and ready to trade.

5. Read pool state

The same GET /v2/pools/{address} response exposes the freshly minted base/quote reserves, the curve config, and your dev-buy reflected on the base side. Poll GET /v2/pools/POOL_ADDRESS/curve-progress for a 0..1 value to drive a migration progress bar.

Common gotchas

  • NSFW rejection. If the image fails moderation, the response is 400 with the offending category in error.message (violence, nudity, etc.). The fix is to swap the image — no transaction was built. The standalone POST /v2/tokens endpoint does not re-screen.
  • Name and symbol regex. name matches /^[a-zA-Z0-9 ]{1,20}$/; symbol matches /^[A-Za-z0-9]{1,10}$/. Hyphens, emojis, and underscores are rejected up front.
  • Forgetting the ephemeral keypairs. The most common integration bug is signing only with the wallet and then watching the bundle fail. The API returns base_mint_secret_key and config_secret_key for exactly this reason — sign with all three keys.
  • Insufficient tip. Jito tips are pulled from the SOL balance of creator. If the bundle keeps getting dropped, top up the wallet and retry — the create-pool response contains a jito_tip_account you can inspect.
  • Same idempotency key, different body. If you replay POST /v2/pools with the same Idempotency-Key but different params, you get a 409 conflict. Generate a new UUID per launch.

Next steps