> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# SVM Server-Controlled Sponsorship

> Build a Solana transaction on your backend, validate it against your own rules, and have Dynamic sponsor it for a specific user, so you decide exactly what gets sponsored.

> [!NOTE]
>
> SVM gas sponsorship is an enterprise-only feature. [Contact Dynamic](https://www.dynamic.xyz/talk-to-us) to learn more about upgrading your plan.

With the default [SVM gas sponsorship](https://www.dynamic.xyz/docs/javascript/reference/solana/svm-gas-sponsorship.md) flow, the client sends a transaction and Dynamic sponsors it automatically. That is the fastest path, but the decision to sponsor lives entirely in the SDK, and any transaction the client builds is eligible.

This recipe moves that decision to **your backend**. Your server builds the exact transaction, applies whatever policy you want, asks Dynamic to sponsor it for a specific user, and returns the co-signed transaction to the client. The user then signs it and broadcasts it. Because your server defines the transaction, nothing outside your policy gets sponsored.

Common reasons to do this:

* Require **KYC** or another verification before sponsoring a user.
* Apply **custom rate limits** or budgets per user or per wallet.
* Enforce **custom preconditions** for each user or wallet, such as a plan tier, a region, or an existing balance.
* Sponsor only **specific actions**, such as a swap through a route you approve, and reject everything else.

Sponsoring a transaction can cost more than the network fee. A transaction can also create accounts that need rent, such as an Associated Token Account (ATA), and an attacker who can submit arbitrary transactions for sponsorship can use that to drain the sponsor, for example by creating many Associated Token Accounts (ATAs). When your server builds the transaction and checks it before requesting sponsorship, you control which instructions the sponsor ever pays for.

> [!NOTE]
>
> The example below uses the **JavaScript client SDK** to sign and broadcast and the **Node server SDK** to build and sponsor. It builds a swap from a [LI.FI](https://li.fi) quote, so the transaction comes straight from the API and your server never assembles instructions by hand.

## How it works

1. **Your backend** builds the transaction, validates it and the user, and asks Dynamic to sponsor it on behalf of that user.
2. **Dynamic** returns the transaction with the sponsor as fee payer and the sponsor's signature already in place.
3. **The client** signs the transaction as the user and broadcasts it.

## 1. Restrict sponsorship to your server

First, make sure SVM gas sponsorship is enabled for your environment. In the [Dynamic Console](https://console.dynamic.xyz), go to **Sponsor Gas → Dynamic** and enable **Sponsor Network Fees (Solana)**, with Solana enabled in your embedded wallet chain configuration.

Then, in the same card, turn on **Restrict sponsorship to server-side requests**. With it on, only your server can request sponsorship for Solana transactions. The change reaches end users the next time their app loads.

While the setting is on, client SDKs stop requesting sponsorship, so a transaction your app sends without going through your backend is paid for by the user.

## 2. Build the transaction on your backend

Authenticate the Node SDK with your environment API token. Read the sponsor address with `getSponsorAddress` and pass it to LI.FI as `svmSponsor`. LI.FI then builds the transaction with the sponsor as fee payer, so the quote you get back is ready to sponsor as is.

This example swaps 0.05 SOL for USDC. Both `getSponsorAddress` and the `userId` option of `sponsorTransaction` used later require `@dynamic-labs-wallet/node-svm` version 1.1.64 or later.

```javascript theme={"system"}
import { DynamicSvmWalletClient } from '@dynamic-labs-wallet/node-svm';
import { VersionedTransaction } from '@solana/web3.js';

const client = new DynamicSvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID,
});
await client.authenticateApiToken(process.env.DYNAMIC_API_TOKEN);

// Mainnet token addresses: USDC, and LI.FI's address for native SOL.
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const SOL_NATIVE = '11111111111111111111111111111111';

const buildSwapTransaction = async ({ sponsorAddress, walletAddress }) => {
  const params = new URLSearchParams({
    fromChain: 'SOL',
    toChain: 'SOL',
    fromToken: SOL_NATIVE,
    toToken: USDC_MINT,
    fromAmount: '50000000', // 0.05 SOL, which has 9 decimals
    fromAddress: walletAddress,
    allowExchanges: 'jupiter',
    svmSponsor: sponsorAddress, // names the sponsor as fee payer in the quote
  });

  const response = await fetch(`https://li.quest/v1/quote?${params}`, {
    headers: process.env.LIFI_API_KEY
      ? { 'x-lifi-api-key': process.env.LIFI_API_KEY }
      : {},
  });
  if (!response.ok) throw new Error(`LI.FI quote failed: ${response.status}`);

  const quote = await response.json();
  return VersionedTransaction.deserialize(
    Buffer.from(quote.transactionRequest.data, 'base64'),
  );
};
```

LI.FI accepts an API key in the `x-lifi-api-key` header for higher rate limits. You can create one on the [LI.FI Partner Portal](https://portal.li.fi/integrations).

## 3. Validate on your backend

Before asking for sponsorship, apply your own rules to the user and to the transaction. The checks below are your business policy. Dynamic separately verifies that the wallet signing the transaction belongs to the `userId` you send, in your environment.

```javascript theme={"system"}
// Application-supplied: the programs your policy allows, for example the
// programs of the swap routes you accept.
const ALLOWED_PROGRAM_IDS = new Set(process.env.ALLOWED_PROGRAM_IDS.split(','));

const isUserEligible = async (userId) => {
  // Add your own checks for the authenticated user here
  // (KYC status, per-user rate limit or budget, region, fraud flags, ...).
  return true;
};

// Reject if ANY instruction falls outside policy.
const isTransactionSponsorable = ({ transaction, sponsorAddress, walletAddress }) => {
  const { message } = transaction;
  const [feePayer] = message.staticAccountKeys;

  // The sponsor must be the fee payer, and the user's wallet must sign.
  if (feePayer.toBase58() !== sponsorAddress) return false;
  const signers = message.staticAccountKeys
    .slice(0, message.header.numRequiredSignatures)
    .map((key) => key.toBase58());
  if (!signers.includes(walletAddress)) return false;

  // Program ids are always static account keys, so no lookup table
  // resolution is needed for this check.
  return message.compiledInstructions.every((instruction) =>
    ALLOWED_PROGRAM_IDS.has(
      message.staticAccountKeys[instruction.programIdIndex].toBase58(),
    ),
  );
};
```

If your policy also inspects the accounts an instruction uses, resolve the transaction's address lookup tables first, because LI.FI transactions reference most accounts through them.

## 4. Sponsor on behalf of the user

Authenticate the request, derive the user and wallet on the server, run your checks, and call `sponsorTransaction` with the user's `userId`. Dynamic returns the transaction with the sponsor's signature. The user's signature slot stays empty.

```javascript theme={"system"}
app.post('/api/sponsored-swap', async (req, res) => {
  // Application-supplied: verify the caller and look up their Dynamic user id
  // and Solana wallet address. Never trust identifiers sent in the request body.
  const { userId, walletAddress } = await getAuthenticatedUser(req);

  if (!(await isUserEligible(userId))) {
    return res.status(403).json({ error: 'Not eligible for sponsorship' });
  }

  const sponsorAddress = await client.getSponsorAddress();
  const transaction = await buildSwapTransaction({ sponsorAddress, walletAddress });

  if (!isTransactionSponsorable({ transaction, sponsorAddress, walletAddress })) {
    return res.status(403).json({ error: 'Not eligible for sponsorship' });
  }

  const sponsored = await client.sponsorTransaction({ transaction, userId });

  return res.json({
    transaction: Buffer.from(sponsored.serialize()).toString('base64'),
  });
});
```

The `userId` tells Dynamic which end user the transaction is for. The wallet that signs the transaction must belong to that user in your environment, and Dynamic rejects the request otherwise. Omit `userId` only when you sponsor transactions signed by your own server wallets.

## 5. Sign and broadcast on the client

The client requests the sponsored transaction from your API, signs it with the user's wallet, and broadcasts it.

```javascript theme={"system"}
import { getActiveNetworkData } from '@dynamic-labs-sdk/client';
import { getSolanaConnection, signTransaction } from '@dynamic-labs-sdk/solana';
import { VersionedTransaction } from '@solana/web3.js';

// `walletAccount` is the user's Solana embedded wallet
const sendSponsoredSwap = async (walletAccount) => {
  // Send your own authentication with this request.
  const response = await fetch('/api/sponsored-swap', { method: 'POST' });
  const { transaction } = await response.json();

  const sponsored = VersionedTransaction.deserialize(
    Uint8Array.from(atob(transaction), (char) => char.charCodeAt(0)),
  );

  // The sponsor has already signed, so this adds only the user's signature.
  const { signedTransaction } = await signTransaction({
    transaction: sponsored,
    walletAccount,
  });

  const { networkData } = await getActiveNetworkData({ walletAccount });
  const connection = getSolanaConnection({ networkData });

  return connection.sendRawTransaction(signedTransaction.serialize());
};
```

The function returns the transaction signature. Confirm it the way you confirm any Solana transaction.

> [!WARNING]
>
> Do not change the transaction after Dynamic sponsors it. The sponsor's signature covers the whole message, including the blockhash, so any change invalidates it. The blockhash also expires, so the user must sign and broadcast within roughly 60 to 90 seconds of your server returning the transaction.

## Related

* [SVM Gas Sponsorship](https://www.dynamic.xyz/docs/javascript/reference/solana/svm-gas-sponsorship.md): the default client-side flow and setup.
* [Signing and Sending Transactions](https://www.dynamic.xyz/docs/javascript/reference/solana/signing-sending-transactions.md): `signTransaction` and the other Solana signing functions.
* [Getting Solana Connection](https://www.dynamic.xyz/docs/javascript/reference/solana/getting-solana-connection.md): create a connection from a wallet account's network.
* [sponsorTransaction](https://www.dynamic.xyz/docs/node/reference/svm/sponsor-transaction.md): the Node SDK method that sponsors a transaction for a user.
* [getSponsorAddress](https://www.dynamic.xyz/docs/node/reference/svm/get-sponsor-address.md): read the sponsor address to build transactions that name it as fee payer.
* [EVM Server-Controlled Sponsorship](https://www.dynamic.xyz/docs/recipes/integrations/evm-server-controlled-sponsorship.md): the same pattern for EVM chains.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.