Skip to main content
TLDR:
  • Staking SOL natively is a two-step flow: create and initialize a stake account you own, then delegate it to a validator’s vote account.
  • You build both transactions with @solana/web3.js (StakeProgram.createAccount and StakeProgram.delegate) and send them through a Chainstack Solana node.
  • Fund the stake account with the rent-exempt reserve plus the amount you want to stake, and stake at least the network minimum — currently 1 SOL (getStakeMinimumDelegation). Delegating less fails with InsufficientDelegation.
  • Delegation is not instant: your stake warms up and becomes active at the start of the next epoch, and rewards accrue from then on.

How delegation works on Solana

Solana uses delegated proof of stake. You do not hand SOL to a validator — instead you create a stake account that you fully control, then delegate the SOL held in it to a validator’s vote account. The validator produces and votes on blocks; you earn a share of the staking rewards, minus the validator’s commission. A stake account is a regular account owned by the Stake program, with three things that matter here:
  • Authorities — a staker (can delegate, deactivate, and split the stake) and a withdrawer (can withdraw the SOL and change either authority). You hold both, so you keep custody the whole time.
  • Rent-exempt reserve — like every Solana account, a stake account must hold a minimum balance to stay rent-exempt. This reserve sits on top of the delegated amount and is not staked.
  • Delegation — once you delegate, the account records the validator’s vote account, the delegated amount, and the epoch the stake activates in.
Two limits decide how much SOL the account needs:
  • Minimum delegation — the network rejects delegations below getStakeMinimumDelegation(), currently 1 SOL (1,000,000,000 lamports). Delegating less returns the Stake program error InsufficientDelegation (custom error 0xc).
  • Rent exemption — a 200-byte stake account needs 2,282,880 lamports (about 0.00228288 SOL) to be rent-exempt, from getMinimumBalanceForRentExemption(StakeProgram.space).
So a stake account that delegates the 1 SOL minimum must be funded with 2,282,880 + 1,000,000,000 = 1,002,282,880 lamports (about 1.00228288 SOL), plus a little SOL in your wallet for transaction fees.
Prerequisites
  • A Chainstack account and a deployed Solana node
  • Node.js 18 or above
  • A funded Solana wallet — a bit more than the amount you plan to stake, so around 1.05 SOL to stake the 1 SOL minimum and cover fees
This tutorial spends real SOL when run against mainnet. Test on devnet first — deploy a Solana devnet node, fund your wallet from a devnet faucet, and run the exact same code. The minimum delegation and rent-exempt reserve are identical on devnet and mainnet.

Deploy a Chainstack Solana node

Sign up with Chainstack

Deploy a node

View node access and credentials

Copy the node’s HTTPS and WSS endpoints from the Access and credentials tab. You pass both to the Connection constructor when you connect to your node.

Set up the project

Create a project directory and install @solana/web3.js. Add bs58 only if you import a base58 secret key (for example, one exported from a wallet like Phantom).
Shell
This tutorial uses CommonJS (require). Each step is a fragment that runs inside an async function so it can use await — the complete script assembles them into one runnable file. The same calls work unchanged with ES module import syntax.

Connect to your Solana node

Initialize a Connection with your Chainstack HTTPS and WSS endpoints and a commitment level. confirmed is a good default — it waits for the transaction to be confirmed by the cluster without waiting for full finalization. Pass the WSS endpoint explicitly. @solana/web3.js opens a WebSocket to confirm transactions; with only the HTTPS endpoint it derives a WebSocket URL from that host, which does not match a Chainstack node’s separate WSS endpoint, and sendAndConfirmTransaction then times out with TransactionExpiredBlockheightExceededError.
connection.js

Load your wallet

Your wallet pays the transaction fees, funds the stake account, and becomes the stake account’s staker and withdrawer authority. Load it with Keypair.fromSecretKey. Pick the form that matches how your key is stored:
wallet.js
Never hardcode or commit a secret key. Load it from a file outside your repository or from an environment variable, and use a dedicated wallet for testing.

Choose a validator

You delegate to a validator’s vote account, not its identity address. List active validators with getVoteAccounts and pick one:
validators.js
Choose deliberately — weigh commission, current stake, and reliability, and consider spreading stake across smaller validators to support decentralization. Explorers such as Solana Explorer, Solscan, and validators.app list vote accounts and performance history.

Create the stake account

Creating a stake account is one transaction that both allocates the account and initializes its authorities. StakeProgram.createAccount returns a ready-to-send Transaction containing two instructions — a System program createAccount and a Stake program Initialize. Fund it with the rent-exempt reserve plus the amount you want to stake, and set both authorities to your wallet with Authorized. Pass a Lockup of new Lockup(0, 0, PublicKey.default) for no lockup — a zero timestamp and epoch mean the stake is never locked, and the default custodian address is 11111111111111111111111111111111.
create-stake-account.js

Delegate the stake

With the stake account created and initialized, delegate it to your chosen validator’s vote account. StakeProgram.delegate returns a Transaction with a single Stake program DelegateStake instruction, signed by the staker authority — your wallet.
delegate.js
You can combine both instructions into a single transaction — add the createStakeAccountTx and delegateTx instructions to one Transaction — because the runtime processes them in order, so the delegate step runs against the just-initialized account. This tutorial keeps them separate for clarity.

Verify the delegation

Read the stake account back with getParsedAccountInfo. Right after delegating, the account type is delegated and deactivationEpoch is 18446744073709551615 (the maximum u64, meaning the stake is not scheduled to deactivate).
verify.js
The parsed output shows the authorities, the rent-exempt reserve, and the delegation. The example values here come from an existing on-chain stake account — yours will differ:
Example output
Your stake does not earn rewards immediately. It warms up and becomes fully active at the start of the epoch after activationEpoch. Track the current epoch with getEpochInfo. To unstake later, the staker authority calls StakeProgram.deactivate (the stake cools down over the next epoch), then the withdrawer calls StakeProgram.withdraw to move the SOL back out.

Full script

The complete flow, ready to run with node stake.js after setting your endpoint, key path, and validator vote account:
stake.js
The script prints two signatures and a delegated stake account whose delegation.voter matches your chosen validator’s vote account. Look either signature up on a Solana explorer to confirm it on-chain.

What’s next

Solana tooling

Libraries, clients, and tools for building on Solana with Chainstack.

Solana API methods

The full JSON-RPC method reference, including getVoteAccounts and getEpochInfo.
Last modified on July 10, 2026