Gasless.cash SDK

Let your wallet users pay network fees in stablecoins — even with zero ETH.

@gasless-cash/sdk is a TypeScript SDK (built on viem) that adds gas sponsorship to any DeFi wallet. Your users sign a transaction with their own key; our relayer fronts the ETH for gas and is reimbursed in the stablecoin the user chose (USDC, USDT, DAI, or your allow-list). On EVM chains the flow is powered by EIP-7702, so it works with plain EOAs — no smart-contract wallet migration required. On TRON (which has no EIP-7702) the same SDK uses counterfactual smart accounts to deliver the identical experience — see TRON.

Who this is for

Wallet and DeFi app builders who want to remove the "you need ETH to move your USDC" dead-end. Integrate once and every user can transact, regardless of their native-token balance.

What you get

  • Cost estimation in the user's stablecoin, before they sign.
  • Sponsored batches — approve + swap + transfer atomically, gas paid in stablecoin.
  • Self-custody — the user signs everything; the SDK never holds keys or funds.
  • One codebase for web and React Native.
  • Trustless outcomes read directly from chain.

How it works

The SDK sits in your wallet (client side). It builds and signs payloads with the user's key, then hands them to the relayer backend over HTTP. The relayer holds the gas-paying key and submits to chain.

┌────────────┐   uses    ┌──────────────────┐   HTTP   ┌──────────────┐   tx
│ your wallet │ ────────▶ │ @gasless-cash/sdk │ ───────▶ │   relayer    │ ─────▶ chain
│ (user key) │           │ build · sign     │          │ (pays ETH gas)│
└────────────┘           └──────────────────┘          └──────────────┘
                                                          reimbursed in stablecoin
  1. Quote — the SDK asks the relayer what a batch will cost in the chosen stablecoin.
  2. Sign — the user signs an EIP-712 ExecuteBatch message (and, the first time, a one-time EIP-7702 authorization).
  3. Submit — the relayer broadcasts the transaction, fronts the gas, and collects the stablecoin fee on-chain.
  4. Resolve — your app reads the outcome from chain: EXECUTED or PAID_BUT_REVERTED.
Self-custodial

The relayer can never move funds on its own — it can only relay a transaction the user already signed. The SDK never sees the relayer key; the relayer never sees the user key.

API keys

Your API key authorizes your app to use the Gasless.cash relayer. You pass it once when constructing the client; the SDK attaches it to every relayer request for you.

  1. Email us at support@gasless.cash to request a key. Include your app's name, the chains and stablecoins you want to support, and (for web or mobile apps) the origins / bundle IDs the key should be locked to.
  2. We'll reply with an API key scoped to your app.
  3. Add it to your app config and pass it as apiKey.
const gasless = new GaslessClient({ chainId, publicClient, apiKey: process.env.GASLESS_API_KEY });
Client-side keys aren't secrets

A key shipped in a web or mobile app is visible to users — treat it as an identifier + quota, not a password. It can't move funds: every transaction still requires the end user's own signature. We additionally protect keys with per-key rate limits and origin / bundle-ID allow-listing we set up for your key — include your origins / bundle IDs in your request. For server-side use, keep the key secret as usual.

Lost or leaked a key? Email support@gasless.cash — old keys can be revoked instantly and a replacement issued.

Installation

Install the SDK and its viem peer dependency.

npm install @gasless-cash/sdk viem

The SDK is platform-agnostic and ships dual ESM/CJS builds with full type definitions. On the web it works out of the box. For React Native there's a one-line polyfill setup — see React Native.

Requirements

Node 18+ for tooling. viem ^2. A JSON-RPC endpoint for the chain you target (Alchemy, Infura, your own node, etc.).

Quickstart

A complete, end-to-end gasless USDC transfer in one file.

import { createPublicClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { GaslessClient, erc20Transfer } from "@gasless-cash/sdk";

const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const user = privateKeyToAccount(USER_PRIVATE_KEY); // or a connected WalletClient

const gasless = new GaslessClient({
  chainId: mainnet.id,
  publicClient,
  apiKey: GASLESS_API_KEY,   // request one at support@gasless.cash — that's it
});

const calls = [erc20Transfer(USDC, recipient, 5_000_000n)]; // 5 USDC (6 decimals)

// 1 · Quote the fee in USDC, show it to the user
const quote = await gasless.estimateCost({ eoa: user.address, calls, paymentToken: USDC });
console.log(`Fee: ${quote.paymentAmount} (smallest unit of USDC)`);

// 2 · Submit the sponsored batch (uses the quoted amount the user approved)
const { id } = await gasless.sendSponsoredBatch({ eoa: user.address, signer: user, calls, quote });

// 3 · Resolve the outcome from chain
const result = await gasless.waitForBatch(id);
if (result.outcome === "EXECUTED") console.log("Done!", result.txHash);
else console.log("User paid but the call reverted — do NOT resubmit", result.failureReason);
First-time users need a delegation

If the user's EOA hasn't been delegated to our implementation yet, include a one-time EIP-7702 authorization in the call. See First-time delegation — it's two extra lines.

Delegation & EIP-7702

EIP-7702 lets a regular EOA temporarily run smart-contract code. When a user delegates to our GasSponsorDelegation implementation, their EOA gains a executeBatch entrypoint that the relayer can call on their behalf — verifying the user's EIP-712 signature, pulling the stablecoin fee, and executing the batch atomically.

Delegation is a one-time, signed authorization per EOA. Once set, every subsequent sponsored batch is a plain call — no further authorization needed.

verifyingContract = the user's EOA

Under 7702 the delegate code runs as the EOA, so the EIP-712 domain binds to the EOA address, not the implementation. The SDK handles this for you — it's called out only so the signatures make sense when you inspect them.

The two nonces

This trips up almost everyone integrating 7702, so the SDK keeps them separate for you. There are two unrelated nonces in play:

NonceWhat it isWhere it comes from
App nonce Replay protection for the ExecuteBatch message. Single-use. GlobalNonceManager (the SDK reads it automatically).
Authorization nonce The EOA's protocol-level account nonce, used only for the 7702 authorization. eth_getTransactionCount via prepareAuthorization().

You never compute either by hand: estimateCost/sendSponsoredBatch fetch the app nonce, and prepareAuthorization fetches the account nonce.

Transaction outcomes

A sponsored batch resolves to one of two on-chain outcomes. Reading them correctly is essential — one of them is not a retry.

OutcomeMeaning
EXECUTEDThe user's calls succeeded. (BatchExecuted event.)
PAID_BUT_REVERTEDThe user's calls reverted, but the relayer was paid and the app nonce was consumed. (BatchExecutionFailed event.)
PAID_BUT_REVERTED is not a free retry

The nonce is spent and the fee was charged. Re-submitting the same signed payload will fail. Surface this to the user and have them start a fresh batch.

Configuration

Construct one GaslessClient per chain. You only provide three things — contract addresses and the relayer endpoint are built into the SDK per chain, so you never hardcode them.

const gasless = new GaslessClient({
  chainId: 1,
  publicClient,                  // viem PublicClient
  apiKey: GASLESS_API_KEY,       // your key — authorizes your app with the relayer
  defaultDeadlineSeconds: 3600,  // optional, default 3600
});
FieldTypeNotes
chainIdnumberTarget chain. Must be one the SDK supports.
publicClientPublicClientviem read client (nonce, delegation, receipts).
apiKeystringYour Gasless.cash API key. Sent to the relayer as the authorization header.
defaultDeadlineSecondsnumber?Signature validity window when no quote/deadline is given.

Check which chains are available with supportedChainIds().

Self-hosting (advanced)

Running your own contracts or relayer? You can override the built-ins with optional contracts, relayerBaseUrl, or a fully custom relayer transport. Most consumers never need these.

Estimating cost

Always quote before signing so the fee you display equals the fee the user pays. The quote's amount becomes the signed amount when you pass it to sendSponsoredBatch.

const quote = await gasless.estimateCost({
  eoa: user.address,
  calls,
  paymentToken: USDC,
});

// quote = {
//   paymentToken, paymentAmount,  // smallest unit, decimal string
//   deadline,                     // unix seconds, decimal string
//   quoteId,                      // forwarded to the relayer on submit
//   gasEstimate, ethCostWei,      // underlying gas + ETH cost
// }

const human = Number(quote.paymentAmount) / 1e6; // USDC has 6 decimals
showFeeToUser(`${human.toFixed(4)} USDC`);
Quotes expire

Quotes carry a short TTL to bound gas-price drift. Quote close to when the user confirms; if a quote expires, just request a fresh one.

Payment tokens

Render your fee-token picker from the relayer's allow-list for the current chain.

const { tokens } = await gasless.getPaymentTokens();
// tokens: [{ address, symbol, decimals, name?, logoUrl? }, …]

for (const t of tokens) {
  renderOption({ label: t.symbol, value: t.address, icon: t.logoUrl });
}

First-time delegation

If an EOA isn't yet delegated to our implementation, include a one-time EIP-7702 authorization with the batch. The relayer installs the delegation and runs the batch in the same transaction.

// 1 · Build the authorization args (uses the EOA ACCOUNT nonce)
const authArgs = await gasless.prepareAuthorization(user.address);

// 2 · The user signs it with their viem account
const authorization = await user.signAuthorization(authArgs);

// 3 · Pass it alongside the batch
const { id } = await gasless.sendSponsoredBatch({
  eoa: user.address,
  signer: user,
  calls,
  quote,
  authorization,   // omit on subsequent batches once delegated
});
When do I need this?

Only when the EOA isn't delegated yet (first use, or after re-delegation). The SDK's pre-flight tells you: if you didn't pass an authorization and one is required, it throws a PreflightError explaining exactly that.

Sending a batch

sendSponsoredBatch builds the params, runs a cheap pre-flight, signs the EIP-712 digest with the user's key, and submits to the relayer. It returns a handle for tracking.

const submission = await gasless.sendSponsoredBatch({
  eoa: user.address,        // the delegated EOA
  signer: user,             // viem Account or WalletClient (only needs signTypedData)
  calls,                    // SponsorTransaction[]
  quote,                    // from estimateCost — sets paymentToken/amount/deadline
  authorization,            // only on first-time delegation
  // paymentToken, paymentAmount, deadline — alternatives if you don't pass a quote
  // skipPreflight: true    — skip the on-chain paused/delegation checks
});
// submission = { id, status: "SUBMITTED", txHash? }

The signer

Any viem Account (e.g. privateKeyToAccount) or a connected WalletClient works — the SDK only needs signTypedData. The user signs; nothing custodial happens.

Tracking the outcome

The batch outcome is on-chain and verifiable. You have two ways to resolve it.

Relayer-driven

waitForBatch polls the relayer for the live transaction hash (following any fee-bump replacement) and for terminal rejections, then reads the receipt from chain and interprets it locally.

const result = await gasless.waitForBatch(submission.id, { pollMs: 2000, timeoutMs: 120_000 });
// result = { outcome, txHash, nonce, failureReason? }

Fully trustless

waitForBatchOnChain needs nothing from the relayer. The outcome events are indexed by (owner, nonce) and the app nonce is single-use, so polling chain logs yields a definitive result — naturally robust to fee-bump replacements.

const result = await gasless.waitForBatchOnChain({
  eoa: user.address,
  nonce: result.nonce,      // the app nonce from the signed batch
});
Which to use?

Use waitForBatch for the typical flow (it also reports pre-broadcast rejections). Use waitForBatchOnChain when you want zero relayer dependency for the success path.

Building calls

A batch is an array of SponsorTransaction objects. Helpers cover the common cases; createTransaction covers everything else.

import {
  erc20Transfer, erc20Approve, nativeTransfer, createTransaction,
} from "@gasless-cash/sdk";

const calls = [
  erc20Approve(USDC, dexRouter, 1_000_000n),         // approve
  createTransaction(dexRouter, swapCalldata),        // arbitrary call
  erc20Transfer(USDC, recipient, 250_000n),          // transfer
  nativeTransfer(recipient, 10n ** 16n),             // 0.01 ETH
];

All calls in a batch execute atomically — if one reverts, the user's calls roll back together (while the relayer still gets paid; see outcomes).

Direct (self-paid) batches

When the user does have ETH and just wants atomic batching without a relayer, use the direct path. The user pays their own gas; no signature or relayer is involved.

const txHash = await gasless.sendDirectBatch({
  walletClient,            // viem WalletClient bound to the EOA
  account,                 // viem Account
  calls,
  // txOverrides: { authorizationList, gas }  // if not yet delegated
});

Error handling

All SDK errors extend GaslessSdkError, so you can catch broadly or narrow by type.

import {
  GaslessSdkError, PreflightError, RelayerError, ContractRevertError,
} from "@gasless-cash/sdk";

try {
  await gasless.sendSponsoredBatch({ eoa, signer, calls, quote });
} catch (e) {
  if (e instanceof PreflightError) {
    // on-chain guard failed before submit (paused, not delegated, …)
  } else if (e instanceof RelayerError) {
    // non-2xx from the relayer; inspect e.status / e.body
  } else if (e instanceof ContractRevertError) {
    // decoded custom error, e.g. e.errorName === "DeadlineExpired"
  } else if (e instanceof GaslessSdkError) {
    // anything else from the SDK
  }
}
ErrorWhen
PreflightErrorPre-flight guard failed (service paused, EOA not delegated, relayer rejected, timeout).
RelayerErrorRelayer HTTP failure. Carries status and body.
ContractRevertErrorA decoded contract custom error. Carries errorName and args.
GaslessSdkErrorBase class for all of the above.

Use tryDecodeContractRevert(data) to turn raw revert bytes into a typed ContractRevertError (or null if it isn't a known error).

React Native

The SDK core is platform-agnostic. React Native (Hermes) just needs two polyfills that viem relies on. Install them and import the SDK's polyfill entry once, at the very top of your app entry — before any other import.

npm install @gasless-cash/sdk viem react-native-get-random-values text-encoding
// index.js / App entry — MUST be the first import
import "@gasless-cash/sdk/react-native";

// …then the rest of your app
import { registerRootComponent } from "expo";
import App from "./App";
registerRootComponent(App);

After that, use the SDK exactly as on the web:

import { GaslessClient, erc20Transfer } from "@gasless-cash/sdk";
Good to know

The polyfill import installs crypto.getRandomValues and (only when missing) TextEncoder/TextDecoder. On RN 0.74+ / Expo SDK 50+ those are built in, so the shim is a no-op. BigInt and fetch are already native in Hermes — no polyfill needed.

TRON (pay gas in USDT)

TRON has no EIP-7702, so the mechanism differs — but the SDK surface is just as small. Each user's EOA maps to a deterministic counterfactual smart account (a CREATE2 clone) that can receive USDT/TRX before it exists on-chain. The relayer deploys it lazily on the first sponsored batch, then calls it directly afterward. Live on TRON mainnet; the user pays fees in USDT.

  • No authorization step — there's no 7702 delegation, so nothing extra to sign or pass on the first transaction.
  • One nonce, held in the account contract (not two).
  • Values are SUN (1 TRX = 106 SUN), never wei — use the SDK's parseTrx/formatTrx.
  • Addresses on the wire are 20-byte 0x-hex; the base58 T… form is display-only (hexToTronAddress/tronAddressToHex).

Everything TRON lives in the dedicated entrypoint:

import {
  TronGaslessClient,
  TRON_MAINNET_CHAIN_ID,
  predictGaslessAccountAddressForChain,
} from "@gasless-cash/sdk/tron";

Deposit address (offline)

Because the account is counterfactual, you can show the user their deposit address the moment they have a key — no chain call, nothing deployed yet. Funds sent there are safe and become spendable on the first batch.

const { address, tronAddress } = predictGaslessAccountAddressForChain(
  TRON_MAINNET_CHAIN_ID,
  owner,                     // the user's EOA (0x-hex)
);
// tronAddress — the base58 "T..." to display as the deposit address

Quote & send on TRON

Same shape as the EVM flow — quote → sign once → send → wait — via TronGaslessClient. There is no authorization field, and the payment defaults to the network's USDT.

import { erc20Transfer } from "@gasless-cash/sdk";
import { privateKeyToAccount } from "viem/accounts";

const owner = privateKeyToAccount(userKey);
const gasless = new TronGaslessClient({
  chainId: TRON_MAINNET_CHAIN_ID,
  account: owner,            // signs TIP-712 (only .address + .signTypedData used)
  apiKey,
});

const usdt = "0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C"; // TRON USDT (0x-hex form)
const transactions = [erc20Transfer(usdt, recipient, amount)];

// 1 · Quote in USDT before the user signs
const quote = await gasless.getQuote({ transactions, paymentToken: usdt });
// quote.paymentAmount (USDT smallest unit), quote.gasEstimate (energy), quote.accountDeployed

// 2 · Sign once + submit (no authorization on TRON)
const { id } = await gasless.execute({ transactions, paymentToken: usdt, quote });

// 3 · Resolve the outcome from the TRON receipt
const { outcome, txId } = await gasless.waitForBatch(id);
// outcome: "EXECUTED" | "PAID_BUT_REVERTED" | "RELAYER_TX_FAILED"
Account state

gasless.getAccountState() returns { account, accountBase58, deployed, nonce }. When deployed is false, the first execute deploys the account and runs the batch in a single transaction.

API reference

new GaslessClient(config)

Creates a client for one chain. See Configuration.

estimateCost(args)

{ eoa, calls, paymentToken } → Promise<QuoteResponse>. Quotes the batch cost in the chosen stablecoin. Pass the returned quote to sendSponsoredBatch.

getPaymentTokens()

() → Promise<PaymentTokensResponse>. The relayer's accepted stablecoins for this chain.

prepareAuthorization(eoa)

(eoa) → Promise<{ chainId, address, nonce }>. Builds the EIP-7702 authorization args (with the EOA account nonce) for the user to sign.

sendSponsoredBatch(args)

{ eoa, signer, calls, quote?, authorization?, paymentToken?, paymentAmount?, deadline?, skipPreflight? } → Promise<{ id, status, txHash? }>. Builds, signs, and submits a sponsored batch.

waitForBatch(id, opts?)

(id, { pollMs?, timeoutMs? }) → Promise<BatchResult>. Relayer-driven resolution. Throws PreflightError on rejection/timeout.

waitForBatchOnChain(args)

{ eoa, nonce, fromBlock?, pollMs?, timeoutMs? } → Promise<BatchResult>. Trustless resolution via indexed chain logs.

sendDirectBatch(args)

{ walletClient, account, calls, txOverrides? } → Promise<Hex>. Self-paid atomic batch (no relayer, no signature).

Call builders

  • erc20Transfer(token, to, amount)
  • erc20Approve(token, spender, amount)
  • nativeTransfer(to, value)
  • createTransaction(to, data, value?)
  • totalValue(calls) — sum of native value across a batch

Types

  • SponsorTransaction{ to, value, data }
  • QuoteResponse{ paymentToken, paymentAmount, deadline, quoteId, gasEstimate, ethCostWei }
  • BatchResult{ outcome, txHash, nonce, failureReason? }
  • BatchOutcome"EXECUTED" | "PAID_BUT_REVERTED" | "PENDING" | "REJECTED"
  • PaymentToken{ address, symbol, decimals, name?, logoUrl? }

FAQ

Do my users need ETH?

No — that's the point. They need the stablecoin they're paying the fee in (plus whatever the batch itself moves).

Is this custodial?

No. The user signs every batch with their own key. The relayer can only relay a transaction the user already authorized; it can't move funds independently.

What chains and tokens are supported?

The relayer is configured per chain with an allow-list of stablecoins. Call getPaymentTokens() to read the live list for the chain you're on.

What happens if the user's transaction reverts?

You get PAID_BUT_REVERTED: the user paid the fee and the nonce was consumed, but their calls rolled back. It is not a retry — start a fresh batch.

Does it work with WalletConnect / injected wallets?

Yes. Pass a viem WalletClient as the signer; the SDK only needs signTypedData (and signAuthorization for first-time delegation).

How do I get an API key?

Email us at support@gasless.cash with your app's name and the chains you want to support. You'll get a key plus the contract addresses for each chain.