llms.txt
@mysten/sui v2.0 and a new dApp Kit are here! Check out the migration guide
Mysten Labs SDKs

Sponsor

Build gas sponsorship services with pluggable transaction validation

Incubation package. This package is still experimental and might change without notice. Pin versions and review changes before upgrading.

Primitives for building gas-sponsorship flows on Sui. You are the sponsor operator: you bring a Signer (the sponsor) and a policy for what you'll pay for, and createSponsor takes a user's transaction, fills in the gas, validates it against that policy, and adds the sponsor's signature. The hard part, validating what you're sponsoring, is handled by a pluggable pipeline backed by the @mysten/wallet-sdk transaction analyzer.

The transaction comes from your user; you decide what's allowed and cover the gas.

Install

npm i @mysten-incubation/sponsor @mysten/sui

Quick start

import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { createSponsor, defaults, gasBudget, allowedFunctions } from '@mysten-incubation/sponsor';

const client = new SuiGrpcClient({
	network: 'testnet',
	baseUrl: 'https://fullnode.testnet.sui.io:443',
});

const sponsor = createSponsor({
	signer: Ed25519Keypair.fromSecretKey(process.env.SPONSOR_KEY!),
	client,
	// `defaults()` keeps the baseline (see Defaults).
	validate: [defaults(), gasBudget({ max: 50_000_000n }), allowedFunctions(['0xabc::shop::buy'])],
});

How gas works

The sponsor pays gas from its address balance. The empty gas payment (setGasPayment([])) draws from the sponsor's onchain SUI balance rather than from specific gas coins. This means the sponsor can sign and execute in parallel:

Address-balance gas doesn't lock specific coin objects, so concurrent sponsored transactions don't contend for gas coins. A sender that reuses its own versioned objects (coins) across rapid transactions still has to serialize those, but that's the sender's concern, not the sponsor's.

You fund the address balance by depositing SUI into it (for example, a one-time 0x2::coin::send_funds(coin, sponsorAddress)); the faucet only hands out coin objects, not address balance.

Because gas is the sponsor's, a sponsored transaction must never use the gas coin (tx.gas). gasCoinNotUsed() (a default) enforces this. When the user needs SUI, they spend their own with tx.coin({ balance, useGasCoin: false }).

Defaults

If you pass no validate, the sponsor runs defaults(): validSender(), onlyAddressBalanceGas(), gasCoinNotUsed(), onlySenderWithdrawals(), simulationSucceeds(), and boundedExpiration(). Once you add your own validators they no longer run automatically; drop them back in as one entry (the validate array flattens nested arrays):

createSponsor({ signer, client }); // runs defaults()
createSponsor({ signer, client, validate: [defaults(), allowedFunctions(['0xabc::shop::buy'])] });

Each default guards something real: validSender() requires a sender and rejects transactions where the sponsor is also the sender; onlyAddressBalanceGas() and gasCoinNotUsed() stop a caller from spending the sponsor's gas (its address balance pays, and the gas coin is the sponsor's); onlySenderWithdrawals() rejects any FundsWithdrawal input that isn't the sender's, including one from the sponsor's address balance (the same balance that pays gas, a direct drain the gas-coin check can't see, because the withdrawal is an input, not a command argument); simulationSucceeds() avoids paying for a transaction that aborts; and boundedExpiration() requires a bounded expiration for address-balance gas.

Two things the defaults don't do, by design (handle them at your service boundary):

  • No gas ceiling. A legitimate but expensive transaction can cost up to the protocol max. Add gasBudget({ max }) for a cap, and rate-limit / authenticate callers.
  • No execution guarantee. simulationSucceeds() is a dry-run; onchain state can shift before execution, so a later-aborting sponsored transaction can still charge the sponsor gas.

Keep defaults() unless you're deliberately replacing those checks.

For offline-only signing (no dry-run), use validators that read only data (no transactionResponse). Nothing depends on simulation, so the sponsor never simulates:

createSponsor({ signer, client, validate: [validSender(), gasCoinNotUsed()] });

What transaction is

The sponsor methods take a transaction that is anything Transaction.from accepts:

type TransactionInput = Transaction | Uint8Array | string; // a Transaction, built bytes, base64, or JSON

When you also pass a userSignature, the bytes are already final (the user signed them), so that form must be the exact bytes: a Uint8Array or a base64 string (not a Transaction or JSON). The sponsor never rebuilds them (so the user's signature stays valid). The sender is already part of those bytes, so there's no sender parameter in this flow. userSignature might be a single string or an array (for example, multiple required signers).

Next steps

  • Basic Usage covers the recommended service flow: client-built bytes, user signatures, network usage, and the advanced sponsor-builds flow.
  • Validation explains custom validation analyzers, request-scoped options, analyzer execution, and built-in validation analyzers.
  • Best Practices adds policy, gas, and operational guidance.

On this page