Skip to content
⌘K

For game developers

Gas sponsoring

How rare.market pays the network fee for a new player's first essential actions on Sui, which transactions qualify, the limits, and a full example.

rare.market can pay the Sui network fee for a new player's first essential actions, so a wallet with no SUI can still claim a trophy or make a first swap. The player's wallet builds only the transaction kind (its commands). rare.market adds the gas, checks the transaction, signs as the gas owner, and submits it once the player has signed the same bytes.

What is sponsored

Sponsoring covers a short list of actions, judged from the transaction's commands. Each transaction must be exactly one of these categories.

CategoryWhat qualifies
trophyThe Rare Trophies claim calls rare.market enables (see Trophies for studios). The trophy goes to the claimer only.
tradeA player's first swap through Rare's router, paying out only to the player and the router's fee wallet.
rescueMoving the player's assets out of their previous wallet (for example a Playtron wallet) to one new wallet: object transfers, kiosk delist, take and withdraw, and personal kiosk borrow and return.
walrusWalrus storage calls. Off unless rare.market turns it on.

Your game's own Move calls are not sponsored. A transaction that calls any package outside these categories is refused with not_sponsored. Check categories in GET /sponsor/status for what is switched on right now.

Only wallets holding 0.1 SUI or less are sponsored. Wallets with more pay their own fee.

Always refused

  • Any use of the gas coin as an argument, including SplitCoins from the gas coin.
  • Withdrawals from any address balance but the sender's.
  • Publish and Upgrade.
  • Transfers to the sponsor, and recipients that are not plain addresses.
  • Mixing categories in one transaction, and empty transactions.

The transaction must also pass two dry runs: it must succeed, cost at most 0.05 SUI net gas and 0.02 SUI net storage, and change no balance of the sponsor's other than the gas itself.

The flow

  1. Build the transaction kind

    Build your Transaction with onlyTransactionKind: true and base64 the bytes. Never pay from the gas coin: the sponsor's coin pays the gas.

  2. POST /sponsor

    Send { sender, kind, category }. rare.market checks the policy and limits, sets itself as gas owner, dry runs the transaction twice, signs, and returns the full transaction bytes with its signature.

  3. The player signs the same bytes

    Check that the bytes carry your sender and your kind, then have the player's wallet sign them with signTransaction (not signAndExecuteTransaction).

  4. POST /sponsor/execute

    Send { digest, signature } within 2 minutes. rare.market adds its signature and submits the transaction once.

POST /sponsor and POST /sponsor/execute send no CORS headers, so a web page can't call them from the browser. Call them from your game server or a native client, and have the browser only sign. GET /sponsor/status is part of the public API: any origin can read it, and it counts toward your rate limit like other reads.

Request a sponsorship

POST/sponsor
FieldTypeRequiredMeaning
senderstringYesThe player's address.
kindstringYesBase64 TransactionKind bytes, at most 200,000 characters.
categorystringNotrophy, trade, rescue or walrus. When given, it must match what the transaction is.
curl -X POST "https://rare.market/api/v1/sponsor" \
  -H "content-type: application/json" \
  -d '{"sender":"0x7a1f...c3d2","kind":"AAMBAQ...","category":"trophy"}'
Response
{
  "data": {
    "bytes": "AAADAQ...AAA=",
    "sponsorSignature": "AHq3...Q==",
    "digest": "8nTq...v4Yx",
    "sponsor": "0x...",
    "category": "trophy",
    "gasBudget": "13875000",
    "estimatedGasMist": "9700000",
    "expiresAt": 1793041320000
  }
}

Long values are shortened here.

FieldMeaning
bytesBase64 TransactionData: your kind, your sender, and the sponsor as gas owner. The player signs exactly these bytes.
sponsorSignatureThe sponsor's signature over bytes. rare.market adds it at execute; you don't need to send it.
digestThe transaction digest. Pass it to /sponsor/execute.
sponsorThe sponsor's address (the gas owner).
categoryThe category the transaction was judged as.
gasBudgetThe gas budget set on the transaction, in MIST.
estimatedGasMistNet gas measured in the dry run, in MIST.
expiresAtMilliseconds since epoch. After this, ask again.

Sending the same request again while its sponsorship has more than 15 seconds left returns the same sponsorship.

Execute

POST/sponsor/execute
FieldTypeRequiredMeaning
digeststringYesThe digest from /sponsor.
signaturestringYesThe player's base64 signature over bytes.
curl -X POST "https://rare.market/api/v1/sponsor/execute" \
  -H "content-type: application/json" \
  -d '{"digest":"8nTq...v4Yx","signature":"AFb2...dw=="}'
Response
{
  "data": {
    "digest": "8nTq...v4Yx",
    "ok": true,
    "gasUsedMist": "9214880"
  }
}

ok is false, with an error, when the transaction ran on chain but failed. Its gas was still paid and counts toward the player's limits. A digest executes at most once.

Check eligibility

GET/sponsor/status

Pass address to see what this player can get right now.

curl "https://rare.market/api/v1/sponsor/status?address=0x7a1f...c3d2"
FieldMeaning
enabledSponsoring is on.
fundedThe sponsor has enough SUI to sponsor.
addressThe sponsor's address.
balanceMist, coinBalanceMist, addressBalanceMistThe sponsor's SUI, in total, in gas coins and in its address balance.
pool{ coins, free, busy }: the sponsor's gas coins.
dailyBudgetMist, spentTodayMist, remainingTodayMist, sponsoredTodayToday's overall budget and use (UTC day).
categories{ rescue, trophy, trade, walrus }: which categories are switched on.
maxUserSuiMistWallets holding more SUI than this pay their own fee.
limitsperAddressTx, perAddressGasMist, perCategoryLifetime, perCategoryDaily, maxNetGasMist.
youWith address only: sponsored (count so far), gasMist, tradeUsed, suiMist (the player's SUI) and eligible (per category, true or false).

Limits

LimitValue
Sponsored transactions per address, ever25
Net gas per address, ever0.25 SUI
trophy per address50 ever, 10 per day
trade per address1 ever
rescue per address20 ever, 20 per day
walrus per address20 ever, 5 per day
Sponsorships waiting for a signature2 per address, 4 per IP
Sponsored transactions per IP60 per day
Overall10 SUI and 3,000 transactions per day
Net gas per transaction0.05 SUI
Net storage per transaction0.02 SUI
Requests to /sponsor20 per minute per IP, 10 per minute per address
Requests to /sponsor/execute30 per minute per IP
Time to sign2 minutes

Days are UTC days. A sponsorship counts from the moment rare.market signs it; one that expires without being sent stops counting.

Errors

Errors have the body { "error": { "code", "message" } }. The message is a plain sentence you can show the player.

Response
{
  "error": {
    "code": "not_sponsored",
    "message": "This wallet has enough SUI to pay its own fee"
  }
}
StatusCodeMeaning
400bad_requestA missing or malformed field: sender, kind, category, digest or signature.
400rejectedThe network rejected the transaction before running it (for example a bad signature). Nothing ran.
403not_sponsoredThe policy refused it. The message says why.
409already_executedThis digest was already sent.
410sponsorship_expiredMore than 2 minutes passed. Ask for a new sponsorship.
422simulation_failedThe dry run failed: the transaction would fail right now.
429limit_reachedThis wallet used its covered fees (overall, per category or today).
429rate_limitedToo many requests, or too many sponsorships waiting for a signature.
429budget_exhaustedToday's overall budget is used up.
502submit_failedThe transaction was sent but its outcome is unknown. Check the chain before trying again.
503sponsor_unavailableSponsoring is off, not funded, or the chain can't be reached.
503sponsor_busyAll gas coins are in use. Try again in a moment.

Treat every refusal as a normal outcome: fall back to the player paying their own fee.

Full example

A trophy claim, sponsored. This runs in Node 18 or later with @mysten/sui. A keypair stands in for the player so the example runs on its own; in your game, the player's wallet signs bytes with signTransaction (for example dapp-kit's useSignTransaction with transaction: bytes).

TypeScript
import { bcs } from "@mysten/sui/bcs";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
import { fromBase64, normalizeSuiAddress, toBase64 } from "@mysten/sui/utils";

const API = "https://rare.market/api/v1";
const client = new SuiGrpcClient({ network: "mainnet", baseUrl: "https://fullnode.mainnet.sui.io:443" });

type Category = "trophy" | "trade" | "rescue" | "walrus";

async function post<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${API}${path}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error?.message ?? `HTTP ${res.status}`), { code: json.error?.code, status: res.status });
  return json.data as T;
}

/** The sponsored bytes must carry our sender, our kind, and the sponsor as gas owner. */
function checkSponsored(bytes: string, kind: string, sender: string, sponsor: string) {
  const data = bcs.TransactionData.parse(fromBase64(bytes)).V1;
  if (!data) throw new Error("unknown transaction version");
  if (normalizeSuiAddress(data.sender) !== normalizeSuiAddress(sender)) throw new Error("sender changed");
  if (normalizeSuiAddress(data.gasData.owner) !== normalizeSuiAddress(sponsor)) throw new Error("gas owner is not the sponsor");
  if (toBase64(bcs.TransactionKind.serialize(data.kind).toBytes()) !== kind) throw new Error("transaction changed");
}

export async function runSponsored(tx: Transaction, sender: string, category: Category, sign: (bytes: Uint8Array) => Promise<string>) {
  // 1. Kind bytes only. The sponsor's coin pays the gas, so never touch tx.gas.
  tx.setSenderIfNotSet(sender);
  const kind = toBase64(await tx.build({ client, onlyTransactionKind: true }));

  // 2. Ask rare.market to cover the fee.
  const s = await post<{ bytes: string; digest: string; sponsor: string; expiresAt: number }>("/sponsor", { sender, kind, category });
  checkSponsored(s.bytes, kind, sender, s.sponsor);

  // 3. The player signs exactly these bytes.
  const signature = await sign(fromBase64(s.bytes));

  // 4. rare.market adds its signature and submits.
  const r = await post<{ digest: string; ok: boolean; error?: string; gasUsedMist: string }>("/sponsor/execute", { digest: s.digest, signature });
  if (!r.ok) throw new Error(r.error ?? "transaction failed");
  return r.digest;
}

// --- usage: claim a Rare trophy for a player with no SUI ---
const player = Ed25519Keypair.fromSecretKey(process.env.PLAYER_KEY!);
const sender = player.toSuiAddress();

const tx = new Transaction();
// ...add the trophy claim calls here (see /docs/games/trophies)...

try {
  const digest = await runSponsored(tx, sender, "trophy", async (bytes) => (await player.signTransaction(bytes)).signature);
  console.log("claimed, fee covered by rare.market:", digest);
} catch (e) {
  const err = e as Error & { code?: string };
  // not_sponsored, limit_reached, sponsor_unavailable, ...: let the player pay their own fee instead.
  console.log("not sponsored:", err.code, err.message);
}

If /sponsor/execute answers sponsorship_expired, ask /sponsor for a new sponsorship and have the player sign again.

--
TPS:--