> ## 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.

# EVM Server-Controlled Sponsorship

> Sign a gas-sponsored EVM transaction on the client, then validate and relay it from your own backend so you decide exactly what gets sponsored.

<Info>
  EVM Gas Sponsorship is an enterprise-only feature. [Contact us](https://www.dynamic.xyz/talk-to-us) to learn more about upgrading your plan.
</Info>

With the default [EVM gas sponsorship](/docs/javascript/reference/evm/evm-gas-sponsorship) flow, the client signs a sponsored transaction and submits it in one step. That's the fastest path, but the decision to sponsor lives entirely in the SDK.

This recipe moves that decision to **your backend**. The user signs the transaction on the client, but instead of submitting it, the client hands the signed transaction to your server. Your server applies whatever policy you want, per user or per transaction, and only then relays it. You decide exactly what gets sponsored and for whom.

Common reasons to do this:

* Sponsor gas only for **specific actions** (e.g. a card top-up, a first purchase) and reject everything else.
* Sponsor gas only for **specific users** (e.g. a plan tier, a verified account, a region).
* Apply **per-user budgets or rate limits** before spending sponsored gas.

<Note>
  The example below uses the **JavaScript client SDK** to sign and the **Node server SDK** to relay. This is not limited to these SDKs. The same pattern works with any Dynamic client SDK for signing and any Dynamic server SDK for relaying.
</Note>

## How it works

1. **Client** signs the transaction and sends the signed bundle to your API.
2. **Your backend** validates the transaction and the user, then relays it through Dynamic.
3. **Dynamic** submits the transaction on-chain and the user pays no gas.

## 1. Restrict sponsorship to your server

First, make sure **EVM gas sponsorship is enabled** for your environment: in the [Dynamic Dashboard](https://app.dynamic.xyz), go to **Wallets** → **Sponsor Gas** → **Dynamic** and enable **Sponsor Network Fees (EVM)** (with the EVM chains you want to sponsor enabled).

Then, in that same **Sponsor Network Fees (EVM)** setting, turn on **Restrict sponsorship to server-side requests**.

With this on, only your server can submit EVM sponsored transactions. Requests sent directly by end users are rejected. This controls who can *submit* a sponsored transaction, not who *signs* it: users still sign on the client exactly as before.

## 2. Sign on the client

Call `signSponsoredTransaction` to produce a signed transaction bundle **without submitting it**, then send that bundle to your backend along with the user's Dynamic user id. This example signs a single **USDC transfer on Base**, the same transaction your backend validates in the next step.

```javascript theme={"system"}
import { signSponsoredTransaction } from '@dynamic-labs-sdk/evm';
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';

// USDC on Base: an ERC-20 contract with 6 decimals.
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// `walletAccount` is the user's EVM embedded wallet
const requestSponsoredTransfer = async (walletAccount, userId, recipient) => {
  const signedTransaction = await signSponsoredTransaction({
    walletAccount,
    calls: [
      {
        target: USDC_ADDRESS,
        value: 0n, // no native token: the call moves USDC, not ETH
        data: encodeFunctionData({
          abi: erc20Abi,
          functionName: 'transfer',
          args: [recipient, parseUnits('5', 6)], // 5 USDC
        }),
      },
    ],
  });

  // `value` is a bigint, so serialize bigints to strings for JSON transport.
  const body = JSON.stringify(
    { userId, signedTransaction },
    (_key, value) => (typeof value === 'bigint' ? value.toString() : value),
  );

  const response = await fetch('/api/sponsor-transaction', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body,
  });

  return response.json();
};
```

## 3. Validate on your backend

Your API receives the signed bundle. This is the shape to expect:

```json theme={"system"}
{
  "userId": "d3b07384-d9a0-4c9b-8f6e-1a2b3c4d5e6f",
  "signedTransaction": {
    "walletAddress": "0x...",
    "chainId": 8453,
    "calls": [{ "target": "0x...", "data": "0x...", "value": "0" }],
    "nonce": "0",
    "deadline": "1730000000",
    "signature": "0x...",
    "relayer": "0x...",
    "authorization": {
      "address": "0x...",
      "chainId": 8453,
      "nonce": 0,
      "r": "0x...",
      "s": "0x...",
      "yParity": 0
    }
  }
}
```

| Field                             | Type                        | Description                                                                                                                                             |
| --------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userId`                          | `string`                    | The Dynamic user id the transaction is relayed on behalf of.                                                                                            |
| `signedTransaction.walletAddress` | `string`                    | The user's embedded wallet address.                                                                                                                     |
| `signedTransaction.chainId`       | `number`                    | Target EVM chain id.                                                                                                                                    |
| `signedTransaction.calls`         | `{ target, data, value }[]` | The batch of calls being sponsored. `value` arrives as a string (serialized from a `bigint`).                                                           |
| `signedTransaction.nonce`         | `string`                    | Bitmap nonce signed into the intent.                                                                                                                    |
| `signedTransaction.deadline`      | `string`                    | Intent expiration, as a unix-timestamp string.                                                                                                          |
| `signedTransaction.signature`     | `string`                    | EIP-712 signature over the intent.                                                                                                                      |
| `signedTransaction.relayer`       | `string`                    | Relayer address signed into the intent.                                                                                                                 |
| `signedTransaction.authorization` | `object` (optional)         | Serialized EIP-7702 authorization. Present on a wallet's **first** sponsored transaction (the one that installs the delegation) and omitted afterwards. |

<Note>
  When you relay, Dynamic already verifies that the wallet belongs to this user in your environment, caps the batch at 5 calls, and checks that the EIP-7702 authorization (when present) targets the correct delegate contract. The checks below are your **business policy** on top of that.
</Note>

Before relaying, you can apply your own rules, for example checking **what** the transaction does and **who** the user is.

The example below validates a single **USDC-on-Base transfer to an allowed recipient**.

```javascript theme={"system"}
import { decodeFunctionData, erc20Abi, getAddress } from 'viem';

// USDC on Base: replace with the token + chain you sponsor.
const SPONSORED_TOKEN = getAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
const ALLOWLIST = new Set(
  ['0x1111111111111111111111111111111111111111'].map((a) => a.toLowerCase()),
);

// Reject if ANY call falls outside policy. `calls` is a batch, so never check only calls[0].
const isTransactionSponsorable = (signedTransaction) =>
  signedTransaction.calls.every((call) => {
    // Only sponsor calls to the USDC contract
    if (getAddress(call.target) !== SPONSORED_TOKEN) return false;

    // Only sponsor calls that don't move native token
    if (call.value && BigInt(call.value) !== 0n) return false;

    try {
      // Only sponsor ERC-20 transfers to an allowed recipient
      const decoded = decodeFunctionData({ abi: erc20Abi, data: call.data });
      if (decoded.functionName !== 'transfer') return false;
      const [recipient] = decoded.args;
      return ALLOWLIST.has(recipient.toLowerCase());
    } catch {
      return false; // unrecognized calldata
    }
  });

const isUserSponsorable = (userId) => {
  // Add your own checks for the authenticated user here
  // (plan tier, per-user quota / budget, KYC status, region, fraud flags, ...).
  return true;
};
```

## 4. Relay from your backend

Authenticate the Node SDK with your environment API token, then relay the signed bundle. Pass the `userId` so the relay is attributed to that end user.

```javascript theme={"system"}
import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';

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

app.post('/api/sponsor-transaction', async (req, res) => {
  const { userId, signedTransaction: received } = req.body;

  // Revive the bigint `value` fields serialized on the client.
  const signedTransaction = {
    ...received,
    calls: received.calls.map((call) => ({ ...call, value: BigInt(call.value) })),
  };

  if (!isTransactionSponsorable(signedTransaction) || !isUserSponsorable(userId)) {
    return res.status(403).json({ error: 'Not eligible for sponsorship' });
  }

  // ... relay (see the two options below)
});
```

You then have two ways to return the result to the client.

### Option A: wait on the server, return the transaction hash

`sendSponsoredTransaction` relays and waits for the transaction to land on-chain, then returns the hash. Simplest when your client just wants the final hash.

```javascript theme={"system"}
const { transactionHash } = await client.sendSponsoredTransaction({
  signedTransaction,
  userId,
});

return res.json({ transactionHash });
```

### Option B: return a request id, wait on the client

`relaySponsoredTransaction` relays **without waiting** and returns a `requestId`. Return it to the client and let the client poll for the hash. This is useful for keeping your request short or driving a progress UI.

```javascript theme={"system"}
const { requestId } = await client.relaySponsoredTransaction({
  signedTransaction,
  userId,
});

return res.json({ requestId });
```

On the client, wait for the hash with `waitForSponsoredTransaction`:

```javascript theme={"system"}
import { waitForSponsoredTransaction } from '@dynamic-labs-sdk/evm';

const { transactionHash } = await waitForSponsoredTransaction({ requestId });
```

## Related

* [EVM Gas Sponsorship overview](/docs/javascript/reference/evm/evm-gas-sponsorship) — the default client-side flow and setup.
* [Splitting sign & send](/docs/javascript/reference/evm/splitting-sign-and-send) — `signSponsoredTransaction`, `relaySponsoredTransaction`, and status polling in depth.
* [sendSponsoredTransaction](/docs/javascript/reference/evm/send-sponsored-transaction) — the primary client-side send function.
