Skip to main content

Overview

The Node SDK supports two approaches to SVM gas sponsorship:
  1. Dynamic sponsorship — pass sponsor: true to signTransaction(). Dynamic replaces the fee payer with a sponsor wallet. No funded wallet required on your end.
  2. Custom fee payer (delegated access) — your server holds a funded Solana wallet and signs as the fee payer alongside the user’s delegated wallet.

Dynamic sponsorship (simplest)

Pass sponsor: true to signTransaction() on DynamicSvmWalletClient. Dynamic’s API replaces the fee payer before signing.
Gas sponsorship must be enabled for your environment in the Dynamic dashboard.
import { DynamicSvmWalletClient } from '@dynamic-labs-wallet/node-svm';
import { decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import { Connection, Transaction, SystemProgram, PublicKey } from '@solana/web3.js';

const svmClient = await authenticatedSvmClient();
const connection = new Connection(process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com');

const { blockhash } = await connection.getLatestBlockhash();

const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(walletMetadata.accountAddress),
    toPubkey: new PublicKey(recipientAddress),
    lamports: 1000000,
  })
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(walletMetadata.accountAddress);

const signatureBase58 = await svmClient.signTransaction({
  walletMetadata,
  externalServerKeyShares,
  transaction,
  sponsor: true,
});

const signatureBytes = decodeBase58(signatureBase58);
const signedTx = addSignatureToTransaction({
  transaction,
  signature: signatureBytes,
  signerPublicKey: new PublicKey(walletMetadata.accountAddress),
});

const txid = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(txid, 'confirmed');
For more control, call sponsorTransaction() directly to inspect or modify the sponsored transaction before signing.

Custom fee payer with delegated access

Use delegated access to sponsor gas fees for user-initiated Solana transactions. Your server signs transactions on behalf of users while paying the transaction fees.
Your server wallet acts as the fee payer for transactions while the user’s delegated wallet signs the transaction instructions.

Prerequisites

  • A funded Solana wallet for paying gas fees
  • Dynamic server API key
  • Environment variables configured
  • @dynamic-labs-wallet/node-svm package installed
npm install @dynamic-labs-wallet/node-svm @solana/web3.js @solana/spl-token bs58 tweetnacl

Delegated Access: Sponsor gas for user wallets

Use delegated credentials to sign transactions on behalf of users while your server pays the gas fees.

1) Create a delegated client and set up fee payer

import { createDelegatedSvmWalletClient } from '@dynamic-labs-wallet/node-svm';
import { Connection, Keypair } from '@solana/web3.js';
import bs58 from 'bs58';

const ENVIRONMENT_ID = process.env.DYNAMIC_ENVIRONMENT_ID as string;
const SERVER_API_KEY = process.env.DYNAMIC_SERVER_API_KEY as string;
const FEE_PAYER_PRIVATE_KEY = process.env.FEE_PAYER_PRIVATE_KEY as string;
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';

const delegatedClient = createDelegatedSvmWalletClient({
  environmentId: ENVIRONMENT_ID,
  apiKey: SERVER_API_KEY,
});

const connection = new Connection(RPC_URL, 'confirmed');
const feePayer = Keypair.fromSecretKey(bs58.decode(FEE_PAYER_PRIVATE_KEY));

2) Prepare delegation credentials from webhook

When a user delegates access, you receive encrypted credentials via webhook. Decrypt and prepare them:
import { decryptHybridRsaAes256 } from './webhook-handler';

async function prepareDelegationCredentials(webhookPayload: DelegationWebhookPayload) {
  const decryptedShare = await decryptHybridRsaAes256(
    webhookPayload.data.encryptedDelegatedShare
  );

  const walletApiKey = await decryptHybridRsaAes256(
    webhookPayload.data.encryptedWalletApiKey
  );

  const keyShareData = JSON.parse(decryptedShare);
  const pubkeyBytes = bs58.decode(webhookPayload.data.publicKey);

  return {
    walletId: webhookPayload.data.walletId,
    walletApiKey,
    keyShare: {
      pubkey: { pubkey: pubkeyBytes },
      secretShare: keyShareData.secretShare,
    },
    publicKey: webhookPayload.data.publicKey,
  };
}

3) Sign transaction with delegated wallet and fee payer

import { delegatedSignTransaction } from '@dynamic-labs-wallet/node-svm';
import { Transaction, SystemProgram, PublicKey, VersionedTransaction } from '@solana/web3.js';
import nacl from 'tweetnacl';

const credentials = await prepareDelegationCredentials(webhookPayload);
const sender = new PublicKey(credentials.publicKey);

const transaction = new Transaction();
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');

transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.feePayer = feePayer.publicKey;

transaction.add(
  SystemProgram.transfer({
    fromPubkey: sender,
    toPubkey: new PublicKey(recipientAddress),
    lamports: 1000000,
  })
);

let signedTransaction = await delegatedSignTransaction(delegatedClient, {
  walletId: credentials.walletId,
  walletApiKey: credentials.walletApiKey,
  keyShare: credentials.keyShare,
  transaction,
  signerAddress: sender.toString(),
});

if (signedTransaction instanceof VersionedTransaction) {
  signedTransaction.sign([feePayer]);
} else {
  const message = signedTransaction.serializeMessage();
  const feePayerSignature = nacl.sign.detached(message, feePayer.secretKey);
  signedTransaction.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
}

const signature = await connection.sendRawTransaction(
  signedTransaction.serialize(),
  { skipPreflight: false, maxRetries: 3 }
);

await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight });

console.log('Transaction signature:', signature);

SPL Token Transfers with Gas Sponsorship

import {
  createTransferCheckedInstruction,
  getAssociatedTokenAddress,
  createAssociatedTokenAccountInstruction,
} from '@solana/spl-token';

const USDC_MINT = new PublicKey('Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr');
const sender = new PublicKey(credentials.publicKey);
const recipient = new PublicKey(recipientAddress);

const senderTokenAccount = await getAssociatedTokenAddress(USDC_MINT, sender);
const recipientTokenAccount = await getAssociatedTokenAddress(USDC_MINT, recipient);

const instructions = [];

const recipientTokenInfo = await connection.getAccountInfo(recipientTokenAccount);
if (!recipientTokenInfo) {
  instructions.push(
    createAssociatedTokenAccountInstruction(
      feePayer.publicKey,
      recipientTokenAccount,
      recipient,
      USDC_MINT
    )
  );
}

instructions.push(
  createTransferCheckedInstruction(
    senderTokenAccount,
    USDC_MINT,
    recipientTokenAccount,
    sender,
    BigInt(1000000),
    6
  )
);

const transaction = new Transaction();
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');

transaction.recentBlockhash = blockhash;
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.feePayer = feePayer.publicKey;
instructions.forEach(instruction => transaction.add(instruction));

let signedTransaction = await delegatedSignTransaction(delegatedClient, {
  walletId: credentials.walletId,
  walletApiKey: credentials.walletApiKey,
  keyShare: credentials.keyShare,
  transaction,
  signerAddress: sender.toString(),
});

if (!(signedTransaction instanceof VersionedTransaction)) {
  const message = signedTransaction.serializeMessage();
  const feePayerSignature = nacl.sign.detached(message, feePayer.secretKey);
  signedTransaction.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
}

const signature = await connection.sendRawTransaction(signedTransaction.serialize());
await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight });

Signature Verification

When using both delegated signing and fee payer signatures, verify all required signatures are present:
if (!(signedTransaction instanceof VersionedTransaction)) {
  const allSigned = signedTransaction.signatures.every(sig => sig.signature !== null);

  if (!allSigned) {
    const unsigned = signedTransaction.signatures
      .filter(sig => sig.signature === null)
      .map(sig => sig.publicKey.toString());
    throw new Error(`Missing signatures for: ${unsigned.join(', ')}`);
  }

  console.log('All required signatures present');
}

Error Handling

try {
  const signature = await connection.sendRawTransaction(signedTransaction.serialize());
  await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight });
} catch (error) {
  if (error.message.includes('insufficient funds')) {
    console.error('Fee payer wallet has insufficient SOL');
  } else if (error.message.includes('key share')) {
    console.error('Invalid or expired delegation credentials');
  } else {
    console.error('Transaction failed:', error);
  }
}
Last modified on June 23, 2026