Sponsor Gas on Solana
How Solana fee sponsorship works
Unlike EVM chains, Solana doesn't need ERC-4337 or paymasters for fee sponsorship. Every Solana transaction has a fee payer field that can be set to any account — it doesn't have to be the same account that initiates the transaction. ZeroDev takes advantage of this by setting the fee payer to a ZeroDev-managed wallet that covers two types of costs:
- Transaction fees — the cost of including and executing the transaction on the network.
- Rent — the minimum SOL deposit required to create new accounts or token accounts on-chain (e.g. when calling
createAccountorcreateAssociatedTokenAccount).
Your users sign the transaction normally, and ZeroDev's sponsorship server co-signs as the fee payer and broadcasts the transaction — all in a single step.
Paying for sponsored gas
When you sponsor gas through ZeroDev, there are two ways to pay for the gas:
-
Put down your credit card. We front the gas for your users, and then at the end of the billing cycle (once a month) we charge your credit card.
-
Buy gas credits from us.
Setting up gas sponsoring policies
To avoid over-spending on sponsoring, you must set up gas-sponsoring policies. Sign up on the ZeroDev dashboard if you haven't already, then set up gas policies.
Note that you MUST set up a gas policy to begin sponsoring. Without setting up a gas policy, there won't be any gas sponsored.
Installation
Install the ZeroDev Solana sponsorship SDK along with its peer dependencies:
npm i @zerodev/solana-sponsorship-sdk @solana/kit @solana-program/systemQuick Start
Get your RPC endpoint
Go to the ZeroDev dashboard and create a Solana project. Copy the RPC endpoint for the correct Solana network (mainnet or devnet), e.g. https://rpc.zerodev.app/api/v3/svm/YOUR_PROJECT_ID/chain/9034109930.
Create the sponsorship and Solana RPC clients
Get a Solana RPC URL from a provider like Alchemy or Helius, or use the public endpoint https://api.mainnet-beta.solana.com (restrictively rate-limited and not advisable for production).
import { createSolanaRpc, mainnet } from "@solana/kit";
import { createSponsorshipRpc } from "@zerodev/solana-sponsorship-sdk";
const sponsorshipRpc = createSponsorshipRpc({
endpoint: "https://rpc.zerodev.app/api/v3/svm/YOUR_PROJECT_ID/chain/9034109930",
});
const solanaRpc = createSolanaRpc(mainnet("YOUR_SOLANA_RPC_URL"));Get a recent blockhash
const { value: { blockhash: recentBlockhash, lastValidBlockHeight } } =
await solanaRpc.getLatestBlockhash({ commitment: "finalized" }).send();Create your transaction instruction(s)
This can be any Solana instruction. Here we use a simple SOL transfer as an example:
import { lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const transferInstruction = getTransferSolInstruction({
source: userKeypair, // your user's keypair signer
destination: toAddress, // destination address
amount: lamports(1_000_000n), // 0.001 SOL
});Get the sponsor fee payer and build the transaction
Fetch ZeroDev's fee payer address and set it as the transaction's fee payer. We use a NoopSigner as a placeholder because the fee payer's private key lives on ZeroDev's server, not the client. The real fee payer signature is added server-side when you call sponsorTransaction in the next step.
import {
createTransactionMessage,
pipe,
setTransactionMessageLifetimeUsingBlockhash,
address,
blockhash,
appendTransactionMessageInstructions,
setTransactionMessageFeePayerSigner,
createNoopSigner,
} from "@solana/kit";
const feePayer = await sponsorshipRpc.getFeePayer().send();
const message = pipe(
createTransactionMessage({ version: "legacy" }),
(msg) => setTransactionMessageFeePayerSigner(createNoopSigner(address(feePayer)), msg),
(msg) => appendTransactionMessageInstructions([transferInstruction], msg),
(msg) => setTransactionMessageLifetimeUsingBlockhash(
{ blockhash: blockhash(recentBlockhash), lastValidBlockHeight },
msg
),
);Sign and sponsor the transaction
The user signs the transaction, then ZeroDev co-signs as the fee payer and broadcasts it to the network:
import { partiallySignTransactionMessageWithSigners } from "@solana/kit";
import { sponsorTransaction } from "@zerodev/solana-sponsorship-sdk";
const signedMessage = await partiallySignTransactionMessageWithSigners(message);
const response = await sponsorTransaction(sponsorshipRpc, signedMessage);
console.log(`Transaction sponsored: https://explorer.solana.com/tx/${response.signature}`);Using with external wallets
If your users sign with an external wallet provider, replace step 6 above: compile the transaction to bytes, sign with the wallet, then decode the signed bytes back for sponsorship.
Privy
import { compileTransaction, getTransactionEncoder, getTransactionDecoder } from "@solana/kit";
import { sponsorTransaction } from "@zerodev/solana-sponsorship-sdk";
import { useSignTransaction } from "@privy-io/react-auth/solana";
const { signTransaction } = useSignTransaction();
// Build the transaction message with pipe() as shown in steps 2–5, then:
const unsignedTx = compileTransaction(message);
const encoded = new Uint8Array(getTransactionEncoder().encode(unsignedTx));
const { signedTransaction } = await signTransaction({ transaction: encoded, wallet });
const decoded = getTransactionDecoder().decode(signedTransaction);
const result = await sponsorTransaction(sponsorshipRpc, decoded);Dynamic
import { compileTransaction, getTransactionEncoder, getTransactionDecoder } from "@solana/kit";
import { sponsorTransaction } from "@zerodev/solana-sponsorship-sdk";
import { isSolanaWallet } from "@dynamic-labs/solana";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
const { primaryWallet } = useDynamicContext();
if (!primaryWallet || !isSolanaWallet(primaryWallet)) throw new Error("No Solana wallet");
// Build the transaction message with pipe() as shown in steps 2–5, then:
const unsignedTx = compileTransaction(message);
const encoded = new Uint8Array(getTransactionEncoder().encode(unsignedTx));
const { VersionedTransaction } = await import("@solana/web3.js");
const signer = await primaryWallet.getSigner();
const versionedTx = VersionedTransaction.deserialize(encoded);
const signedTx = await (signer.signTransaction as any)(versionedTx);
const decoded = getTransactionDecoder().decode((signedTx as any).serialize());
const result = await sponsorTransaction(sponsorshipRpc, decoded);Error Handling
If sponsorship fails (e.g., you've hit a policy limit), the SDK throws a SponsorshipError:
import { sponsorTransaction, SponsorshipError } from "@zerodev/solana-sponsorship-sdk";
try {
const response = await sponsorTransaction(sponsorshipRpc, signedMessage);
} catch (error) {
if (error instanceof SponsorshipError) {
console.error(`Sponsorship failed (code ${error.code}): ${error.message}`);
}
}