Skip to main content

Overview

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, allowing users to interact with Solana without holding SOL for gas 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);
  }
}