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.
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
- Quote — the SDK asks the relayer what a batch will cost in the chosen stablecoin.
- Sign — the user signs an EIP-712
ExecuteBatchmessage (and, the first time, a one-time EIP-7702 authorization). - Submit — the relayer broadcasts the transaction, fronts the gas, and collects the stablecoin fee on-chain.
- Resolve — your app reads the outcome from chain:
EXECUTEDorPAID_BUT_REVERTED.
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.
- 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.
- We'll reply with an API key scoped to your app.
- Add it to your app config and pass it as
apiKey.
const gasless = new GaslessClient({ chainId, publicClient, apiKey: process.env.GASLESS_API_KEY });
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.
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);
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.
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:
| Nonce | What it is | Where 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.
| Outcome | Meaning |
|---|---|
EXECUTED | The user's calls succeeded. (BatchExecuted event.) |
PAID_BUT_REVERTED | The user's calls reverted, but the relayer was paid and the app nonce was consumed. (BatchExecutionFailed event.) |
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
});
| Field | Type | Notes |
|---|---|---|
chainId | number | Target chain. Must be one the SDK supports. |
publicClient | PublicClient | viem read client (nonce, delegation, receipts). |
apiKey | string | Your Gasless.cash API key. Sent to the relayer as the authorization header. |
defaultDeadlineSeconds | number? | Signature validity window when no quote/deadline is given. |
Check which chains are available with supportedChainIds().
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 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
});
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
});
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
}
}
| Error | When |
|---|---|
PreflightError | Pre-flight guard failed (service paused, EOA not delegated, relayer rejected, timeout). |
RelayerError | Relayer HTTP failure. Carries status and body. |
ContractRevertError | A decoded contract custom error. Carries errorName and args. |
GaslessSdkError | Base 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";
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 base58T…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"
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.