Skip to main content
Gas sponsorship allows your app to pay for transaction fees on behalf of users. Privy natively offers controls to limit total spend and allows for per-transaction control on whether to sponsor gas, but your app may implement finer controls on spend across wallets, users, or apps. This guide walks through an implementation of wallet-level, user-level, and app-level spending limits for Privy gas-sponsored transactions across a given timeframe.

Prerequisites

Overview

The recipe implements a three-tier spending control system:
  • Wallet-level: Limit spending per individual wallet
  • User-level: Limit spending per user across all their wallets
  • App-level: Limit total spending across all users
Privy exposes a sponsor parameter in a transaction request to conditionally enable gas sponsorship, which your app can integrate with. Based on whether a given meter is exceeded, the app can conditionally choose to send a gas sponsored transaction or fallback to a non-sponsored submission. This allows the app to create a stopgap in spend based on custom metering.

Step 1: Configure custom spending limits

To start, set up a simple policy configuration based on a daily spending limit. This strategy defines daily spending limits for each tier, resetting these limits every day, e.g. at midnight UTC.
const LIMITS = {
  perWalletDailyCents: 200, // $2 per wallet per day
  perUserDailyCents: 500, // $5 per user per day
  perAppDailyCents: 10000 // $100 per app per day
};

Step 2: Set up spend tracking

To actually know how much gas your app has consumed, set up a storage system to track spending.
interface SpendTracker {
  date: string; // YYYY-MM-DD
  spentCents: number;
}

const walletSpend = new Map<string, SpendTracker>();
const userSpend = new Map<string, SpendTracker>();
let appSpend: SpendTracker = {date: '', spentCents: 0};

Step 3: Estimate transaction costs

Define cost estimates per chain. For production use, implement dynamic cost estimation based on current gas prices. In practice, your app could also adjust the cost multipliers dynamically based on the type of transaction and a given chain. For example, recording a higher multiple of gas spend for a transaction on Solana that is also an SPL token transfer.
const CHAIN_COSTS: Record<string, number> = {
  'eip155:1': 100, // Ethereum: ~$1
  'eip155:8453': 5, // Base: ~$0.05
  'solana:mainnet': 1 // Solana: ~$0.01
};

function estimateCostCents(chainId: string): number {
  return CHAIN_COSTS[chainId] || 100; // Default $1
}

Step 4: Implement rate limit checks

Check spending against all tiers based on current date before allowing sponsorship.
function getToday(): string {
  return new Date().toISOString().split('T')[0];
}

function canSponsor(
  walletAddress: string,
  userId: string,
  costCents: number
): {allowed: boolean; reason?: string} {
  const today = getToday();

  // Level 1: Check wallet limit
  let walletTracker = walletSpend.get(walletAddress);
  if (!walletTracker || walletTracker.date !== today) {
    walletTracker = {date: today, spentCents: 0};
  }

  if (walletTracker.spentCents + costCents > LIMITS.perWalletDailyCents) {
    return {
      allowed: false,
      reason: `Wallet daily limit of $${LIMITS.perWalletDailyCents / 100} reached`
    };
  }

  // Level 2: Check user limit
  let userTracker = userSpend.get(userId);
  if (!userTracker || userTracker.date !== today) {
    userTracker = {date: today, spentCents: 0};
  }

  if (userTracker.spentCents + costCents > LIMITS.perUserDailyCents) {
    return {
      allowed: false,
      reason: `User daily limit of $${LIMITS.perUserDailyCents / 100} reached`
    };
  }

  // Level 3: Check app limit
  if (appSpend.date !== today) {
    appSpend = {date: today, spentCents: 0};
  }

  if (appSpend.spentCents + costCents > LIMITS.perAppDailyCents) {
    return {
      allowed: false,
      reason: `App daily limit of $${LIMITS.perAppDailyCents / 100} reached`
    };
  }

  return {allowed: true};
}

Step 5: Record spending after transactions

Update all spending trackers when a sponsored transaction is allowed and is successfully submitted.
function recordSpend(walletAddress: string, userId: string, costCents: number): void {
  const today = getToday();

  // Update wallet tracker
  let walletTracker = walletSpend.get(walletAddress);
  if (!walletTracker || walletTracker.date !== today) {
    walletTracker = {date: today, spentCents: 0};
  }
  walletTracker.spentCents += costCents;
  walletSpend.set(walletAddress, walletTracker);

  // Update user tracker
  let userTracker = userSpend.get(userId);
  if (!userTracker || userTracker.date !== today) {
    userTracker = {date: today, spentCents: 0};
  }
  userTracker.spentCents += costCents;
  userSpend.set(userId, userTracker);

  // Update app tracker
  if (appSpend.date !== today) {
    appSpend = {date: today, spentCents: 0};
  }
  appSpend.spentCents += costCents;
}

Step 6: Send transactions with conditional sponsorship

Integrate the rate limiting logic with Privy’s transaction API.
async function sendTransaction({userId, walletAddress, chainId, transaction}) {
  const estimatedCostCents = estimateCostCents(chainId);
  const {allowed, reason} = canSponsor(walletAddress, userId, estimatedCostCents);

  const response = await fetch(`https://api.privy.io/v1/wallets/${walletAddress}/rpc`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'privy-app-id': process.env.PRIVY_APP_ID!,
      Authorization: `Bearer ${process.env.PRIVY_APP_SECRET}`
    },
    body: JSON.stringify({
      method: 'eth_sendTransaction',
      params: {
        transaction: {transaction}
      },
      caip2: chainId,
      sponsor: allowed // Conditionally sponsor based on limits
    })
  });

  if (response.ok) {
    const result = await response.json();

    // Record spend if sponsored
    if (allowed) {
      recordSpend(walletAddress, userId, estimatedCostCents);
    } else {
      console.log(reason);
    }
  }
}

Advanced considerations

Some additional directions to explore for more advanced custom rate limiting:
  • Replace in-memory rate limit storage with persistent storage, e.g. Redis
  • For smoother limits, consider implementing sliding window rate limiting
  • Implement rate limit counters for number of total transactions sent in addition to transaction dollar volume. For example, a rate limit to allow at most 100 transactions per day
  • Implement per-transaction limits. For example, reject a transaction if its gas cost estimate prior to submission is over $1