Skip to main content
Sponsored transactions allow one party (the sponsor) to pay for another party’s (the user’s) transaction gas fees. This is crucial for improving user onboarding and creating seamless Web3 experiences.

How Sponsored Transactions Work

In a sponsored transaction:
1

User builds transaction

User creates and signs the transaction with their intent
2

Sponsor is designated

Transaction specifies a different address as the gas owner
3

Sponsor signs separately

Sponsor reviews and signs to approve paying for gas
4

Transaction executes

Gas is deducted from sponsor’s gas coins

Basic Implementation

TypeScript SDK

import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';

// User creates transaction
const userKeypair = Ed25519Keypair.deriveKeypair(userMnemonic);
const sponsorKeypair = Ed25519Keypair.deriveKeypair(sponsorMnemonic);

const tx = new Transaction();
tx.moveCall({
    target: `${packageId}::module::function`,
    arguments: [/* ... */],
});

// Set sponsor as gas owner
tx.setGasOwner(sponsorKeypair.toSuiAddress());

// User signs transaction
const userSignature = await tx.sign({ client, signer: userKeypair });

// Sponsor signs transaction
const sponsorSignature = await tx.sign({ client, signer: sponsorKeypair });

// Execute with both signatures
const result = await client.executeTransactionBlock({
    transactionBlock: userSignature.bytes,
    signature: [userSignature.signature, sponsorSignature.signature],
});

Withdrawal from Sponsor

With address balances, sponsors can allow users to withdraw funds directly:
pub struct FundsWithdrawalArg {
    /// The reservation of the funds accumulator to withdraw
    pub reservation: Reservation,
    /// The type argument of the funds accumulator to withdraw
    pub type_arg: WithdrawalTypeArg,
    /// The source of the funds to withdraw
    pub withdraw_from: WithdrawFrom,
}

pub enum WithdrawFrom {
    /// Withdraw from the sender of the transaction
    Sender,
    /// Withdraw from the sponsor of the transaction (gas owner)
    Sponsor,
}

impl FundsWithdrawalArg {
    /// Withdraws from `Balance<balance_type>` in the sponsor's address (gas owner)
    pub fn balance_from_sponsor(amount: u64, balance_type: TypeTag) -> Self {
        Self {
            reservation: Reservation::MaxAmountU64(amount),
            type_arg: WithdrawalTypeArg::Balance(balance_type),
            withdraw_from: WithdrawFrom::Sponsor,
        }
    }

    pub fn owner_for_withdrawal(&self, tx: &impl TransactionDataAPI) -> SuiAddress {
        match self.withdraw_from {
            WithdrawFrom::Sender => tx.sender(),
            WithdrawFrom::Sponsor => tx.gas_owner(),
        }
    }
}
This allows users to perform actions using the sponsor’s balance without the sponsor needing to transfer coins first.

Use Cases

User Onboarding

Let new users try your app without acquiring SUI first

Gaming

Players focus on gameplay without managing gas

Enterprise Apps

Company pays for employee transactions

DAO Operations

Treasury sponsors member transactions
A typical sponsor service:
import express from 'express';
import { SuiClient } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';

const app = express();
const client = new SuiClient({ url: RPC_URL });
const sponsorKeypair = /* load from secure storage */;

// Rate limiting and security
const rateLimiter = /* implement rate limiting */;
const requestValidator = /* implement validation */;

app.post('/sponsor', rateLimiter, async (req, res) => {
    try {
        const { transactionBytes, userSignature } = req.body;
        
        // Validate request
        if (!requestValidator.isValid(req)) {
            return res.status(400).json({ error: 'Invalid request' });
        }
        
        // Validate transaction doesn't do anything malicious
        const tx = Transaction.from(transactionBytes);
        if (!isTransactionSafe(tx)) {
            return res.status(403).json({ error: 'Unsafe transaction' });
        }
        
        // Check gas budget is reasonable
        const gasBudget = tx.getData().gasData.budget;
        if (gasBudget > MAX_SPONSORED_GAS) {
            return res.status(400).json({ error: 'Gas budget too high' });
        }
        
        // Sponsor signs
        const sponsorSignature = await tx.sign({
            client,
            signer: sponsorKeypair,
        });
        
        // Execute transaction
        const result = await client.executeTransactionBlock({
            transactionBlock: transactionBytes,
            signature: [userSignature, sponsorSignature.signature],
        });
        
        res.json({
            digest: result.digest,
            effects: result.effects,
        });
    } catch (error) {
        console.error('Sponsorship error:', error);
        res.status(500).json({ error: 'Sponsorship failed' });
    }
});

function isTransactionSafe(tx: Transaction): boolean {
    // Implement safety checks:
    // - Whitelist of allowed modules/functions
    // - Check transaction doesn't transfer sponsor's assets
    // - Validate arguments are reasonable
    // - etc.
    return true; // placeholder
}

Security Considerations

Sponsors must carefully validate transactions to prevent abuse. Malicious users could craft transactions that drain the sponsor’s balance or perform unwanted actions.

Validation Checklist

Set maximum gas budget to prevent excessive costs:
const MAX_SPONSORED_GAS = 10_000_000; // 10M gas units

if (tx.getData().gasData.budget > MAX_SPONSORED_GAS) {
    throw new Error('Gas budget exceeds limit');
}
Only sponsor calls to approved functions:
const ALLOWED_FUNCTIONS = [
    `${MY_PACKAGE}::game::play_move`,
    `${MY_PACKAGE}::game::claim_reward`,
];

function validateMoveCall(target: string): boolean {
    return ALLOWED_FUNCTIONS.includes(target);
}
Prevent spam and abuse:
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each user to 100 requests per window
    keyGenerator: (req) => req.body.userAddress,
});
Verify user identity before sponsoring:
// Require signature over challenge to prove ownership
const challenge = generateChallenge();
const signature = await userKeypair.sign(challenge);

if (!verifySignature(signature, userAddress, challenge)) {
    throw new Error('Invalid user authentication');
}
Track sponsor balance and alert when low:
const sponsorBalance = await client.getBalance({
    owner: sponsorAddress,
});

if (BigInt(sponsorBalance.totalBalance) < MINIMUM_BALANCE) {
    await alertAdmin('Sponsor balance low');
}

Advanced Patterns

Conditional Sponsorship

Sponsor only specific types of transactions:
interface SponsorshipPolicy {
    canSponsor(tx: Transaction, user: string): Promise<boolean>;
}

class GameSponsorshipPolicy implements SponsorshipPolicy {
    async canSponsor(tx: Transaction, user: string): Promise<boolean> {
        // Check user has NFT or is active player
        const hasNFT = await checkUserHasGameNFT(user);
        if (!hasNFT) return false;
        
        // Check transaction is game-related
        const calls = tx.getData().commands;
        return calls.every(cmd => 
            cmd.kind === 'MoveCall' && 
            cmd.target.startsWith(`${GAME_PACKAGE}::`)
        );
    }
}

Quota-Based Sponsorship

Limit sponsorship per user:
class QuotaManager {
    private quotas = new Map<string, number>();
    
    async checkAndDeduct(user: string, gasCost: number): Promise<boolean> {
        const remaining = this.quotas.get(user) ?? DAILY_QUOTA;
        
        if (remaining < gasCost) {
            return false;
        }
        
        this.quotas.set(user, remaining - gasCost);
        return true;
    }
    
    async resetDaily() {
        this.quotas.clear();
    }
}

Transaction Batching

Sponsor multiple operations in one transaction:
const tx = new Transaction();

// User operations
tx.moveCall({
    target: `${packageId}::game::action1`,
    arguments: [/* ... */],
});

tx.moveCall({
    target: `${packageId}::game::action2`,
    arguments: [/* ... */],
});

// Sponsor pays for all operations
tx.setGasOwner(sponsorAddress);

Client Integration

React Hook Example

import { useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { useState } from 'react';

function useSponsoredTransaction() {
    const { mutateAsync: signTransaction } = useSignAndExecuteTransaction();
    const [isPending, setIsPending] = useState(false);
    
    async function executeSponsored(tx: Transaction) {
        setIsPending(true);
        try {
            // Set sponsor
            tx.setGasOwner(SPONSOR_ADDRESS);
            
            // User signs
            const userSig = await signTransaction({
                transaction: tx,
            });
            
            // Request sponsor signature
            const response = await fetch('/api/sponsor', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    transactionBytes: userSig.bytes,
                    userSignature: userSig.signature,
                }),
            });
            
            const result = await response.json();
            return result;
        } finally {
            setIsPending(false);
        }
    }
    
    return { executeSponsored, isPending };
}

Cost Management

Estimating Sponsorship Costs

async function estimateMonthlyCost(
    avgTransactionsPerUser: number,
    avgGasPerTx: number,
    activeUsers: number,
) {
    const rgp = await client.getReferenceGasPrice();
    
    const totalTxPerMonth = avgTransactionsPerUser * activeUsers * 30;
    const totalGas = totalTxPerMonth * avgGasPerTx;
    const costInMIST = totalGas * Number(rgp);
    const costInSUI = costInMIST / 1_000_000_000;
    
    return {
        totalTransactions: totalTxPerMonth,
        costInSUI,
        costPerUser: costInSUI / activeUsers,
    };
}

// Usage
const estimate = await estimateMonthlyCost(
    10,      // 10 transactions per user per day
    100_000, // 100k gas per transaction
    1000,    // 1000 active users
);

console.log(`Monthly sponsorship cost: ${estimate.costInSUI} SUI`);
console.log(`Cost per user: ${estimate.costPerUser} SUI`);

Monitoring and Alerts

class SponsorMonitor {
    async checkHealth() {
        const balance = await client.getBalance({
            owner: SPONSOR_ADDRESS,
        });
        
        const balanceSUI = Number(balance.totalBalance) / 1e9;
        
        if (balanceSUI < CRITICAL_BALANCE) {
            await this.alert('CRITICAL: Sponsor balance very low');
        } else if (balanceSUI < WARNING_BALANCE) {
            await this.alert('WARNING: Sponsor balance low');
        }
        
        return {
            balance: balanceSUI,
            status: balanceSUI > WARNING_BALANCE ? 'healthy' : 'warning',
        };
    }
    
    async logTransaction(tx: string, cost: number) {
        // Log to database/analytics
        await db.sponsorshipLogs.insert({
            timestamp: Date.now(),
            transaction: tx,
            gasUsed: cost,
        });
    }
}

Best Practices

Begin with strict limits and relax as needed:
const INITIAL_POLICY = {
    maxGasPerTx: 1_000_000,
    maxTxPerDay: 10,
    allowedFunctions: ['basic_operation'],
};
Maintain audit trail for analysis:
await logSponsoredTx({
    user: userAddress,
    transaction: txDigest,
    gasCost: gasUsed,
    timestamp: Date.now(),
});
Stop sponsorship if abuse detected:
if (await detectAnomalousActivity(userAddress)) {
    await pauseSponsorship(userAddress);
    await alertSecurityTeam();
}
Isolate sponsor funds from main treasury:
// Don't use same account for sponsorship and asset storage
const SPONSOR_ACCOUNT = '0xabc...';
const TREASURY_ACCOUNT = '0xdef...';

Gas Pricing

Understand gas costs

Transactions

Learn about transactions

Transaction Lifecycle

Follow transaction execution

Build docs developers (and LLMs) love