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.
In a sponsored transaction:
User builds transaction
User creates and signs the transaction with their intent
Sponsor is designated
Transaction specifies a different address as the gas owner
Sponsor signs separately
Sponsor reviews and signs to approve paying for gas
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 ],
});
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
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 } ::` )
);
}
}
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
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
Start with tight restrictions
Begin with strict limits and relax as needed: const INITIAL_POLICY = {
maxGasPerTx: 1_000_000 ,
maxTxPerDay: 10 ,
allowedFunctions: [ 'basic_operation' ],
};
Log all sponsored transactions
Implement circuit breakers
Stop sponsorship if abuse detected: if ( await detectAnomalousActivity ( userAddress )) {
await pauseSponsorship ( userAddress );
await alertSecurityTeam ();
}
Use separate sponsor accounts
Gas Pricing Understand gas costs
Transactions Learn about transactions
Transaction Lifecycle Follow transaction execution