How do sponsored transactions work on Sui?
November 11, 2026
A sponsored transaction is a normal Sui transaction whose GasData.owner (the sponsor) is a different address from the sender (the user). The sponsor pays the gas, and the user doesn't need to hold any SUI. Both parties sign the exact same TransactionData, gas data included, and the transaction is submitted with both signatures. It's a protocol feature, so no smart contract is involved.
The data model
pub struct TransactionDataV1 {
pub kind: TransactionKind, // the PTB
pub sender: SuiAddress, // the user
pub gas_data: GasData,
pub expiration: TransactionExpiration,
}
pub struct GasData {
pub payment: Vec<ObjectRef>, // sponsor's SUI coins, or empty for address-balance gas
pub owner: SuiAddress, // the sponsor
pub price: u64,
pub budget: u64,
}Each signature covers all of TransactionData. A relay or full node in between can't swap the gas payment or change the commands without breaking both signatures.
Recommended flow: user builds, sponsor co-signs
With address balances live on mainnet, the simplest approach has the sponsor pay gas from its address balance instead of from specific coin objects. You set an empty gas payment list. Because no gas coin gets locked, one sponsor can sign many transactions in parallel without them contending for coins.
Client side, with @mysten/sui v2:
import { Transaction } from '@mysten/sui/transactions'
import { toBase64 } from '@mysten/sui/utils'
const tx = new Transaction()
tx.moveCall({ target: `${PACKAGE_ID}::game::play`, arguments: [tx.object(GAME_ID)] })
tx.setSender(userAddress)
tx.setGasOwner(sponsorAddress) // sponsor pays...
tx.setGasPayment([]) // ...from its address balance
const bytes = await tx.build({ client })
const { signature: userSignature } = await userKeypair.signTransaction(bytes)
await fetch('/sponsor', {
method: 'POST',
body: JSON.stringify({ transaction: toBase64(bytes), userSignature }),
})Sponsor side, check the bytes, add the sponsor's signature, and execute:
import { Transaction } from '@mysten/sui/transactions'
import { fromBase64 } from '@mysten/sui/utils'
const bytes = fromBase64(body.transaction)
const data = Transaction.from(bytes).getData()
// Validate here: allowed move-call targets, gas budget cap, no use of the gas coin...
const { signature: sponsorSignature } = await sponsorKeypair.signTransaction(bytes)
const result = await client.executeTransaction({
transaction: bytes,
signatures: [body.userSignature, sponsorSignature],
include: { effects: true },
})Signature order doesn't matter. If you already hold one signature, signAndExecuteTransaction also accepts additionalSignatures. To fund a sponsor's address balance, send SUI to it with 0x2::coin::send_funds. The faucet only gives out coin objects.
Mysten Labs also publishes @mysten-incubation/sponsor, an experimental package that builds a sponsor service with pluggable validators (for example gasBudget({ max }) and allowedFunctions([...])). It's still incubating, so pin the version if you use it.
Classic coin-based sponsorship
Older gas stations put specific sponsor-owned SUI coins in setGasPayment([...]). The sponsor either signs first and passes the transaction to the user ("sponsor-proposed"), or receives the user's transaction without gas data, adds it, and signs ("user-proposed"). This still works, but every gas coin can back only one in-flight transaction at a time.
Rules for the user's side
- Never use
tx.gasin a sponsored transaction. The gas coin belongs to the sponsor. When the user needs to spend their own SUI, usetx.coin({ balance, useGasCoin: false }). - In Move,
ctx.sponsor()returnsOption<address>with the sponsor's address, if there is one.
Risks to handle as a sponsor
- Abuse. Rate-limit per user, allowlist the
moveCalltargets you'll pay for, and cap the gas budget you'll sign. Use a dedicated sponsor address that holds a limited balance. - Draining through withdrawals. Reject transactions that withdraw from the sponsor's address balance as an input. That's a separate route to the same funds the gas check can't see.
- Failed transactions still cost gas. Simulate before you sign, but on-chain state can change between simulation and execution. See dry run a transaction.
- Equivocation with coin-based gas. Reusing the same gas coin in two in-flight transactions can lock it until the end of the epoch. See object version errors.
- Censorship. If users send the dual-signed transaction through your service, you can delay or drop it. Users who care can submit directly to a full node.
Sponsorship isn't multisig. The sponsor pays for the transaction but doesn't authorize what it does.
Sources
- Sponsored Transactions, Sui documentation
- Sponsor and Basic Usage, Sui TypeScript SDK
- Coins and Balances, Sui TypeScript SDK
- Executing Transactions, Sui TypeScript SDK