Skip to content

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) ​

  1. Open Fuel Station in the dashboard and pick a testnet project.
  2. Choose Create tank. Name it default so you can omit the tank parameter later.
  3. 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.

bash
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.

bash
mkdir fuel-quickstart && cd fuel-quickstart && bun init -y >/dev/null
bun add @mysten/sui @protobuf-ts/grpc-transport @grpc/grpc-js
ts
// 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)
bash
bun run quickstart.ts

Expected output:

json
{
  "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 seeFix
401 from the APIThe key is missing the Sponsor scope, or belongs to another project.
TANK_NOT_FOUNDNo tank named default in the key's project. Create it or pass ?tank=.
INSUFFICIENT_TANK_FUNDSThe deposit has not been credited yet, or the tank is empty. Wait a minute.
GAS_OWNER_MISMATCHsetGasOwner is not config.sponsorAddress.
TRANSACTION_EXPIREDThe epoch rolled between config and submit. Run the script again.
sponsorshipAvailable: falseInodra 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.

The full-stack Sui data layer.