This tutorial shows how to use the pre-funded sponsorship flow, where a sponsor allocates XRP upfront that a sponsee draws on for fees and reserves. In this example, a sponsor onboards a new user who holds no XRP, then sets up a pool the user can spend without further approval.
Use the default pre-funded flow when sponsees must be able to transact without waiting on the sponsor. If the sponsor needs to review each transaction without setting up a pool, use co-signing instead.
Requires the Sponsor amendment. Loading...
By the end of this tutorial, you should be able to:
- Create a pre-funded sponsorship pool for a sponsee.
- Submit a sponsored transaction that draws on the pool.
- Confirm what the pool spent on fees and reserves.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger and Sponsored Fees and Reserves.
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
- JavaScript with the xrpl.js library. See Get Started Using JavaScript for setup steps.
- Python with the xrpl-py library. See Get Started Using Python for setup steps.
You can find the complete source code for this tutorial's example in the code samples section of this website's repository.
From the code sample folder, use npm to install dependencies:
npm installImport the necessary libraries and instantiate a client to connect to the XRPL. This example imports:
xrpl: Used for XRPL client connection, transaction submission, and wallet handling.
import {
Client,
PaymentFlags,
SponsorFlags,
Wallet,
addPreFundedSponsor,
validate
} from 'xrpl'
// Connect to the network ----------------------
const client = new Client('wss://s.devnet.rippletest.net:51233')
await client.connect()Fund the sponsor and generate a key pair for the sponsee. Only the sponsor needs XRP, because it covers every cost in this example.
// Create the sponsor and sponsee wallets ----------------------
console.log(`\n=== Creating the sponsor and sponsee wallets... ===`)
const { wallet: sponsor } = await client.fundWallet()
const sponsee = Wallet.generate()
console.log(`Sponsor address: ${sponsor.address}`)
console.log(`Sponsee address: ${sponsee.address}`)Create a Payment transaction with the tfSponsorCreatedAccount flag enabled to create the sponsee's account. The flag makes the sponsor responsible for the new account's reserve, so the payment only needs to deliver the smallest possible XRP amount.
// Prepare Payment transaction to create the sponsee's account ----------------------
console.log(`\n=== Preparing Payment transaction to create the sponsee's account... ===`)
const createAccountTx = {
TransactionType: 'Payment',
Account: sponsor.address,
Destination: sponsee.address,
Amount: '1',
Flags: PaymentFlags.tfSponsorCreatedAccount
}
validate(createAccountTx)
console.log(JSON.stringify(createAccountTx, null, 2))
// Submit the Payment transaction ----------------------
console.log(`\n=== Submitting Payment transaction... ===`)
const createAccountResponse = await client.submitAndWait(createAccountTx, {
wallet: sponsor,
autofill: true
})
if (createAccountResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = createAccountResponse.result.meta.TransactionResult
console.error(`Error: Unable to create the sponsee's account:`, resultCode)
await client.disconnect()
process.exit(1)
}
console.log('Sponsee account created successfully!')
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${createAccountResponse.result.hash}`)To create the pre-funded pool (Sponsorship entry), prepare a SponsorshipSet transaction.
// Prepare SponsorshipSet transaction ----------------------
console.log(`\n=== Preparing SponsorshipSet transaction... ===`)
const sponsorshipSetTx = {
TransactionType: 'SponsorshipSet',
Account: sponsor.address,
Sponsee: sponsee.address,
FeeAmountDelta: '1000000',
MaxFee: '1000',
RemainingOwnerCountDelta: 5
}
validate(sponsorshipSetTx)
console.log(JSON.stringify(sponsorshipSetTx, null, 2))Set MaxFee with enough headroom for changes in the network's required transaction cost. A cap near the current minimum can block the sponsee's transactions when the cost rises. Monitor the pool's FeeAmount so it can be topped up before it runs out.
The FeeAmountDelta field represents the drops available for fees, MaxFee caps what the pool pays for any single transaction, and RemainingOwnerCountDelta is the number of owner reserves the sponsor covers.
The two delta fields are amounts to add to the pool's current values (FeeAmount and RemainingOwnerCount), not replacements. For this example, the pool is new so each delta becomes its starting value. Both fields also accept a negative value:
- A negative
FeeAmountDeltareturns the unspent XRP to the sponsor. - A negative
RemainingOwnerCountDeltalowers how many owner reserves the pool covers.
A negative delta is a subtraction, so neither field goes below zero. If you subtract more than a field has left, that field drops to zero instead, as long as the other one stays positive. A subtraction that would leave both at zero fails with tecNO_PERMISSION.
Sign and submit the SponsorshipSet transaction.
// Submit the SponsorshipSet transaction ----------------------
console.log(`\n=== Submitting SponsorshipSet transaction... ===`)
const sponsorshipResponse = await client.submitAndWait(sponsorshipSetTx, {
wallet: sponsor,
autofill: true
})
if (sponsorshipResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = sponsorshipResponse.result.meta.TransactionResult
console.error('Error: Unable to create the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
// Extract the Sponsorship entry from the transaction result ----------------------
const sponsorshipNode = sponsorshipResponse.result.meta.AffectedNodes.find(
node => node.CreatedNode?.LedgerEntryType === 'Sponsorship'
)
console.log('Sponsorship created successfully!')
console.log(`Sponsorship ID: ${sponsorshipNode.CreatedNode.LedgerIndex}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${sponsorshipResponse.result.hash}`)The sponsor pays the FeeAmountDelta up front and must also meet the reserve requirement for the new Sponsorship entry. If it can't cover those costs, the transaction fails with tecUNFUNDED.
Each pool is a single Sponsorship entry that serves one sponsee. To fund several sponsees, the sponsor must create a pool for each one and hold an owner reserve for every pool.
By default, the sponsee can spend from the pool without further sponsor approval. A sponsor can also require a signature on each use by enabling the tfSponsorshipSetRequireSignForFee and tfSponsorshipSetRequireSignForReserve flags. In that variation, transactions still draw from the pre-funded pool, but each sponsored transaction must also include the sponsor's signature.
Submit the sponsored transaction and wait for validation.
The addPreFundedSponsor helper adds the Sponsor and SponsorFlags fields for a transaction that draws from an existing pre-funded Sponsorship entry.
// Prepare the sponsored DepositPreauth transaction ----------------------
console.log(`\n=== Preparing sponsored DepositPreauth transaction... ===`)
const depositPreauthTx = addPreFundedSponsor(
{
TransactionType: 'DepositPreauth',
Account: sponsee.address,
Authorize: sponsor.address
},
sponsor.address,
SponsorFlags.spfSponsorFee | SponsorFlags.spfSponsorReserve
)
validate(depositPreauthTx)
console.log(JSON.stringify(depositPreauthTx, null, 2))
// Submit the sponsored DepositPreauth transaction ----------------------
console.log(`\n=== Submitting sponsored DepositPreauth transaction... ===`)
const submitResponse = await client.submitAndWait(depositPreauthTx, {
wallet: sponsee,
autofill: true
})
if (submitResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = submitResponse.result.meta.TransactionResult
console.error('Error: Unable to create the preauthorization:', resultCode)
await client.disconnect()
process.exit(1)
}
// The transaction carries no SponsorSignature, which is what distinguishes the
// pre-funded flow from the co-signed flow.
if (submitResponse.result.tx_json.SponsorSignature !== undefined) {
console.error('Error: A pre-funded sponsorship should not need a SponsorSignature')
await client.disconnect()
process.exit(1)
}
console.log('Transaction sponsored successfully!')
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${submitResponse.result.hash}`)In this example, the sponsee submits a DepositPreauth transaction without a signature from the sponsor. The transaction draws its transaction fee and the new ledger entry's reserve from the pool. Many other transaction types can also be sponsored; see SponsorFlags field to learn more.
Inspect the affected nodes to verify the Sponsor field is on the new DepositPreauth entry, and compare the Sponsorship entry's fields before and after to see what the pool spent.
// Extract sponsorship information from the transaction result ----------------------
console.log(`\n=== Sponsorship Pool information ===`)
const preauthNode = submitResponse.result.meta.AffectedNodes.find(
node => node.CreatedNode?.LedgerEntryType === 'DepositPreauth'
)
console.log(`DepositPreauth ID: ${preauthNode.CreatedNode.LedgerIndex}`)
console.log(`DepositPreauth reserve sponsored by: ${preauthNode.CreatedNode.NewFields.Sponsor}`)
// The Sponsorship entry shows the fee drops and owner reserves the pool spent.
const sponsorshipPool = submitResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'Sponsorship'
)
const fields = sponsorshipPool.ModifiedNode.FinalFields
const previous = sponsorshipPool.ModifiedNode.PreviousFields
const feePaid = BigInt(previous.FeeAmount) - BigInt(fields.FeeAmount)
console.log(`\nFee spent from the pool: ${feePaid} drops`)
console.log(`Fee remaining in the pool: ${fields.FeeAmount} drops`)
console.log(`Owner reserves spent: ${previous.RemainingOwnerCount - fields.RemainingOwnerCount}`)
console.log(`Owner reserves remaining: ${fields.RemainingOwnerCount}`)
await client.disconnect()