Official SDK for building token-gated applications on Solana using apps.fun bonding curves.
- What is apps.fun?
- Prerequisites
- Installation
- Getting Started
- Core Concepts
- Quick Start Examples
- API Reference
- Security Best Practices
- Troubleshooting
- Migration Guide
- Templates
apps.fun is a platform for launching tokens on Solana using bonding curves. Key features:
- Token-gated apps: Control access to your app based on token ownership
- Bonding curves: Automatic price discovery with Meteora Dynamic Bonding Curves
- Revenue sharing: 1% trading fee split between platform (0.5%) and creators (0.5%)
- Graduation: Tokens automatically migrate to Raydium when reaching ~$69k market cap
- No presales: Fair launch mechanism with transparent pricing
Before using the SDK, you need:
- Development: Generate a test wallet with
solana-keygen new - Production: Use hardware wallet (Ledger) or browser wallet (Phantom, Backpack)
- Funding: You need SOL for transaction fees (~0.01 SOL per transaction)
- Free tier:
https://api.mainnet-beta.solana.com(rate limited) - Recommended providers:
- Node.js 16+ required
- TypeScript recommended for better type safety
npm install @apps-fun/sdk @solana/web3.jsCreate a .env file:
# Required for trading operations
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
# For testing on devnet
# SOLANA_RPC_URL=https://api.devnet.solana.com
# Your wallet private key (NEVER commit this!)
# For development only - use environment variables in production
WALLET_PRIVATE_KEY=[your_private_key_array_or_base58]
# Optional: Custom apps.fun API URL
APPS_FUN_API_URL=https://apps.funimport { TokenGate } from '@apps-fun/sdk';
import { Connection, PublicKey } from '@solana/web3.js';
// Initialize connection
const connection = new Connection(process.env.SOLANA_RPC_URL!);
// Create a token gate
const gate = new TokenGate({
tokenMint: new PublicKey('YOUR_TOKEN_MINT_ADDRESS'),
minAmount: BigInt(1_000_000), // 1 token (tokens have 6 decimals)
connection,
cacheTtlMs: 30000, // Cache for 30 seconds
});
// Check if a wallet has access
async function checkAccess(walletAddress: string): Promise<boolean> {
const result = await gate.check(walletAddress);
if (result.allowed) {
console.log(`âś… Access granted! Balance: ${result.balance}`);
return true;
} else {
const needed = result.required - result.balance;
console.log(`❌ Need ${needed} more tokens for access`);
return false;
}
}To use token gating, you need a token mint address. You have three options:
Browse tokens on apps.fun and copy the mint address from any token page.
- Go to apps.fun
- Click "Launch Token"
- Fill in token details
- Pay ~0.02 SOL for creation
- Copy the mint address from your token page
import { AppsFunClient } from '@apps-fun/sdk';
const client = new AppsFunClient({ cluster: 'mainnet-beta' });
// This requires authentication through apps.fun website
// See "Token Creation" section for detailsTokens on apps.fun use Meteora Dynamic Bonding Curves for pricing:
- Price increases as more tokens are bought
- Price decreases as tokens are sold
- No liquidity provider needed - the curve IS the liquidity
- Instant trading - no order books or waiting
All tokens use 6 decimals (like USDC). This means:
1_000_000units = 1 token1_000units = 0.001 tokens- Always use
BigIntfor token amounts to avoid precision issues
Every trade has a 1% fee:
- 0.5% to apps.fun platform
- 0.5% to token creator
- Fees are taken from the output amount
- Example: Buy 100 tokens → receive 99 tokens
When a token reaches ~$69,000 market cap:
- Liquidity migrates to Raydium automatically
- Token becomes tradeable on all Solana DEXs
- Bonding curve closes (no more buys through apps.fun)
import { TokenGate } from '@apps-fun/sdk';
import { Client, GatewayIntentBits } from 'discord.js';
import { Connection, PublicKey } from '@solana/web3.js';
const TOKEN_MINT = 'YOUR_TOKEN_MINT_ADDRESS';
const MIN_TOKENS = BigInt(10_000_000); // 10 tokens
const gate = new TokenGate({
tokenMint: new PublicKey(TOKEN_MINT),
minAmount: MIN_TOKENS,
rpcUrl: process.env.SOLANA_RPC_URL,
cacheTtlMs: 60000, // Cache for 1 minute
});
const bot = new Client({ intents: [GatewayIntentBits.Guilds] });
bot.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
if (interaction.commandName === 'verify') {
// Get user's linked wallet (you need to implement this)
const walletAddress = await getUserWallet(interaction.user.id);
if (!walletAddress) {
return interaction.reply('Please link your wallet first!');
}
const result = await gate.check(walletAddress);
if (result.allowed) {
// Grant role
const role = interaction.guild?.roles.cache.find(r => r.name === 'Token Holder');
await interaction.member?.roles.add(role!);
return interaction.reply('âś… Verified! You have been granted access.');
} else {
return interaction.reply(
`❌ You need ${result.required - result.balance} more tokens to access this server.`
);
}
}
});
bot.login(process.env.DISCORD_TOKEN);import { buyTokenDirect, sellTokenDirect, getPoolInfo } from '@apps-fun/sdk';
import { Connection, Keypair } from '@solana/web3.js';
// Load wallet (NEVER hardcode private keys!)
const wallet = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.WALLET_PRIVATE_KEY!))
);
const connection = new Connection(process.env.SOLANA_RPC_URL!);
const TOKEN_MINT = 'YOUR_TOKEN_MINT_ADDRESS';
async function tradingBot() {
// Check pool info first
const poolInfo = await getPoolInfo(connection, TOKEN_MINT);
if (!poolInfo) {
console.error('Pool not found!');
return;
}
console.log(`Current price: ${poolInfo.currentPrice} SOL`);
console.log(`TVL: ${poolInfo.tvlSol} SOL`);
// Buy when price is below threshold
if (poolInfo.currentPrice < 0.001) {
try {
const result = await buyTokenDirect(connection, {
tokenMint: TOKEN_MINT,
amount: 0.1, // Buy with 0.1 SOL
wallet,
slippageBps: 100, // 1% slippage
priorityFee: 300_000, // Higher priority for faster execution
});
console.log(`Bought tokens: ${result.outputAmount}`);
console.log(`Transaction: https://solscan.io/tx/${result.signature}`);
} catch (error) {
console.error('Buy failed:', error);
}
}
// Sell when price is above threshold
if (poolInfo.currentPrice > 0.002) {
try {
const result = await sellTokenDirect(connection, {
tokenMint: TOKEN_MINT,
amount: 1000, // Sell 1000 tokens
wallet,
slippageBps: 100,
});
console.log(`Sold for ${result.outputAmount} SOL`);
console.log(`Transaction: https://solscan.io/tx/${result.signature}`);
} catch (error) {
console.error('Sell failed:', error);
}
}
}
// Run every 30 seconds
setInterval(tradingBot, 30000);// pages/api/premium-content.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { TokenGate } from '@apps-fun/sdk';
import { PublicKey } from '@solana/web3.js';
const gate = new TokenGate({
tokenMint: new PublicKey(process.env.NEXT_PUBLIC_TOKEN_MINT!),
minAmount: BigInt(process.env.MIN_TOKENS || '1000000'),
rpcUrl: process.env.SOLANA_RPC_URL,
cacheTtlMs: 60000,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { wallet } = req.query;
if (!wallet || typeof wallet !== 'string') {
return res.status(400).json({ error: 'Wallet address required' });
}
try {
const result = await gate.check(wallet);
if (!result.allowed) {
return res.status(403).json({
error: 'Insufficient tokens',
required: result.required.toString(),
balance: result.balance.toString(),
buyUrl: `https://apps.fun/token/${process.env.NEXT_PUBLIC_TOKEN_MINT}`,
});
}
// Return premium content
return res.json({
content: 'This is premium content only for token holders!',
data: {
// Your premium data here
},
});
} catch (error) {
console.error('Gate check failed:', error);
return res.status(500).json({ error: 'Failed to verify token ownership' });
}
}Token gating functionality for controlling access based on token ownership.
new TokenGate(config: TokenGateConfig)Parameters:
tokenMint: PublicKey- The SPL token mint address to checkminAmount: bigint- Minimum token balance required (in smallest units, 6 decimals)connection?: Connection- Optional Solana connection instancerpcUrl?: string- Alternative to connection, provide RPC URL directlycacheTtlMs?: number- Cache duration in milliseconds (default: 30000)appId?: number- Optional app ID for tracking
Check if a wallet meets the token requirement.
Returns:
{
allowed: boolean; // Whether access should be granted
balance: bigint; // Wallet's token balance
required: bigint; // Required minimum balance
message: string; // Human-readable status message
}Throws:
Errorif wallet address is invalidErrorif RPC connection fails
Check multiple wallets in parallel (optimized for performance).
Returns: Map with wallet addresses as keys and GateResult as values
Clear the internal balance cache. Useful after token transfers.
Get the gate configuration.
Returns:
{
mint: string; // Token mint address
minAmount: bigint; // Required balance
appId?: number; // App ID if set
graduated: boolean; // Whether token has graduated
}Execute trades directly on-chain without API authentication.
Buy tokens with SOL.
Parameters:
{
tokenMint: string | PublicKey; // Token to buy
amount: number; // SOL amount to spend
wallet: Keypair; // Wallet with private key
slippageBps?: number; // Max slippage (default: 100 = 1%)
priorityFee?: number; // Priority fee in microlamports (default: 200000)
}Returns:
{
success: boolean;
signature: string; // Transaction signature
inputAmount: string; // SOL spent (as string)
outputAmount: string; // Tokens received (as string)
}Throws:
Error('Amount must be positive')if amount <= 0Error('Slippage must be between 0 and 10000 basis points')if slippage invalidError('Priority fee must be non-negative')if fee < 0Error('Invalid token mint address')if mint is invalidError('Insufficient SOL balance')if wallet lacks funds- Network/RPC errors
Sell tokens for SOL.
Parameters:
{
tokenMint: string | PublicKey; // Token to sell
amount: number; // Token amount to sell
wallet: Keypair; // Wallet with private key
slippageBps?: number; // Max slippage (default: 100 = 1%)
priorityFee?: number; // Priority fee (default: 200000)
}Returns: Same as buyTokenDirect
Throws:
- Same validation errors as
buyTokenDirect Error('Insufficient token balance')if wallet lacks tokens
Permanently burn tokens to reduce supply.
Parameters:
{
tokenMint: string | PublicKey; // Token to burn
amount: number; // Amount to burn
wallet: Keypair; // Wallet with private key
priorityFee?: number; // Priority fee (default: 200000)
}Returns:
{
success: boolean;
signature: string; // Transaction signature
burnedAmount: string; // Amount burned (as string)
}For wallets that don't expose private keys (Privy, hardware wallets).
Build unsigned buy transaction.
Parameters:
{
tokenMint: string | PublicKey;
amount: number;
walletAddress: string | PublicKey; // Just public key, no private key
slippageBps?: number;
priorityFee?: number;
}Returns:
{
transaction: string; // Base64 encoded unsigned transaction
blockhash: string; // Recent blockhash used
lastValidBlockHeight: number; // Transaction expiry
estimatedOutput: string; // Expected tokens to receive
}Submit externally signed transaction.
Parameters:
signedTx: string | Buffer | Uint8Array- Signed transaction
Get detailed pool information.
Returns:
{
tokenMint: string;
currentPrice: number; // Current token price in SOL
tvlSol: number; // Total value locked in SOL
totalSupply: bigint; // Total token supply
circulatingSupply: bigint; // Circulating supply
isGraduated: boolean; // Has reached graduation
tradingFeePercent: number; // Fee percentage (1.0 = 1%)
}Verify a pool uses official apps.fun fee configuration.
Returns: true if pool has correct 1% fee split, false otherwise
Get fee collection metrics.
Returns:
{
accumulatedFees: bigint; // Total fees collected
claimableFees: bigint; // Fees ready to claim
lastClaim: Date | null; // Last claim timestamp
}API client for authenticated operations.
new AppsFunClient(config?: AppsFunClientConfig)Parameters:
{
cluster?: 'mainnet-beta' | 'devnet' | 'testnet';
rpc?: string | Connection;
apiUrl?: string; // Default: https://apps.fun
}Get price quote for a trade.
Get market data for a token.
Get all tokens created by a wallet.
These methods require authentication token from apps.fun login:
NEVER:
- Hardcode private keys in source code
- Commit private keys to git
- Share private keys in Discord/Telegram
- Store private keys in frontend code
- Log private keys
ALWAYS:
- Use environment variables for development
- Use secure key management (AWS KMS, HashiCorp Vault) in production
- Use hardware wallets for large amounts
- Rotate keys regularly
- Use separate wallets for testing
import { Keypair } from '@solana/web3.js';
import * as bs58 from 'bs58';
function loadWallet(): Keypair {
const key = process.env.WALLET_PRIVATE_KEY;
if (!key) {
throw new Error('WALLET_PRIVATE_KEY environment variable not set');
}
// Support both JSON array and base58 formats
try {
// Try JSON array format first
const secretKey = JSON.parse(key);
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
} catch {
// Try base58 format
try {
return Keypair.fromSecretKey(bs58.decode(key));
} catch (error) {
throw new Error('Invalid private key format. Use JSON array or base58.');
}
}
}- Don't expose RPC endpoints with credit cards attached in frontend
- Use read-only endpoints for public operations
- Implement rate limiting on your API routes
- Consider using RPC proxies for production
Always validate user inputs:
// Good
async function checkAccess(walletInput: unknown) {
// Validate input
if (typeof walletInput !== 'string') {
throw new Error('Invalid wallet address');
}
// Verify it's a valid Solana address
try {
new PublicKey(walletInput);
} catch {
throw new Error('Invalid Solana address format');
}
// Now safe to use
return gate.check(walletInput);
}
// Bad - no validation
async function checkAccess(wallet: any) {
return gate.check(wallet); // Could throw or behave unexpectedly
}Causes:
- Insufficient SOL for fees (need ~0.01 SOL)
- Token account doesn't exist (need ~0.002 SOL to create)
- Slippage too low for volatile tokens
Solution:
// Ensure wallet has enough SOL
const balance = await connection.getBalance(wallet.publicKey);
if (balance < 0.01 * LAMPORTS_PER_SOL) {
throw new Error('Insufficient SOL for fees');
}
// Increase slippage for volatile tokens
const result = await buyTokenDirect(connection, {
tokenMint,
amount: 0.1,
wallet,
slippageBps: 500, // 5% slippage for volatile tokens
});Cause: Transaction took too long to submit (blockhash expired).
Solution:
// Retry with fresh blockhash
async function retryTransaction(fn: () => Promise<any>, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error: any) {
if (error.message?.includes('blockhash not found') && i < maxRetries - 1) {
console.log(`Retry ${i + 1}/${maxRetries}...`);
await new Promise(r => setTimeout(r, 1000));
continue;
}
throw error;
}
}
}Cause: RPC rate limiting.
Solutions:
- Use a paid RPC provider (Helius, QuickNode)
- Implement request throttling:
import { TokenGate } from '@apps-fun/sdk';
// Batch checks to reduce RPC calls
const gate = new TokenGate({
tokenMint: new PublicKey(TOKEN_MINT),
minAmount: BigInt(1000000),
rpcUrl: process.env.SOLANA_RPC_URL,
cacheTtlMs: 60000, // Cache for 1 minute
});
// Check multiple wallets in one call
const wallets = ['wallet1', 'wallet2', 'wallet3'];
const results = await gate.checkBatch(wallets);Cause: Token account doesn't exist for the wallet.
Solution:
import { getAssociatedTokenAddress } from '@solana/spl-token';
// Check if token account exists
const tokenAccount = await getAssociatedTokenAddress(
new PublicKey(tokenMint),
wallet.publicKey
);
try {
await connection.getAccountInfo(tokenAccount);
} catch {
console.log('Token account does not exist. Will be created on first trade.');
}Cause: Insufficient token balance for sell/burn operations.
Solution:
// Check balance before selling
const gate = new TokenGate({ tokenMint, minAmount: BigInt(0), connection });
const result = await gate.check(wallet.publicKey.toString());
if (result.balance < amountToSell) {
throw new Error(`Insufficient balance. Have: ${result.balance}, Need: ${amountToSell}`);
}Development:
// Free tier - good for testing
const connection = new Connection('https://api.devnet.solana.com');Production:
// Paid RPC with higher limits
const connection = new Connection(
`https://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`,
{
commitment: 'confirmed',
confirmTransactionInitialTimeout: 60000,
}
);// Reuse connections for better performance
let connection: Connection | null = null;
function getConnection(): Connection {
if (!connection) {
connection = new Connection(process.env.SOLANA_RPC_URL!, {
commitment: 'confirmed',
disableRetryOnRateLimit: false,
});
}
return connection;
}Key differences:
- apps.fun uses 6 decimals (pump.fun uses 9)
- Different fee structure (1% vs 1%)
- Different graduation threshold ($69k vs $100k)
// pump.fun (9 decimals)
const amount = 1_000_000_000; // 1 token
// apps.fun (6 decimals)
const amount = 1_000_000; // 1 token
// Migration helper
function convertPumpToApps(pumpAmount: bigint): bigint {
return pumpAmount / BigInt(1000); // Convert 9 decimals to 6
}If you're using standard DEX trading:
// Before: Using Raydium SDK
import { Liquidity } from '@raydium-io/raydium-sdk';
// After: Using apps.fun SDK
import { buyTokenDirect } from '@apps-fun/sdk';
// Simpler API, no pool setup needed
const result = await buyTokenDirect(connection, {
tokenMint,
amount: 0.1,
wallet,
});Replace custom balance checking:
// Before: Manual balance checking
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
wallet,
{ mint: tokenMint }
);
const balance = tokenAccounts.value[0]?.account.data.parsed.info.tokenAmount.uiAmount || 0;
// After: Using TokenGate
const gate = new TokenGate({ tokenMint, minAmount, connection });
const result = await gate.check(wallet);Ready-to-deploy applications:
| Template | Description | Features |
|---|---|---|
nextjs-privy |
Next.js with Privy wallet | Email login, embedded wallets, token gating |
discord-bot |
Discord bot with roles | Auto-role assignment, verification commands |
express-api |
REST API server | Token-gated endpoints, caching, rate limiting |
# Clone template
cp -r node_modules/@apps-fun/sdk/templates/nextjs-privy my-app
cd my-app
# Install dependencies
npm install
# Configure environment
cp .env.example .env.local
# Edit .env.local with your values
# Run development server
npm run devEach template includes:
- Complete working application
- Environment variable setup
- Deployment instructions
- Best practices implementation
import { TokenGate } from '@apps-fun/sdk';
import { Connection, PublicKey } from '@solana/web3.js';
describe('Token Gate', () => {
let gate: TokenGate;
beforeAll(() => {
gate = new TokenGate({
tokenMint: new PublicKey('YOUR_TOKEN_MINT'),
minAmount: BigInt(1_000_000),
rpcUrl: 'https://api.devnet.solana.com',
});
});
test('should allow access with sufficient balance', async () => {
// Use a known wallet with tokens on devnet
const result = await gate.check('WALLET_WITH_TOKENS');
expect(result.allowed).toBe(true);
});
test('should deny access with insufficient balance', async () => {
// Random wallet with no tokens
const result = await gate.check('11111111111111111111111111111111');
expect(result.allowed).toBe(false);
});
});- Get devnet SOL:
solana airdrop 2 YOUR_WALLET_ADDRESS --url devnet- Use devnet tokens for testing:
const connection = new Connection('https://api.devnet.solana.com');
// Test with devnet tokens
const result = await buyTokenDirect(connection, {
tokenMint: 'DEVNET_TOKEN_MINT',
amount: 0.1,
wallet: testWallet,
});- Documentation: Full API docs
- Discord: Join community
- Issues: Report bugs
MIT