Solana Stake Program: stake, unstake, and merge
Practical guide to Solana’s native Stake program: create and delegate (stake), deactivate and withdraw (unstake), and merge (plus split). CLI, @solana/web3.js, RPC reads, and docs.
devrels.xyz/a/174short linkSolana Stake Program: native stake, unstake, and merge — without an LST.
Solana staking is not a single “stake button.” It is a small state machine on a native program. If you are building a wallet, a validator dashboard, a custody flow, or a bot that rebalances stake, three operations show up constantly: stake, unstake, and merge (with split as the reverse gear).
Program: Stake11111111111111111111111111111111111111. Deeper account layout and warmup math: Solana staking technical reference. This piece is the operator/builder path — what to call, in what order, with which tools.
Mental model in one screen
System wallet (liquid SOL)
│ create-stake-account + fund
▼
Stake account (initialized, undelegated)
│ delegate-stake → vote account
▼
Activating → Active (earns rewards while active)
│ deactivate-stake
▼
Deactivating → Inactive
│ withdraw-stake
▼
SOL back in a system account
merge-stake: B ──into──► A (A grows, B closes when empty)
split-stake: A ──part──► new accountTwo authorities matter on every stake account:
- Stake authority — can delegate, deactivate, split, merge, and re-authorize the staker
- Withdraw authority — can withdraw lamports and re-authorize the withdrawer (often the “owner” key in product UX)
They can be the same pubkey. Custody designs often separate them.
1. Stake (create + delegate)
“Staking” is two steps: put SOL in a stake account, then delegate that account to a validator’s vote account (not the validator identity key).
CLI
From Anza’s Staking SOL with the Solana CLI:
# one-time throwaway key for the stake account address
solana-keygen new --no-passphrase -o stake-account.json
# fund + initialize (stake + withdraw authority default to your CLI keypair)
solana create-stake-account stake-account.json 10 \
--stake-authority ~/.config/solana/id.json \
--withdraw-authority ~/.config/solana/id.json
# pick a vote account (second column of `solana validators`)
solana validators | head
solana delegate-stake <STAKE_ACCOUNT_ADDRESS> <VOTE_ACCOUNT_ADDRESS>
solana stake-account <STAKE_ACCOUNT_ADDRESS>After delegate you should see delegated stake, vote account, and an activation epoch. For normal-sized stakes, full activation is typically one epoch (~2 days at current mainnet timing) — see the warmup math notes.
Multiple validators ⇒ multiple stake accounts. Seeds help:
solana create-stake-account stake-base.json 5 --seed 0 ...
solana create-address-with-seed --from <BASE_PUBKEY> 0 STAKETypeScript (@solana/web3.js legacy StakeProgram)
import {
Connection, Keypair, PublicKey, StakeProgram, Authorized, Lockup,
Transaction, sendAndConfirmTransaction, LAMPORTS_PER_SOL,
} from "@solana/web3.js"
const connection = new Connection("https://api.mainnet-beta.solana.com")
const payer = /* Keypair */
const stakeAccount = Keypair.generate()
const vote = new PublicKey("<VALIDATOR_VOTE_ACCOUNT>")
const lamports = 10 * LAMPORTS_PER_SOL
const rent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)
const tx = new Transaction().add(
StakeProgram.createAccount({
fromPubkey: payer.publicKey,
stakePubkey: stakeAccount.publicKey,
authorized: new Authorized(payer.publicKey, payer.publicKey),
lockup: Lockup.default,
lamports: lamports + rent,
}),
StakeProgram.delegate({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: payer.publicKey,
votePubkey: vote,
}),
)
await sendAndConfirmTransaction(connection, tx, [payer, stakeAccount])StakeProgram.createAccount bundles system create + initialize. You still co-sign with the new stake account keypair once.
2. Unstake (deactivate + withdraw)
Native unstake is not instant. Two instructions, one wait:
- Deactivate — stake authority starts cooldown
- Wait until the stake is inactive (usually next epoch boundary for normal sizes)
- Withdraw — withdraw authority moves lamports out
CLI
solana deactivate-stake <STAKE_ACCOUNT_ADDRESS>
# after inactive:
solana stake-account <STAKE_ACCOUNT_ADDRESS> # confirm inactive / withdrawable
solana withdraw-stake <STAKE_ACCOUNT_ADDRESS> <RECIPIENT> ALL
# or: AVAILABLE — only the currently withdrawable portionDelinquent validator path (when the vote account is delinquent long enough): solana deactivate-stake --delinquent <STAKE> — see the same Anza CLI guide.
TypeScript
// start cooldown
await sendAndConfirmTransaction(
connection,
new Transaction().add(
StakeProgram.deactivate({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: payer.publicKey,
}),
),
[payer],
)
// later — after inactive — withdraw
const info = await connection.getAccountInfo(stakeAccount.publicKey)
// leave rent-exempt reserve if you keep the account open; or withdraw max
await sendAndConfirmTransaction(
connection,
new Transaction().add(
StakeProgram.withdraw({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: payer.publicKey,
toPubkey: payer.publicKey,
lamports: /* computed withdrawable */,
}),
),
[payer],
)Reading status
Prefer RPC helpers over guessing epochs:
// legacy web3 helper (where available on your connection stack)
const activation = await connection.getStakeActivation(stakePubkey)
// { state: "active" | "activating" | "deactivating" | "inactive",
// active, inactive }
// always works: parse stake account + solana stake-account CLI
// solana stake-account <ADDR> --output jsonProduct rule: show users deactivating until epoch N, not “unstake failed” while cooldown is working.
3. Merge
Merge folds a source stake account into a destination. Destination keeps the address; source is drained. Use it to clean up many small accounts on the same validator, or to consolidate after splits.
CLI
# merge SOURCE into DESTINATION (destination address is first arg)
solana merge-stake <DESTINATION_STAKE_ACCOUNT> <SOURCE_STAKE_ACCOUNT> \
--stake-authority <KEYPAIR>TypeScript
await sendAndConfirmTransaction(
connection,
new Transaction().add(
StakeProgram.merge({
stakePubkey: destinationStake, // kept
sourceStakePubKey: sourceStake, // merged away
authorizedPubkey: payer.publicKey,
}),
),
[payer],
)When merge works (compatibility checklist)
The Stake program rejects merges that would break activation accounting. In practice builders enforce:
- Same stake authority (and lockup rules must allow it)
- Compatible delegation: both inactive, or both delegated to the same vote account with compatible activation/deactivation epochs (two fully active stakes on one validator is the common happy path)
- You are not trying to merge two stakes that are mid-flight in conflicting warmup/cooldown states
If merge fails with a custom program error, split the problem: print both solana stake-account dumps and compare vote account, activation epoch, deactivation epoch, and authorities. Source of truth: Stake program merge checks in agave/programs/stake.
Split (the other half of merge)
Need two validators, or to free part of a stake while the rest stays active? Split moves lamports into a new stake account and copies delegation metadata when applicable.
solana split-stake <STAKE_ACCOUNT_ADDRESS> new-stake.json 3 \
--stake-authority <KEYPAIR>
# amount must respect cluster minimum delegation + rent-exempt on the new accountconst newStake = Keypair.generate()
const tx = StakeProgram.split(
{
stakePubkey: existingStake,
authorizedPubkey: payer.publicKey,
splitStakePubkey: newStake.publicKey,
lamports: 3 * LAMPORTS_PER_SOL,
},
rentExemptForStakeAccount,
)
await sendAndConfirmTransaction(connection, tx, [payer, newStake])Newer cluster features also expose MoveStake / MoveLamports for more atomic reallocation — treat them as advanced variants once you need them; merge/split cover most product UX. Instruction list overview: existing stake reference.
Builder checklist
- Resolve vote account addresses with
getVoteAccounts/solana validators— never confuse identity vs vote. - Track stake authority vs withdraw authority in your account model.
- After delegate/deactivate, poll activation state before offering withdraw or re-delegate.
- Merge only same-validator active pairs (or both inactive) unless you have tested the edge states.
- Respect minimum delegation on splits (
GetMinimumDelegationinstruction / cluster constant). - For liquid UX (instant exit), use an LST — native path always has a cooldown. See Sanctum LST mechanics.
Related RPC / APIs
getProgramAccounts(Stake1111…)with memcmp filters on withdrawer/staker — portfolio indexersgetVoteAccounts— validator list + activated stakegetStakeActivation/ stake account parse — UI stategetInflationReward— historical epoch rewards per stake account- CLI config JSON:
solana stake-account <ADDR> --output json
Resources
- Anza CLI — staking examples (create, delegate, deactivate, withdraw, split)
- Anza runtime — native programs
- Stake program source
- @solana/web3.js StakeProgram
- DevRels — stake account states & warmup math
- DevRels — Vote program
- DevRels — validators landscape
- DevRels — LSTs
Bottom line
Native Solana stake UX is three verbs: stake (create + delegate to a vote account), unstake (deactivate, wait, withdraw), and merge (plus split when you need another account). Everything else — LSTs, stake pools, restaking UIs — sits on top of that program. Learn the CLI once, mirror it in StakeProgram.*, and your wallet or agent will speak the same language as the runtime.
Keep reading
Stake accounts, the warmup/cooldown math, the four states, and the actual instruction shapes for delegate, deactivate, split, merge, and withdraw. The technical reference.
Solana's protocol votes used to be ad hoc SPL-token ballots coordinated off chain. As of July 2026 there is standing machinery: two mainnet programs, a fixed 11-epoch lifecycle, Merkle-proof-verified stake weights, and a staker sovereignty mechanism that lets delegators overrule their validator's vote with their own stake. Here is how the SGP system works end to end, how it differs from SIMDs, and what is already queued to vote.
On July 10, 2026 at 04:11 UTC, Solana mainnet ticked into epoch 1000. No resets, no rollbacks, one continuous chain since March 16, 2020. The record includes eight outages and a 17-hour night in September 2021, and it also includes 885 straight days of uptime since the last one. Here is the full timeline for builders: what an epoch actually is, what broke, what fixed it, and what the numbers look like at the milestone.
Get new articles in your inbox
Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.
