A permissionless token launchpad on Solana using a bonding curve AMM. Token prices adjust algorithmically based on supply and demand — no order books, no presales, no pre-mint.
- How It Works
- Architecture
- On-Chain Program
- Frontend
- Getting Started
- Project Structure
- Configuration
- SDK Usage
- License
-
Create a Token — Anyone can launch a token by providing a name, symbol, and description. The frontend uploads the metadata to IPFS via Pinata, creating a Metaplex-compatible token metadata account on-chain. The program mints the full supply into a bonding curve vault. The creator deposits a small SOL seed (0.001 SOL) to initialize the curve's liquidity.
-
Buy — Users send SOL to the bonding curve. The curve mints tokens to the buyer based on a constant-product formula. The SOL accumulates in the curve PDA.
-
Sell — Users burn tokens back to the curve. The curve returns SOL from its reserves based on the same formula. The price decreases as tokens are sold back.
-
Graduate — When the SOL in the curve reaches the graduation threshold (default: 85 SOL), the token graduates. A migration instruction deposits the accumulated SOL and remaining tokens into a Raydium CPMM liquidity pool.
The bonding curve acts as an automated market maker (AMM). The price per token increases on buys and decreases on sells, creating organic price discovery without external liquidity providers.
┌─────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Create │ │ Buy │ │ Sell │ │
│ │ Token │ │ Token │ │ Token │ │
│ └────┬─────┘ └────┬─────┘ └────────┬──────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ Generated SDK │ │
│ │ (Codama) │ │
│ └─────────┬─────────┘ │
└──────────────────────┼───────────────────────────────┘
│ Solana RPC
┌──────────────────────┼───────────────────────────────┐
│ On-Chain Program │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ GlobalConfig (PDA) │ │
│ │ authority, fee_recipient, protocol_fee_bps, │ │
│ │ graduation_threshold, is_paused │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ BondingCurve (PDA per token) │ │
│ │ mint, real_token_reserve, real_sol_reserve, │ │
│ │ complete, pool_address │ │
│ └───────────────────┬──────────────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ Vault (ATA) │ ← holds token supply │
│ └───────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Metaplex Token Metadata (PDA per mint) │ │
│ │ name, symbol, uri, update_authority │ │
│ │ (wallets, explorers, and dApps read this) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
| Tool | Purpose |
|---|---|
| Pinocchio | Zero-dependency Solana smart contract framework. Replaces Anchor with raw syscall-level performance. No runtime overhead — programs compile to lean SBF bytecode. |
| Metaplex Token Metadata | On-chain standard for token metadata (name, symbol, URI). Created via CPI during token creation. Enables wallet detection and display. |
| Shank | Derive macros (#[derive(ShankType, ShankAccount)]) that extract instruction args and account structs from Rust source into a JSON IDL. |
| Codama | Reads the Shank IDL and generates a fully-typed TypeScript client (encoders, decoders, PDAs, error types). |
| bytemuck | Zero-copy serialization for #[repr(C)] structs. Accounts are read/written as typed references without deserialization overhead. |
| LiteSVM | Lightweight in-process Solana VM for fast program testing without a full validator. |
| Tool | Purpose |
|---|---|
| Next.js 15 | React framework (App Router) |
| React 19 | UI library |
| Tailwind CSS v4 | Utility-first CSS |
| @solana/web3.js | Solana RPC and transaction building |
| @solana/wallet-adapter | Wallet connection (Phantom, Solflare, etc.) |
| Pinata | IPFS pinning service for token metadata storage. Uploads JSON metadata (name, symbol, description) to IPFS before on-chain token creation. |
| pnpm | Package manager |
| Lucide React | Icons |
Shank inspects Rust structs annotated with #[derive(ShankType)] and #[derive(ShankAccount)] to produce a JSON IDL at idl/bonding_curve.json.
// src/instructions/buy_token.rs
#[repr(C, packed)]
#[derive(Zeroable, Pod, Copy, Clone, ShankType)] // ← Shank extracts this
pub struct BuyTokenArgs {
pub amount_to_buy: u64,
pub min_tokens_out: u64,
pub bump: u8,
}
// src/state/bonding_curve.rs
#[repr(C)]
#[derive(Zeroable, Pod, Clone, Copy)] // ← Account structs (no ShankAccount needed for bytemuck)
pub struct BondingCurve { ... }The generated IDL (idl/bonding_curve.json) contains:
- Instructions — discriminants, account layouts, arg types
- Accounts — struct field definitions with types and offsets
- Types — error enums, custom types
- Metadata — program address
Known Shank limitation: When instruction args are defined as standalone structs (like BuyTokenArgs), Shank puts them in the types section and may leave args empty. The fix is to manually copy the struct fields into the args array of each instruction in the IDL. This project has already been patched.
Codama reads the Shank IDL and generates a type-safe TypeScript client in app/src/sdk/.
cd app
npx tsx generate-sdk.mtsThe generator (app/generate-sdk.mts) uses @codama/nodes-from-anchor and @codama/renderers to produce:
app/src/sdk/
├── programs/
│ └── bondingCurve.ts # Program address constant
├── instructions/
│ ├── index.ts # Re-exports
│ ├── buyToken.ts # Encoder + types for BuyToken
│ ├── sellToken.ts # Encoder + types for SellToken
│ ├── createToken.ts # Encoder + types for CreateToken
│ └── ... # Other instructions
├── accounts/
│ ├── index.ts
│ ├── bondingCurve.ts # BondingCurve account decoder
│ └── globalConfig.ts # GlobalConfig account decoder
├── types/
│ └── bondingCurveError.ts # Error enum → TypeScript union
└── index.ts # Top-level re-exports
The frontend imports from this SDK for instruction encoding:
import { getBuyTokenInstructionDataEncoder } from "../src/sdk/instructions";Regenerate after any program change:
- Update
idl/bonding_curve.json(manually patch Shank's emptyargsif needed) - Run
npx tsx generate-sdk.mts - Run
pnpm exec tsc --noEmitto verify no type errors
Single instance. Stores protocol-wide settings.
| Field | Type | Description |
|---|---|---|
initialized |
u8 |
1 if initialized |
authority |
[u8; 32] |
Admin pubkey (can update config) |
total_token_supply |
[u8; 8] |
Total supply per token (raw, 1B × 10^6) |
initial_real_sol_deposit |
[u8; 8] |
SOL (lamports) creator deposits on creation |
fee_recipient |
[u8; 32] |
Receives protocol fees |
protocol_fee_bps |
[u8; 2] |
Fee in basis points (default: 100 = 1%) |
graduation_threshold_lamports |
[u8; 8] |
SOL threshold for graduation |
migration_dex_program |
[u8; 32] |
Target DEX for migration |
is_paused |
u8 |
Emergency pause flag |
bump |
u8 |
PDA bump |
_reserved |
[u8; 64] |
Reserved for future upgrades |
Size: 189 bytes
One per token. Stores curve state. Token metadata (name, symbol, URI) lives in the Metaplex Token Metadata account.
| Field | Type | Description |
|---|---|---|
mint |
[u8; 32] |
SPL token mint address |
token_owner |
[u8; 32] |
Creator pubkey |
real_token_reserve |
[u8; 8] |
Tokens held by the curve (raw units) |
real_sol_reserve |
[u8; 8] |
SOL in the curve (lamports) |
total_token_supply |
[u8; 8] |
Total supply (raw units, constant) |
created_at |
[u8; 8] |
Unix timestamp of creation |
complete |
u8 |
1 if graduated |
bump |
u8 |
PDA bump |
pool_address |
[u8; 32] |
Raydium pool address (zeroed until migrated) |
Size: 130 bytes
| Discriminant | Name | Description |
|---|---|---|
0 |
InitializeGlobalConfig |
Create the global config PDA |
1 |
CreateToken |
Launch a new token with bonding curve |
2 |
UpdateFeeRecipient |
Change fee recipient (admin only) |
3 |
UpdateBondingCurve |
Update supply/deposit params (admin only) |
4 |
ToggleTrading |
Pause/resume all trading (admin only) |
5 |
UpdateAuthority |
Transfer admin ownership (admin only) |
6 |
BuyToken |
Buy tokens from the bonding curve |
7 |
SellToken |
Sell tokens back to the bonding curve |
8 |
Migrate |
Graduate to Raydium DEX pool |
Accounts:
[signer, writable] creator
[] global_config
[writable] bonding_curve (PDA)
[signer, writable] mint
[writable] vault (curve's ATA)
[] system_program
[] token_program
[] associated_token_program
[writable] metadata (Metaplex Token Metadata PDA)
[] mpl_program (Metaplex Token Metadata program)
Args (variable-length after discriminator):
bump: u8
name_len: u8, name: [u8; name_len] ← UTF-8 token name (max 32 bytes)
symbol_len: u8, symbol: [u8; symbol_len] ← UTF-8 token symbol (max 10 bytes)
uri_len: u16, uri: [u8; uri_len] ← IPFS/Arweave metadata URI (max 200 bytes)
The program creates a Metaplex Token Metadata account via CPI during token creation. This enables wallets (Phantom, Solflare) and explorers to detect and display the token's name, symbol, and image.
The creator must have enough SOL for:
- Rent-exempt balance for the bonding curve PDA (~0.003 SOL)
- Rent-exempt balance for the mint account (~0.002 SOL)
- Initial SOL deposit (default: 0.001 SOL)
- Transaction fee
Accounts:
[signer, writable] buyer
[] mint
[writable] vault (curve's ATA)
[writable] buyer_ata
[writable] bonding_curve (PDA)
[] global_config
[writable] fee_recipient
[] system_program
[] token_program
[] associated_token_program
Args:
amount_to_buy: u64 ← SOL in lamports
min_tokens_out: u64 ← slippage protection (raw token units)
bump: u8
Accounts:
[signer, writable] seller
[] global_config
[writable] bonding_curve (PDA)
[writable] mint
[writable] seller_token_account
[writable] fee_recipient
[] token_program
Args:
amount: u64 ← tokens to sell (raw units)
min_sol_out: u64 ← slippage protection (lamports)
The program uses a constant-product formula with real reserves only (no virtual reserves).
Buy — given SOL input, calculate tokens received:
tokens_out = (sol_in × token_reserve) / (sol_reserve + sol_in)
Sell — given token input, calculate SOL returned:
sol_out = (tokens_in × sol_reserve) / (token_reserve + tokens_in)
Both formulas use u128 intermediate arithmetic to prevent overflow.
Since the sell formula only uses SOL that actually exists in the PDA, InsufficientReserves errors are impossible. The output is always ≤ the real SOL balance.
Example (with 0.001 SOL initial deposit and 1B token supply):
| Action | Input | Output |
|---|---|---|
| Buy | 0.1 SOL | ~90.9B tokens |
| Buy | 1 SOL | ~500B tokens |
| Sell | 100B tokens | ~0.000167 SOL |
- Protocol fee: 1% (100 basis points), configurable by admin
- Fee is calculated on each buy:
fee = sol_amount × protocol_fee_bps / 10000 - The full SOL amount transfers to the bonding curve PDA, then the fee portion moves to the fee recipient
- No fee on sell (seller receives full output minus slippage)
When real_sol_reserve ≥ graduation_threshold_lamports:
- The bonding curve's
completeflag is set to1 - Buying/selling is disabled on the curve
- The
Migrateinstruction can be called to:- Transfer remaining tokens and SOL to a Raydium CPMM pool
- Set
pool_addresson the bonding curve
Next.js 15 application with React 19, Tailwind CSS, and Solana wallet adapter.
| Route | Description |
|---|---|
/ |
Home — shows latest token launches with progress bars |
/create |
Token creation form (name, symbol, metadata) |
/explore |
Browse all launched tokens with search |
/trade/[address] |
Buy/sell interface for a specific token |
/admin |
Protocol admin panel (initialize, update config, pause) |
The TypeScript SDK is auto-generated by Codama from the IDL. Located in app/src/sdk/.
import { getBondingCurveAddress, getGlobalConfigAddress } from "./src/sdk/accounts";
import { getBuyTokenInstructionDataEncoder } from "./src/sdk/instructions";- Rust (latest stable)
- Solana CLI v2.x
- Node.js v20+
- pnpm (
npm install -g pnpm)
# Build for Solana SBF target
cargo build-sbf
# Output: target/deploy/bonding_curve.so# Run all tests (single-threaded for LiteSVM compatibility)
cargo test -- --test-threads=1# Generate a new program keypair (or use existing)
solana-keygen new -o target/deploy/bonding_curve-keypair.json
# Fund the deployer wallet
solana airdrop 2 --url devnet
# Upload buffer and deploy
BUFFER=$(solana program write-buffer target/deploy/bonding_curve.so --url devnet | grep Buffer: | awk '{print $2}')
solana program deploy --program-id target/deploy/bonding_curve-keypair.json --buffer $BUFFER --url devnetAfter deploying, initialize the global config via the admin page or with a script:
cd app
# Run the initialization (creates the global_config PDA)
# Then open /admin to configure fee recipient, thresholds, etc.
pnpm run devConnect the deployer wallet on the /admin page. The first connected wallet becomes the authority.
cd app
pnpm install
pnpm run devOpen http://localhost:3000.
If you modify the on-chain program's instructions or accounts:
cd app
# 1. Update idl/bonding_curve.json with the new struct definitions
# 2. Run the Codama generator
npx tsx generate-sdk.mtsaccel-Bond/
├── Cargo.toml # Rust workspace config
├── src/
│ ├── lib.rs # Program entrypoint (entrypoint! macro)
│ ├── constant.rs # Constants (seeds, fees, thresholds)
│ ├── errors.rs # Custom error enum
│ ├── utils.rs # Utility macros (check_ata!)
│ ├── state/
│ │ ├── mod.rs # State module exports
│ │ ├── bonding_curve.rs # BondingCurve struct + math
│ │ └── global_config.rs # GlobalConfig struct
│ ├── instructions/
│ │ ├── mod.rs # Instruction dispatcher
│ │ ├── init_global_config.rs # Create protocol config
│ │ ├── update_global_config.rs # Admin config updates
│ │ ├── create_token.rs # Token + curve creation
│ │ ├── buy_token.rs # Buy from curve
│ │ ├── sell_token.rs # Sell back to curve
│ │ └── migrate_to_raydium.rs # Graduate to DEX
│ └── tests/
│ ├── mod.rs # Core instruction tests
│ └── update_global_config.rs # Admin instruction tests
├── idl/
│ └── bonding_curve.json # Shank-generated IDL
├── app/
│ ├── package.json
│ ├── next.config.ts
│ ├── generate-sdk.mts # Codama SDK generator script
│ ├── lib/
│ │ └── solana.ts # Core Solana helpers + encoder
│ ├── app/
│ │ ├── layout.tsx # Root layout with wallet provider
│ │ ├── page.tsx # Home page
│ │ ├── create/page.tsx # Token creation
│ │ ├── explore/page.tsx # Token browser
│ │ ├── trade/[address]/page.tsx # Trade page
│ │ ├── admin/page.tsx # Admin panel
│ │ └── api/metadata/route.ts # Pinata IPFS metadata upload
│ ├── components/
│ │ ├── Header.tsx # Navigation header
│ │ └── WalletContextProvider.tsx # Wallet adapter setup
│ └── src/sdk/ # Auto-generated by Codama (DO NOT EDIT)
│ ├── index.ts
│ ├── programs/ # Program client
│ ├── instructions/ # Instruction encoders
│ ├── accounts/ # Account decoders
│ └── types/ # Shared types
└── .gitignore
| Parameter | Default | Description |
|---|---|---|
total_token_supply |
1,000,000,000 × 10^6 | Raw token supply per curve |
initial_real_sol_deposit |
0.001 SOL | SOL creator deposits on creation |
protocol_fee_bps |
100 (1%) | Fee on buys |
graduation_threshold_lamports |
85 SOL | SOL required for graduation |
is_paused |
false |
Emergency trading halt |
Token metadata is stored in a Metaplex Token Metadata account (not in the bonding curve). This ensures wallets and explorers can detect and display token information.
- Name: Up to 32 bytes (UTF-8)
- Symbol: Up to 10 bytes (UTF-8)
- URI: Up to 200 bytes (IPFS or Arweave link to JSON metadata)
- Decimals: 6 (standard SPL token)
- Update Authority: Token creator (can update metadata later)
The frontend uploads metadata JSON to IPFS via Pinata before creating the on-chain token.
NEXT_PUBLIC_SOLANA_RPC_URL=https://devnet.helius-rpc.com/?api-key=YOUR_KEY
PINATA_API_KEY=your_pinata_api_key
PINATA_SECRET_KEY=your_pinata_secret_keyPINATA_API_KEY and PINATA_SECRET_KEY are used for uploading token metadata JSON to IPFS. Get free keys from pinata.cloud.
import { Connection, PublicKey } from "@solana/web3.js";
import {
fetchAllBondingCurves,
getBondingCurveAddress,
getGlobalConfigAddress,
buyToken,
sellToken,
createToken,
} from "./lib/solana";
const connection = new Connection("https://api.devnet.solana.com");
// Fetch all launched tokens
const curves = await fetchAllBondingCurves(connection);
curves.forEach(c => {
console.log(`${c.name} (${c.symbol}) — ${c.mint}`);
console.log(` Liquidity: ${c.realSolReserve / 1e9} SOL`);
console.log(` Tokens available: ${c.realTokenReserve / 1e6}`);
});
// Get a specific curve
const { address } = await getBondingCurveAddress(mintPubkey);
const info = await connection.getAccountInfo(address);MIT