Quickstart: first sponsored transaction
Ten minutes from an empty account to a transaction that a wallet with no SUI signs and your tank pays for. Everything here runs on testnet. Mainnet is the same flow with a mainnet project.
1. Create a tank (dashboard, 2 minutes)
- Open Fuel Station in the dashboard and pick a testnet project.
- Choose Create tank. Name it
defaultso you can omit thetankparameter later. - Copy the tank's deposit address from the tank card.
The tank starts with the default rules: 0.05 SUI max gas budget per transaction, no allowlists. Good for a first run.
2. Fund it (2 minutes)
Send 0.5 testnet SUI to the deposit address from any testnet wallet, or use the dashboard Faucet page to get testnet SUI first. Address balance and coin objects both work. The tank card shows the balance within a minute.
3. Mint a key with the Sponsor scope (1 minute)
Under API keys, create a key for the same project with the Sponsor and Data scopes. Sponsor lets the key submit and read tank state; Data lets the build step resolve objects through the gRPC gateway.
export INODRA_API_KEY=ik_...4. Run the script (5 minutes)
The user is a fresh keypair with no SUI. It calls the on-chain clock, which needs no assets, so nothing else has to exist. Swap the Move call for your own once it works.
mkdir fuel-quickstart && cd fuel-quickstart && bun init -y >/dev/null
bun add @mysten/sui @protobuf-ts/grpc-transport @grpc/grpc-js// quickstart.ts
import { credentials } from '@grpc/grpc-js'
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
import { Transaction } from '@mysten/sui/transactions'
import { toBase64 } from '@mysten/sui/utils'
import { GrpcTransport } from '@protobuf-ts/grpc-transport'
const API = 'https://testnet-api.inodra.com/v1'
const KEY = process.env.INODRA_API_KEY!
const headers = { 'x-api-key': KEY, 'Content-Type': 'application/json' }
// A user with no SUI at all.
const user = Ed25519Keypair.generate()
// 1. Tank config: sponsor address, gas price, epoch, chain, max budget.
const { data: config } = await fetch(`${API}/gas/config`, { headers }).then((r) => r.json())
if (!config.sponsorshipAvailable) throw new Error('sponsorship paused')
// 2. Build with the tank as gas owner and an empty gas payment.
const client = new SuiGrpcClient({
network: 'testnet',
transport: new GrpcTransport({
host: 'testnet-grpc.inodra.com:443',
channelCredentials: credentials.createSsl(),
meta: { 'x-api-key': KEY }
})
})
const tx = new Transaction()
tx.setSender(user.toSuiAddress())
tx.setGasOwner(config.sponsorAddress)
tx.setGasPayment([])
tx.setGasPrice(BigInt(config.gasPrice))
tx.setGasBudget(5_000_000n) // 0.005 SUI, well under the tank max
tx.setExpiration({
ValidDuring: {
minEpoch: config.epoch,
maxEpoch: String(BigInt(config.epoch) + 1n),
minTimestamp: null,
maxTimestamp: null,
chain: config.chainIdentifier,
nonce: crypto.getRandomValues(new Uint32Array(1))[0]
}
})
tx.moveCall({ target: '0x2::clock::timestamp_ms', arguments: [tx.object('0x6')] })
const bytes = await tx.build({ client })
// 3. The user signs the exact bytes.
const { signature } = await user.signTransaction(bytes)
// 4. Submit. Inodra checks the rules, adds the sponsor signature, executes.
const res = await fetch(`${API}/gas/submit`, {
method: 'POST',
headers,
body: JSON.stringify({ transactionBytes: toBase64(bytes), userSignature: signature })
})
const body = await res.json()
if (!res.ok) throw new Error(`${body.code}: ${body.error}`)
console.log(body.data)bun run quickstart.tsExpected output:
{
"digest": "8Qk…Zp",
"tank": "default",
"status": "success",
"gasUsedMist": "1000000",
"feeMist": "1000000",
"effects": { "executionError": null, "epoch": "1221", "gas": { "…": "…" } }
}The user address never held SUI. The tank paid the gas plus the 0.001 SUI fee.
5. See it (30 seconds)
- The tank card in the dashboard lists the sponsorship with its digest, gas, and fee, and the spendable balance dropped by that much.
GET /v1/gas/transactions/{digest}returns the same record from the API.- The digest opens in any Sui explorer. The sender is your throwaway user, the gas owner is the tank.
If something fails
| What you see | Fix |
|---|---|
401 from the API | The key is missing the Sponsor scope, or belongs to another project. |
TANK_NOT_FOUND | No tank named default in the key's project. Create it or pass ?tank=. |
INSUFFICIENT_TANK_FUNDS | The deposit has not been credited yet, or the tank is empty. Wait a minute. |
GAS_OWNER_MISMATCH | setGasOwner is not config.sponsorAddress. |
TRANSACTION_EXPIRED | The epoch rolled between config and submit. Run the script again. |
sponsorshipAvailable: false | Inodra paused sponsorship. Try again later. |
Every other code is in Errors.
Next
- Move the build and submit into your backend and let your app's wallet sign. See Sponsoring transactions.
- Bound what the tank pays for with rules before you ship a Sponsor key anywhere near a browser.
- Read Funding and refunds for deposits, fees, and getting SUI back.