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:
- Tx 1 — create the SPL mint, attach Metaplex metadata, initialize the DBC pool config, and create the DBC pool.
- 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 arrayTrack A — SDK one-liner
The SDK does the whole flow in one call. Install:
npm install @piratecrewfun/pirate-sdk @solana/web3.jsimport { 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:
POST /v2/poolsto build the two unsigned transactions.- Signs locally with your wallet plus the ephemeral
base_mint_secret_keyandconfig_secret_keyreturned by the API. POST /v2/transactionswithbundle: trueto land the bundle atomically.- Polls
GET /v2/pools/{address}until the resource flips tostatus: "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[]— base64VersionedTransactions. 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 withsignature 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
liveThe 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
400with the offending category inerror.message(violence,nudity, etc.). The fix is to swap the image — no transaction was built. The standalonePOST /v2/tokensendpoint does not re-screen. - Name and symbol regex.
namematches/^[a-zA-Z0-9 ]{1,20}$/;symbolmatches/^[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_keyandconfig_secret_keyfor 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 ajito_tip_accountyou can inspect. - Same idempotency key, different body. If you replay
POST /v2/poolswith the sameIdempotency-Keybut different params, you get a409 conflict. Generate a new UUID per launch.
Next steps
- Claim and split partner fees — once trading volume hits, route claims to the team in one tx.
- Pools — the full pool lifecycle, including DBC → DAMM v2 migration.
- Transaction Modes — why
create-poolis unsigned-only.