A technical walkthrough of fee-bump sponsorship for Soroban dApps — shipped in production on Nova Esusu, a rotating savings circles app on Stellar.
If you're building a Soroban dApp for people who have never held crypto, you will hit the same wall I did: the first transaction is the hardest one. A new user has to acquire XLM, create an account, pay a base reserve, and fund a transaction fee — all before they experience a single feature of your product.
For my first 51 testnet testers this was friction-free (thanks, friendbot). But mainnet has no faucet. So before launching Nova Esusu — on-chain rotating savings circles (the West African Esusu tradition) — on mainnet, I needed an answer to one question:
Can a new user join a savings circle without ever owning a fee?
The answer is yes, and it's built into Stellar itself: fee-bump transactions.
The pattern: user signs, sponsor pays
Stellar's transaction model separates two things that most chains conflate:
-
source— the account whose sequence number the transaction consumes, and whose signature authorizes the operations - fee source — the account that pays the network fee
A fee-bump envelope (ENVELOPE_TYPE_TX_FEE_BUMP) wraps a fully-signed inner transaction with a second payer:
const feeBump = TransactionBuilder.buildFeeBumpTransaction(
sponsorKeypair.publicKey(), // feeSource — pays everything
(innerFee * 2).toString(), // declared fee, capped
innerTransaction, // signed by the user only
networkPassphrase
);
feeBump.sign(sponsorKeypair);
The magic: the user's signature only ever authorizes the inner operations. The sponsor's signature only authorizes paying. Neither can spend the other's funds. This is the safest possible trust boundary — the sponsor is not a custodian, not a co-signer, and cannot move the user's money.
The architecture
Browser Serverless API Stellar
| | |
| 1. sign inner tx (user) | |
|-------------------------------> |
| 2. POST { xdr, network } | |
|----------------------------->| |
| | 3. validate rails |
| | 4. wrap + sign fee-bump |
| | 5. submit ----------------->|
| | |
| <------ 6. { hash } ---------|---------- chain ---------- |
Three pieces, ~150 lines total:
1. The shared builder (shared/feebump.ts) — wraps any user-signed XDR in a fee-bump envelope. This module typechecks under both the Vite toolchain and the serverless toolchain, which matters more than it sounds (ask me how I know).
2. The sponsor endpoint (api/sponsor.ts, Vercel serverless) — receives the signed inner transaction, validates it, wraps, signs, submits. When the sponsor key is absent it returns 503, and the frontend silently falls back to normal submission — the user never sees an error, the feature just gracefully disappears.
3. The frontend opt-in — a "Gasless" checkbox on the join/create flows. Opt-in, never default-hidden, so reviewers can see exactly what's happening.
The abuse rails (read this before you ship one)
An open "someone else pays my fees" endpoint is a generous gift to the internet. Mine has four rails, all enforced server-side:
-
Fee cap — the inner transaction's declared fee is capped (5 XLM). A malicious client can't submit
MAX_INTand drain the sponsor. - Sponsor ≠ source — the endpoint refuses to sponsor a transaction whose inner source is the sponsor itself (prevents using the endpoint as a weird signing oracle).
- Already-bumped envelopes rejected — only raw inner transactions are accepted, one wrap only.
-
Network pinned per environment —
testnetandmainnetsponsor keys are separate env vars; a mainnet request can never be signed by the testnet wallet.
The cap matters most. Soroban fees are based on declared resource limits — an adversarial client could declare enormous instructions. Cap early, cap server-side.
Proof it works: a fresh user, zero fees, on-chain
I verified the full path in production: created a brand-new keypair, funded it with the minimum, had it sign a create_circle call, POSTed to the live endpoint, and confirmed on-chain:
- Transaction:
912b64619d39e2f6800894685a37adee77711bb5f58af1957da388df1b24b94a - Envelope type:
ENVELOPE_TYPE_TX_FEE_BUMP - Fee source: the sponsor wallet
- The user account's balance: unchanged except for the operation itself
Things that cost me an afternoon (so they don't cost you one)
-
The SDK wants class instances.
server.sendTransaction()acceptsTransaction | FeeBumpTransaction, not a raw XDR object — a base64 string serialized as an object gets rejected by the RPC with a wonderfully unhelpfulinvalid parameters. -
Vercel functions are Express-style. A web-standard
Request/Responsehandler doesn't hang on import — it hangs on invocation: the runtime waits forres.end()forever and you getFUNCTION_INVOCATION_TIMEOUT. Write(req, res)handlers. -
Soroban rejects classic operations.
bumpSequence,manageDataet al. fail simulation (unsupported operation type). Your inner transaction must be contract calls. - Fee-bump signing order is irrelevant but presence isn't. The inner signature must exist before wrapping — you're signing the hash of the inner tx, and the sponsor signs the hash of the wrapped envelope.
Why this matters for adoption
Every chain ecosystem says it wants "the next billion users." Most wallets in the markets that actually need savings circles (West Africa, Southeast Asia, Latin America) start empty. Fee sponsorship turns a four-step onboarding (get an exchange account → buy XLM → withdraw → fund transactions) into a one-step one (connect wallet, join).
The Esusu circle my testers joined cost them exactly zero in fees on testnet, and on mainnet the sponsor pool will carry the fees for the first wave of members. The people who need savings infrastructure the most are the ones least able to pay friction costs to try it. Fee sponsorship is how a protocol says "the first round is on me."
Nova Esusu is open source: github.com/ubongn/stellar-nova-esusu. Contracts: SavingsPool (escrow + payouts) and MemberManager (reputation + eligibility), with the payout order locked on-chain at circle activation. Questions or feedback — find me on X @ubongn.