Skip to main content
TLDR:
  • Normal Solana transactions expire after ~80 seconds (150 slots) because they reference a recent blockhash.
  • Durable nonces replace the recent blockhash with a stored nonce value, removing the expiry window entirely.
  • The nonce must be advanced as the first instruction in the transaction.
  • This enables offline signing, scheduled transactions, and multi-party approval flows.

The problem: blockhash expiry

Every Solana transaction includes a recentBlockhash — a reference to a recent block that acts as a timestamp and replay-prevention mechanism. If the transaction is not confirmed within ~150 slots (~80 seconds), it expires and is dropped. This is a problem when:
  • Offline signing — the signer is air-gapped and can’t access the network in real time
  • Multi-sig approval — multiple parties need to sign, which takes longer than 80 seconds
  • Scheduled execution — you want to build a transaction now and submit it later
  • Custodial workflows — approval chains in enterprise systems

How durable nonces work

A nonce account is a special System Program account that stores:
  • authority — the pubkey authorized to advance the nonce
  • durable nonce — a hash value derived from a recent blockhash
  • lamports per signature — the fee rate when the nonce was last advanced
Instead of a recent blockhash, the transaction uses the nonce value. The nonce does not expire — it stays valid until someone advances it. The lifecycle:
  1. Create and initialize a nonce account
  2. Build the transaction with AdvanceNonceAccount as the first instruction and the nonce value as the recentBlockhash
  3. Sign the transaction (can be done offline, days or weeks later)
  4. Submit when ready — the nonce is advanced atomically, preventing replay
The AdvanceNonceAccount instruction must be the first instruction in the transaction. If it’s not at index 0, the transaction is processed as a regular (non-nonce) transaction and will fail if the blockhash is stale.

Get your own node endpoint today

Start for free and get your app to production levels immediately. No credit card required.You can sign up with your GitHub, X, Google, or Microsoft account.

Prerequisites

The examples are shown in both Solana JavaScript libraries — both are actively maintained, so use whichever fits your project. @solana/kit is the newer, tree-shakable, functional SDK; @solana/web3.js is the classic Connection/PublicKey API.
npm install @solana/kit @solana-program/system dotenv

Create a nonce account

TypeScript
import {
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createKeyPairSignerFromBytes,
  getBase58Encoder,
  generateKeyPairSigner,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
} from "@solana/kit";
import {
  getCreateAccountInstruction,
  getInitializeNonceAccountInstruction,
  getNonceSize,
  fetchNonce,
  SYSTEM_PROGRAM_ADDRESS,
} from "@solana-program/system";
import "dotenv/config";

const rpc = createSolanaRpc(process.env.SOLANA_RPC!);
const rpcSubscriptions = createSolanaRpcSubscriptions(process.env.SOLANA_WSS!);
// @solana/kit decodes base58, so no separate bs58 package is needed
const payer = await createKeyPairSignerFromBytes(
  getBase58Encoder().encode(process.env.PRIVATE_KEY!)
);

async function createNonceAccount() {
  const nonceAccount = await generateKeyPairSigner();
  const space = BigInt(getNonceSize()); // nonce accounts are 80 bytes
  const lamports = await rpc.getMinimumBalanceForRentExemption(space).send();

  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
    (m) => appendTransactionMessageInstructions([
      getCreateAccountInstruction({
        payer,
        newAccount: nonceAccount,
        lamports,
        space,
        programAddress: SYSTEM_PROGRAM_ADDRESS,
      }),
      getInitializeNonceAccountInstruction({
        nonceAccount: nonceAccount.address,
        nonceAuthority: payer.address, // nonce authority
      }),
    ], m),
  );

  const signedTx = await signTransactionMessageWithSigners(message);
  await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTx, {
    commitment: "confirmed",
  });
  console.log("Nonce account:", nonceAccount.address);
  console.log("Transaction:", getSignatureFromTransaction(signedTx));

  // Fetch the nonce value
  const nonce = await fetchNonce(rpc, nonceAccount.address);
  console.log("Nonce value:", nonce.data.blockhash);
}

createNonceAccount();

Build and sign a transaction with a durable nonce

TypeScript
import {
  createSolanaRpc,
  createKeyPairSignerFromBytes,
  getBase58Encoder,
  address,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingDurableNonce,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  getBase64EncodedWireTransaction,
  lamports,
} from "@solana/kit";
import { getTransferSolInstruction, fetchNonce } from "@solana-program/system";
import "dotenv/config";

const rpc = createSolanaRpc(process.env.SOLANA_RPC!);
const payer = await createKeyPairSignerFromBytes(
  getBase58Encoder().encode(process.env.PRIVATE_KEY!)
);

const NONCE_ACCOUNT = address("YOUR_NONCE_ACCOUNT_ADDRESS");

async function buildNonceTransaction() {
  // Fetch the current nonce value
  const nonce = await fetchNonce(rpc, NONCE_ACCOUNT);

  // setTransactionMessageLifetimeUsingDurableNonce prepends the required
  // AdvanceNonceAccount instruction as the first instruction automatically.
  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) => setTransactionMessageLifetimeUsingDurableNonce({
      nonce: nonce.data.blockhash,
      nonceAccountAddress: NONCE_ACCOUNT,
      nonceAuthorityAddress: payer.address,
    }, m),
    (m) => appendTransactionMessageInstructions([
      // Your actual instruction(s)
      getTransferSolInstruction({
        source: payer,
        destination: payer.address,
        amount: lamports(1000n),
      }),
    ], m),
  );

  // Sign — this can be done offline
  const signedTx = await signTransactionMessageWithSigners(message);

  // Serialize for later submission
  const base64Tx = getBase64EncodedWireTransaction(signedTx);
  console.log("Signed transaction (base64):", base64Tx);
  console.log("This transaction will not expire. Submit it whenever ready.");

  return base64Tx;
}

buildNonceTransaction();

Submit the transaction later

TypeScript
import { createSolanaRpc } from "@solana/kit";
import "dotenv/config";

const rpc = createSolanaRpc(process.env.SOLANA_RPC!);

async function submitNonceTransaction(base64Tx: string) {
  // Broadcast the stored, serialized transaction as-is
  const signature = await rpc
    .sendTransaction(base64Tx, { encoding: "base64", preflightCommitment: "confirmed" })
    .send();
  console.log("Submitted:", signature);

  // Poll until the transaction is confirmed. A durable-nonce transaction is
  // valid until the nonce is advanced, so there is no blockhash to expire.
  while (true) {
    const { value } = await rpc.getSignatureStatuses([signature]).send();
    const status = value[0];
    if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
      console.log("Confirmed:", status.err ? "FAILED" : "SUCCESS");
      break;
    }
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

// Paste the base64 from the previous step
submitNonceTransaction("YOUR_BASE64_TX_HERE");

Failure behavior

Understanding how nonce transactions fail is important:
  • Validation failure (nonce already used, authority not signed, account not found) — the entire transaction is dropped. No fees collected, no state changes.
  • Execution failure (an instruction returns an error after validation passes) — the nonce is still advanced and fees are still collected. This prevents replay while ensuring the validator is compensated.

Additional resources

Last modified on July 4, 2026