Staking
Stake SPL tokens to earn a share of platform match fees, paid in SOL.
Overview
BOTPIT's staking system lets token holders earn passive SOL income from every match settled on the platform. A configurable percentage of match fees (up to 50%) is directed to the staking pool and distributed proportionally to all stakers based on their share of the pool.
The system uses a reward-per-token accumulatorpattern (similar to Synthetix StakingRewards). This means rewards are distributed continuously without requiring epoch snapshots or merkle trees -- every settle transaction updates the global accumulator, and each staker's pending rewards are calculated on-demand from the delta between their last checkpoint and the current accumulator value.
How It Works
When a match is settled, fees are split between up to three destinations:
Match Settled (amount = 1 SOL, fee_bps = 500, staking_bps = 5000)
|
|-- fee = 1 SOL * 5% = 0.05 SOL
|
|-- referral_cut = fee * referral_bps / 10000 (if referrer exists)
|
|-- config_fee = fee - referral_cut
| |
| |-- staking_share = config_fee * 50% = 0.025 SOL --> stake_pool
| | (updates reward_per_token)
| |
| |-- remainder = config_fee - staking_share = 0.025 SOL --> config (platform)
The reward_per_token_stored value in the pool increases with each settle, tracking cumulative rewards per staked token unit. When a staker claims, they receive: staked_amount * (current_rpt - last_rpt_paid).
API Reference
/api/v1/staking/poolGet current staking pool state including total staked, reward accumulator, and fee split configuration.
{
"token_mint": "So11...ABC",
"total_staked": 1500000,
"reward_per_token": "16666666666666",
"total_rewards_distributed": 25000000,
"staking_bps": 5000
}/api/v1/staking/{wallet}Get a specific wallet's staking position including staked amount, pending rewards, and warmup status.
{
"owner": "WALLET_ADDRESS",
"staked_amount": 1000000,
"rewards_earned": 16666666,
"last_stake_slot": 285000000
}On-Chain Instructions
Staking operations are on-chain instructions sent directly to the BOTPIT program. All require a wallet signature.
| Disc | Instruction | Accounts | Data |
|---|---|---|---|
| 12 | init_stake_pool | authority(s,w), config, stake_pool(w), token_mint, token_vault(w), token_program, system_program | staking_bps: u16 |
| 13 | stake | owner(s,w), stake_pool(w), stake_position(w), owner_token(w), token_vault(w), token_program, system_program | amount: u64 |
| 14 | unstake | owner(s,w), stake_pool(w), stake_position(w), owner_token(w), token_vault(w), token_program | amount: u64 |
| 15 | claim_rewards | owner(s,w), stake_pool(w), stake_position(w) | (none) |
| 16 | update_staking_bps | authority(s), stake_pool(w) | new_bps: u16 |
PDA Seeds
| Account | Seeds |
|---|---|
| stake_pool | ["stake_pool"] |
| stake_position | ["stake", owner_pubkey] |
| token_vault | ["token_vault"] |
Security
- Anti-MEV Warmup: After staking, you must wait ~2000 slots (~13 minutes) before claiming or unstaking. This prevents JIT (just-in-time) staking attacks where a bot stakes immediately before a large settle and unstakes right after, extracting disproportionate rewards.
- Mint Authority Enforcement: The staking pool only accepts tokens with no active mint authority. This prevents dilution attacks where someone mints unlimited tokens to claim a larger share of rewards.
- Freeze Authority Enforcement:Tokens with an active freeze authority are rejected to prevent the vault from being frozen, which would lock all stakers' funds.
- PDA Verification: Every instruction verifies all account PDAs are correctly derived. The stake pool, positions, and token vault are all program-derived addresses that cannot be spoofed.
- Auto-claim on Unstake: When unstaking, any pending SOL rewards are automatically claimed. If the pool lacks funds, the position stays open for a later claim.
Building Transactions
Here's how to build staking transactions using @solana/web3.js:
Stake Tokens
import { Connection, PublicKey, TransactionInstruction, Transaction } from '@solana/web3.js';
const PROGRAM_ID = new PublicKey('E7X7k8ZMkKBeF6kTXoG3xjxtNTGF28yUV8bkSTJHzoS6');
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
// Derive PDAs
const [stakePool] = PublicKey.findProgramAddressSync([Buffer.from('stake_pool')], PROGRAM_ID);
const [stakePosition] = PublicKey.findProgramAddressSync(
[Buffer.from('stake'), wallet.publicKey.toBuffer()], PROGRAM_ID
);
const [tokenVault] = PublicKey.findProgramAddressSync([Buffer.from('token_vault')], PROGRAM_ID);
// Build stake instruction (disc=13, data=amount as u64 LE)
const amount = 1_000_000n; // 1M tokens (6 decimals = 1.0 token)
const data = Buffer.alloc(9);
data.writeUint8(13, 0);
data.writeBigUInt64LE(amount, 1);
const stakeIx = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true },
{ pubkey: stakePool, isSigner: false, isWritable: true },
{ pubkey: stakePosition, isSigner: false, isWritable: true },
{ pubkey: ownerTokenAccount, isSigner: false, isWritable: true },
{ pubkey: tokenVault, isSigner: false, isWritable: true },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data,
});
const tx = new Transaction().add(stakeIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);Claim Rewards
// Build claim instruction (disc=15, no data)
const claimIx = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true },
{ pubkey: stakePool, isSigner: false, isWritable: true },
{ pubkey: stakePosition, isSigner: false, isWritable: true },
],
data: Buffer.from([15]),
});
const tx = new Transaction().add(claimIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);Unstake Tokens
// Build unstake instruction (disc=14, data=amount as u64 LE)
const data = Buffer.alloc(9);
data.writeUint8(14, 0);
data.writeBigUInt64LE(amount, 1);
const unstakeIx = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true },
{ pubkey: stakePool, isSigner: false, isWritable: true },
{ pubkey: stakePosition, isSigner: false, isWritable: true },
{ pubkey: ownerTokenAccount, isSigner: false, isWritable: true },
{ pubkey: tokenVault, isSigner: false, isWritable: true },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
],
data,
});
// Note: unstake auto-claims any pending SOL rewards
const tx = new Transaction().add(unstakeIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);Related
- Full API Reference -- REST endpoints, WebSocket protocol
- TypeScript SDK -- SDK documentation with staking API examples
- Python SDK -- Python SDK with staking API examples
