Skip to content

Sponsoring transactions on Solana

With embedded wallets, your app can also sponsor gas fees for transactions on Solana, allowing users to transact without a SOL balance.

This is done by configuring the feePayer property of the sponsored transaction to be a fee payer wallet that your app manages to pay users' gas fees.

Overview

At a high-level, sponsoring transactions on Solana involves the following:

  1. In your backend, set up a fee payer wallet managed by your app that will pay for gas fees on behalf of users.
  2. In your frontend, when sending a sponsored transaction:
    • Prepare the transaction to be sponsored by setting the feePayer to be your fee payer wallet's address.
    • Serialize the transaction message, and sign it with the embedded wallet.
    • Serialize the partially signed transaction, and send it to your backend.
  3. In your backend, when you receive the serialized partially signed transaction:
    • Deserialize the partially signed transaction, and sign it with your fee payer wallet.
    • Broadcast the transaction, signed by both the user and the feepayer, to the network.

Follow the guide below to see these steps illustrated in code.

TIP

To prepare transactions with a fee payer, and to manage your keypair wallet, we strongly recommend using the @solana/web3.js library. The examples here will illustrate the sponsorship flow using this library in conjunction with Privy's React and Expo SDKs.

1. Create a fee payer wallet (backend)

To start, create a fee payer wallet in your backend to sponsor transactions sent by users. You can either:

  • generate a new Solana keypair directly and save the private key for the wallet yourself, or
  • create a Solana server wallet to act as your fee payer.

Generate a new keypair using @solana/web3.js or your preferred Solana library.

tsx
import {Keypair} from '@solana/web3.js';
import bs58 from 'bs58';

const feePayerWallet = new Keypair();
const feePayerAddress = feePayerWallet.publicKey.toBase58();
const feePayerPrivateKey = bs58.encode(feePayerWallet.secretKey);

Then, save the wallet's base58-encoded address and private key. Make sure to store the private key securely; it should never leave your server.

Make sure to fund your fee payer wallet with a balance of SOL so it can pay for transaction fees.

2. Sign the transaction with the embedded wallet (frontend)

Next, when a user intends to send a sponsored transaction from your frontend, prepare the transaction with the feePayer configured to be your server wallet's address, and partially sign the transaction with the user's embedded wallet.

Prepare the transaction with a custom feePayer

First, prepare a VersionedTransaction object for the transaction you want to send per the steps below. Set the feePayer for the transaction to be the fee payer wallet you created in step (1).

tsx
import {
  TransactionMessage,
  PublicKey,
  VersionedTransaction,
  Connection,
  clusterApiUrl,
} from '@solana/web3.js';

// Get the latest blockhash
const connection = new Connection(clusterApiUrl('insert-your-rpc-url'));
const {blockhash} = await connection.getLatestBlockhash();

// Replace this with an array of the instructions to include in your transaction
const instructions = [];

// Construct the transaction message from instructions
const message = new TransactionMessage({
  payerKey: new PublicKey('insert-your-fee-payer-wallet-address'), // Set the fee payer
  recentBlockhash: blockhash,
  instructions,
}).compileToV0Message();

// Construct the transaction from the message
const transaction = new VersionedTransaction(message);

Sign the transaction with the embedded wallet

Next, serialize the transaction message to a base64-encoded string. The user will sign this data with their embedded wallet.

tsx
const serializedMessage = Buffer.from(transaction.message.serialize()).toString('base64');

Then, sign the serializedMessage with the user's embedded wallet via the signMessage RPC:

tsx
import {useSolanaWallets} from '@privy-io/react-auth/solana';

// Find the user's embedded wallet
const {wallets} = useSolanaWallets();
const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy');

// Request a signature over the serialized transaction message from the embedded wallet
const provider = await embeddedWallet.getProvider();
const {signature: serializedUserSignature} = await provider.request({
  method: 'signMessage',
  params: {
    message: serializedMessage, // Serialized transaction message as base64 string
  },
});

Note that sending sponsored transactions on Solana requires that the user sign an opaque string (the serialized transaction message) which may be a confusing experience. If you'd like to sponsor transactions for your users, consider using your own UIs to present transaction details to your user. We are actively working on improvements to this flow.

Serialize the partially signed transaction and send it to your backend

Given the signature from the user's embedded wallet, add it to the transaction.

tsx
import {PublicKey} from '@solana/web3.js';

// Deserialize the signature returned from the embedded wallet, and add it to the transaction
const userSignature = Buffer.from(serializedUserSignature, 'base64');
transaction.addSignature(new PublicKey('insert-embedded-wallet-address'), userSignature);

Then, serialize the transaction to be sent over the wire, and send it to your backend.

tsx
const transactionBytes = Buffer.from(transaction.serialize());
const serializedTransaction = transactionBytes.toString('base64');

Send this serialized transaction to your backend, where it will then be signed by the fee payer wallet.

3. Sign the transaction with the fee payer wallet (backend)

Finally, sign the partially signed transaction with the fee payer wallet to authorize sponsorship, and submit it to the network.

Deserialize the partially signed transaction

Once your backend receives the serialized transaction, partially signed by the user's embedded wallet, deserialize it back to a VersionedTransaction object:

tsx
import {VersionedTransaction} from '@solana/web3.js';

// The `serializedTransaction` should be sent from your frontend to your backend
const transaction = VersionedTransaction.deserialize(Buffer.from(serializedTransaction, 'base64'));

Sign the transaction with the fee payer wallet

Next, sign the transaction with your fee payer wallet. Follow the appropriate interface depending on if you are managing the Keypair yourself or using a server wallet.

tsx
import {Keypair} from '@solana/web3.js';
import bs58 from 'bs58';

// Initialize a keypair instance for your fee payer wallet using the private key you saved
const feePayerWallet = Keypair.fromSecretKey(bs58.decode('insert-fee-payer-wallet-private-key'));

// Sign the transaction with the fee payer wallet
transaction.sign([feePayerWallet]);

Broadcast the transaction

Lastly, broadcast the transaction that has been signed by both the user's embedded wallet and your fee payer wallet.

tsx
import {Connection, clusterApiUrl} from '@solana/web3.js';

const connection = new Connection(clusterApiUrl('insert-your-rpc-url'));
await connection.sendTransaction(transaction);