Durable Nonce Fan-out for SWQoS

Background and usage scenario

In Solana transaction sending, slot progression, leader placement, and network-path congestion constantly change which route reaches the leader fastest. This is not specific to any RPC provider or sending service; it is a structural characteristic of Solana's execution model.
ERPC's SWQoS endpoint provides a sending path that injects transactions into the priority lane allocated by leaders based on Stake-weighted Quality of Service (SWQoS). This priority bandwidth is about 5× that of the non-priority lane and is applied before priority fee evaluation.
For that reason, the SWQoS endpoint is an important option for transaction sending, but in production a single endpoint is not always the fastest. Even within the same slot, transient path differences or load skew can allow another fast endpoint to lead.
Given these characteristics, an operational pattern of sending the same transaction to multiple fast paths in parallel and accepting the first one processed can be effective, rather than relying on a single route.
However, when the same transaction is sent to multiple endpoints, you cannot guarantee that it will execute only once without additional control. Fan-out without this control can lead to unintended double execution or broken retry control.
Solana provides Durable Nonce as the mechanism for this. Using Durable Nonce lets you send the same signed transaction across multiple routes while limiting on-chain execution to a single time.
This page explains how to implement fan-out operations that combine ERPC's SWQoS endpoint with other fast RPC endpoints, assuming transaction sending with Durable Nonce.

Scope and prerequisites

This guide covers creating a Durable Nonce account with web3.js and using it for transaction sending and fan-out operations.
Prerequisites to understand:
  • For Durable Nonce transactions, use the nonce value as recentBlockhash and place nonceAdvance as the first instruction
  • Once nonceAdvance executes, the nonce can be consumed even if later instructions fail. You cannot resend the same rawTx as-is.
  • Nonce account creation is a one-time setup and is typically reused

Step 1: Prepare the nonce authority and connection

The nonce authority is a Keypair that can advance the nonce.
ts
import { Connection, Keypair, SystemProgram, NONCE_ACCOUNT_LENGTH, } from '@solana/web3.js' const connection = new Connection( 'https://<primary-rpc-endpoint>', 'confirmed', ) const nonceAuthority = Keypair.fromSecretKey(/* secret key */)
  • The nonce authority can execute nonceAdvance
  • It can be the fee payer, or a separate keypair

Step 2: Generate a nonce account Keypair

A nonce account is a System Account.
ts
const nonceAccount = Keypair.generate()
This Keypair is used for:
  • Holding the nonce value (replacement for recentBlockhash)
  • Signing only at creation time
  • Safe storage afterward; it is not needed for daily sends

Step 3: Calculate the rent-exempt minimum

Nonce accounts must be rent-exempt. Do not hardcode values; fetch them from the RPC.
ts
const lamports = await connection.getMinimumBalanceForRentExemption( NONCE_ACCOUNT_LENGTH, )

Step 4: Create and initialize the nonce account

Create the nonce account with createAccount + nonceInitialize in a single transaction.
ts
import { Transaction } from '@solana/web3.js' const tx = new Transaction() tx.add( SystemProgram.createAccount({ fromPubkey: nonceAuthority.publicKey, newAccountPubkey: nonceAccount.publicKey, lamports, space: NONCE_ACCOUNT_LENGTH, programId: SystemProgram.programId, }), SystemProgram.nonceInitialize({ noncePubkey: nonceAccount.publicKey, authorizedPubkey: nonceAuthority.publicKey, }), ) // fee payer is the nonce authority tx.feePayer = nonceAuthority.publicKey // use a normal blockhash for initialization const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed') tx.recentBlockhash = blockhash // sign with both the new account and the authority tx.sign(nonceAccount, nonceAuthority) const signature = await connection.sendRawTransaction(tx.serialize()) await connection.confirmTransaction( { signature, blockhash, lastValidBlockHeight }, 'confirmed', )
Use this nonceAccount.publicKey for subsequent sends.

Step 5: Fetch the nonce before each send

For every transaction, fetch the current nonce value.
ts
import { NonceAccount } from '@solana/web3.js' const { value, context } = await connection.getAccountInfoAndContext( nonceAccount.publicKey, 'confirmed', ) if (!value) { throw new Error('Nonce account not found') } const nonce = NonceAccount.fromAccountData(value.data).nonce const minContextSlot = context.slot
  • Use nonce as recentBlockhash
  • context.slot is used in confirmation

Step 6: Build the Durable Nonce transaction

Durable Nonce transactions must satisfy the following conditions:
  • recentBlockhash = nonce
  • The first instruction is nonceAdvance
ts
import { TransactionMessage, VersionedTransaction, } from '@solana/web3.js' const instructions = [ SystemProgram.nonceAdvance({ noncePubkey: nonceAccount.publicKey, authorizedPubkey: nonceAuthority.publicKey, }), // add your real instructions after this ] const message = new TransactionMessage({ payerKey: nonceAuthority.publicKey, recentBlockhash: nonce, instructions, }).compileToV0Message() const tx = new VersionedTransaction(message) tx.sign([nonceAuthority /* + other signers */]) const rawTx = tx.serialize()
Notes:
  • If you use ComputeBudget instructions, they must come after nonceAdvance
  • If nonceAdvance is not first, the transaction will be rejected

Step 7: Fan out to multiple RPCs

Send the same rawTx concurrently.
ts
const endpoints = [ 'https://<erpc-swqos-fra>', 'https://<backup-rpc-1>', 'https://<backup-rpc-2>', ] const results = await Promise.allSettled( endpoints.map((url) => new Connection(url, 'confirmed').sendRawTransaction(rawTx, { skipPreflight: true, minContextSlot, }), ), ) const success = results.find( (r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled', ) if (!success) { throw new Error('All sends failed') } const signature = success.value
  • The signature is identical across endpoints
  • A successful send is not a confirmation

Step 8: Confirm with Durable Nonce

With Durable Nonce, include the nonce info in confirmation.
ts
await connection.confirmTransaction( { signature, nonceAccountPubkey: nonceAccount.publicKey, nonceValue: nonce, minContextSlot, }, 'confirmed', )
If confirmation succeeds:
  • The nonce advances
  • Copies sent to other RPCs return InvalidNonce

Next sends

  • After confirmation, fetch a fresh nonce
  • Do not reuse the same rawTx or nonce value
  • For parallel workflows, use separate nonce accounts
For the next transaction, fetch the updated nonce and build the transaction using the same steps.