@apps.fun/sdk
v0.2.0
Published
Official SDK for building token-gated applications on Solana and EVM chains using apps.fun bonding curves
Readme
@apps-fun/sdk
Official SDK for building token-gated applications on Solana using apps.fun bonding curves.
Table of Contents
- What is apps.fun?
- Prerequisites
- Installation
- Getting Started
- Core Concepts
- Quick Start Examples
- API Reference
- Security Best Practices
- Troubleshooting
- Migration Guide
- Templates
What is apps.fun?
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
Prerequisites
Before using the SDK, you need:
1. Solana Wallet
- 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)
2. RPC Endpoint
- Free tier:
https://api.mainnet-beta.solana.com(rate limited) - Recommended providers:
3. Node.js Environment
- Node.js 16+ required
- TypeScript recommended for better type safety
Installation
npm install @apps-fun/sdk @solana/web3.jsGetting Started
Step 1: Set Up Your Environment
Create 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.funStep 2: Create Your First Token Gate
import { 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;
}
}Step 3: Get a Token Mint Address
To use token gating, you need a token mint address. You have three options:
Option A: Use an Existing Token
Browse tokens on apps.fun and copy the mint address from any token page.
Option B: Create via Website
- 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
Option C: Create via API (Requires Auth)
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 detailsCore Concepts
Bonding Curves
Tokens 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
Token Decimals
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
Trading Fees
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
Graduation
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)
Quick Start Examples
Token Gating for Discord Bot
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);Trading Bot
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);Next.js API Route with Token Gate
// 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' });
}
}API Reference
TokenGate Class
Token gating functionality for controlling access based on token ownership.
Constructor
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
Methods
check(wallet: string | PublicKey): Promise<GateResult>
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
checkBatch(wallets: string[]): Promise<Map<string, GateResult>>
Check multiple wallets in parallel (optimized for performance).
Returns: Map with wallet addresses as keys and GateResult as values
clearCache(): void
Clear the internal balance cache. Useful after token transfers.
getTokenInfo()
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
}Direct Trading Functions
Execute trades directly on-chain without API authentication.
buyTokenDirect(connection, params): Promise<DirectTradeResult>
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
sellTokenDirect(connection, params): Promise<DirectTradeResult>
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
burnTokenDirect(connection, params): Promise<BurnResult>
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)
}Prepare/Submit Pattern (for Privy/External Wallets)
For wallets that don't expose private keys (Privy, hardware wallets).
prepareDirectBuy(connection, params): Promise<PreparedDirectTransaction>
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
}submitSignedTransaction(connection, signedTx): Promise<DirectTradeResult>
Submit externally signed transaction.
Parameters:
signedTx: string | Buffer | Uint8Array- Signed transaction
Pool Information Functions
getPoolInfo(connection, tokenMint): Promise<PoolInfo | null>
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%)
}verifyAppsFunPool(connection, tokenMint): Promise<boolean>
Verify a pool uses official apps.fun fee configuration.
Returns: true if pool has correct 1% fee split, false otherwise
getPoolFeeMetrics(connection, tokenMint): Promise<FeeMetrics>
Get fee collection metrics.
Returns:
{
accumulatedFees: bigint; // Total fees collected
claimableFees: bigint; // Fees ready to claim
lastClaim: Date | null; // Last claim timestamp
}AppsFunClient
API client for authenticated operations.
Constructor
new AppsFunClient(config?: AppsFunClientConfig)Parameters:
{
cluster?: 'mainnet-beta' | 'devnet' | 'testnet';
rpc?: string | Connection;
apiUrl?: string; // Default: https://apps.fun
}Methods (No Auth Required)
getQuote(params): Promise<QuoteResult>
Get price quote for a trade.
getMarketData(tokenMint): Promise<MarketData>
Get market data for a token.
getTokensByCreator(wallet): Promise<TokenInfo[]>
Get all tokens created by a wallet.
Methods (Auth Required)
These methods require authentication token from apps.fun login:
prepareTrade(params, authToken): Promise<PreparedTransaction>
submitTrade(params, authToken): Promise<TradeResult>
getEarnings(authToken): Promise<EarningsResult>
prepareTokenLaunch(params, authToken): Promise<PreparedLaunch>
Security Best Practices
🔴 CRITICAL: Private Key Management
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
Example: Secure Wallet Loading
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.');
}
}
}RPC Endpoint Security
- 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
Input Validation
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
}Troubleshooting
Common Issues and Solutions
"Transaction simulation failed"
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
});"blockhash not found"
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;
}
}
}"429 Too Many Requests"
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);"Account not found"
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.');
}"Custom program error: 0x1"
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}`);
}RPC Endpoint Issues
Choosing the Right RPC
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,
}
);Connection Pooling
// 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;
}Migration Guide
From pump.fun
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
}From Raydium/Orca
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,
});From Custom Token Gates
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);Templates
Ready-to-deploy applications:
Available Templates
| 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 |
Using Templates
# 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 devTemplate Configuration
Each template includes:
- Complete working application
- Environment variable setup
- Deployment instructions
- Best practices implementation
Testing
Unit Testing Your Token Gates
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);
});
});Testing on Devnet
- 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,
});Support
- Documentation: Full API docs
- Discord: Join community
- Issues: Report bugs
License
MIT
