This tutorial shows you how to manage a pre-funded sponsorship pool as a sponsor. The example creates a pool, checks what it spends, tops it up, and deletes it to reclaim any unspent XRP.
Requires the Sponsor amendment. Loading...
By the end of this tutorial, you should be able to:
- Create a sponsorship pool and track what it spends.
- Update a pool to adjust its fee allocation and reserve allowance.
- Delete a pool to return the remaining funds to the sponsor.
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.SponsorshipSetFlagsholds the flags that update and delete the pool.
import {
Client,
SponsorFlags,
SponsorshipSetFlags,
validate
} from 'xrpl'
// Connect to the network ----------------------
const client = new Client('wss://s.devnet.rippletest.net:51233')
await client.connect()Create and fund the sponsor and the sponsee accounts.
// Create the sponsor and sponsee wallets ----------------------
console.log(`\n=== Creating the sponsor and sponsee wallets... ===`)
const { wallet: sponsor } = await client.fundWallet()
const { wallet: sponsee } = await client.fundWallet()
console.log(`Sponsor address: ${sponsor.address}`)
console.log(`Sponsee address: ${sponsee.address}`)Submit a SponsorshipSet transaction to create the pool.
// Prepare SponsorshipSet transaction ----------------------
// FeeAmountDelta adds 1 XRP to the fee pool, MaxFee caps the pool's contribution
// to any single transaction, and RemainingOwnerCountDelta allows five sponsored ledger entries.
console.log(`\n=== Preparing SponsorshipSet transaction... ===`)
const createPoolTx = {
TransactionType: 'SponsorshipSet',
Account: sponsor.address,
Sponsee: sponsee.address,
FeeAmountDelta: '1000000',
MaxFee: '1000',
RemainingOwnerCountDelta: 5
}
validate(createPoolTx)
console.log(JSON.stringify(createPoolTx, null, 2))Only the sponsor can create a pool, so it signs and submits the transaction alone. The metadata returns the new Sponsorship entry's ID.
// Submit the SponsorshipSet transaction ----------------------
console.log(`\n=== Submitting SponsorshipSet transaction... ===`)
const createResponse = await client.submitAndWait(createPoolTx, {
wallet: sponsor,
autofill: true
})
if (createResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = createResponse.result.meta.TransactionResult
console.error('Error: Unable to create the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
const sponsorshipNode = createResponse.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/${createResponse.result.hash}`)The Sponsorship entry appears in both accounts' owner directories, which makes it a deletion blocker for the sponsor and the sponsee alike. Neither account can be deleted until the pool is.
Spend part of the pool by submitting a DepositPreauth transaction that draws the fee and one owner reserve from the pool.
// Spend part of the pool ----------------------
// The sponsee creates a DepositPreauth entry, drawing the fee and one owner reserve
// from the pool.
console.log(`\n=== Submitting sponsored DepositPreauth transaction... ===`)
const depositPreauthTx = {
TransactionType: 'DepositPreauth',
Account: sponsee.address,
Authorize: sponsor.address,
Sponsor: sponsor.address,
SponsorFlags: SponsorFlags.spfSponsorFee | SponsorFlags.spfSponsorReserve
}
validate(depositPreauthTx)
const depositPreauthResponse = await client.submitAndWait(depositPreauthTx, {
wallet: sponsee,
autofill: true
})
if (depositPreauthResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = depositPreauthResponse.result.meta.TransactionResult
console.error('Error: Unable to create the preauthorization:', resultCode)
await client.disconnect()
process.exit(1)
}
let fields = depositPreauthResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'Sponsorship'
).ModifiedNode.FinalFields
console.log('Sponsorship pool:')
console.log(` Fee amount: ${fields.FeeAmount} drops`)
console.log(` Owner reserves count: ${fields.RemainingOwnerCount}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${depositPreauthResponse.result.hash}`)The Sponsorship entry's FeeAmount and RemainingOwnerCount fields drop accordingly.
Send another SponsorshipSet transaction for the same sponsee to change the pool's allowances.
FeeAmountDelta and RemainingOwnerCountDelta apply changes to the current allowances. Positive deltas add budget and negative deltas reduce it. MaxFee remains an absolute per-transaction cap.
The pool doesn't automatically regain RemainingOwnerCount when a sponsored ledger entry is deleted or reassigned. Those actions free the sponsor's reserve and lower its SponsoringOwnerCount, but the pool's spent reserve allowance remains spent. Top the pool up with RemainingOwnerCountDelta to sponsor more ledger entries.
// Prepare SponsorshipSet transaction to top up the pool ----------------------
// A second SponsorshipSet on the same sponsee applies deltas to the current allowances.
// Here the sponsor adds another 1 XRP of fee budget and five more reserve units.
console.log(`\n=== Preparing SponsorshipSet transaction to top up sponsorship pool... ===`)
const updatePoolTx = {
TransactionType: 'SponsorshipSet',
Account: sponsor.address,
Sponsee: sponsee.address,
FeeAmountDelta: '1000000',
MaxFee: '1000',
RemainingOwnerCountDelta: 5
}
validate(updatePoolTx)
console.log(JSON.stringify(updatePoolTx, null, 2))The sponsor submits the update, and the metadata shows the entry's raised FeeAmount and RemainingOwnerCount.
// Submit the SponsorshipSet transaction to top up the pool ----------------------
console.log(`\n=== Submitting SponsorshipSet transaction... ===`)
const updateResponse = await client.submitAndWait(updatePoolTx, {
wallet: sponsor,
autofill: true
})
if (updateResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = updateResponse.result.meta.TransactionResult
console.error('Error: Unable to update the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
fields = updateResponse.result.meta.AffectedNodes.find(
node => node.ModifiedNode?.LedgerEntryType === 'Sponsorship'
).ModifiedNode.FinalFields
console.log('Sponsorship pool topped up successfully:')
console.log(` Fee amount: ${fields.FeeAmount} drops`)
console.log(` Owner reserves count: ${fields.RemainingOwnerCount}`)
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${updateResponse.result.hash}`)Submit a SponsorshipSet transaction with the tfDeleteObject flag enabled to delete the Sponsorship entry and return the unspent FeeAmount to the sponsor. The example looks up the sponsor's balance with the account_info method first, so it can compare the balance after the deletion.
// Prepare SponsorshipSet transaction to delete the sponsorship ----------------------
// tfDeleteObject returns the unspent FeeAmount to the sponsor. Ledger entries the
// pool already paid reserves for stay sponsored until they're transferred or deleted.
console.log(`\n=== Preparing SponsorshipSet transaction to delete the sponsorship... ===`)
const balanceBeforeResponse = await client.request({
command: 'account_info',
account: sponsor.address,
ledger_index: 'validated'
})
const sponsorBalanceBefore = BigInt(
balanceBeforeResponse.result.account_data.Balance
)
const deletePoolTx = {
TransactionType: 'SponsorshipSet',
Account: sponsor.address,
Sponsee: sponsee.address,
Flags: SponsorshipSetFlags.tfDeleteObject
}
validate(deletePoolTx)
console.log(JSON.stringify(deletePoolTx, null, 2))The sponsor submits the deletion, and the metadata confirms the Sponsorship entry is deleted.
// Submit the SponsorshipSet transaction to delete the sponsorship ----------------------
console.log(`\n=== Submitting SponsorshipSet transaction... ===`)
const deleteResponse = await client.submitAndWait(deletePoolTx, {
wallet: sponsor,
autofill: true
})
if (deleteResponse.result.meta.TransactionResult !== 'tesSUCCESS') {
const resultCode = deleteResponse.result.meta.TransactionResult
console.error('Error: Unable to delete the sponsorship:', resultCode)
await client.disconnect()
process.exit(1)
}
const deletedNode = deleteResponse.result.meta.AffectedNodes.find(
node => node.DeletedNode?.LedgerEntryType === 'Sponsorship'
)
console.log('Sponsorship deleted successfully!')
console.log(`Transaction URL: https://devnet.xrpl.org/transactions/${deleteResponse.result.hash}`)Deleting a pool does not revoke sponsorship on ledger entries that already consumed its reserve allowance. Those entries stay sponsored until the sponsorship ends through a SponsorshipTransfer transaction with the tfSponsorshipEnd flag, or until the entries are deleted.
Send another account_info method request to compare the sponsor's balance before and after the deletion. The sponsor gets the pool's unspent FeeAmount back, minus the transaction fee paid to submit the delete transaction.
Deleting the Sponsorship entry also releases the sponsor's owner reserve requirement, but that does not appear as an XRP balance increase. The balance check only shows the returned FeeAmount minus the delete transaction fee.
// Show the reclaimed XRP ----------------------
console.log(`\n=== Reclaimed Funds ===`)
const balanceAfterResponse = await client.request({
command: 'account_info',
account: sponsor.address,
ledger_index: 'validated'
})
const sponsorBalanceAfter = BigInt(
balanceAfterResponse.result.account_data.Balance
)
const deleteFee = BigInt(deleteResponse.result.tx_json.Fee)
console.log(`Unspent fee amount returned from pool: ${deletedNode.DeletedNode.FinalFields.FeeAmount} drops`)
console.log(`Sponsor balance "before" deletion: ${sponsorBalanceBefore} drops`)
console.log(`Sponsor balance "after" deletion: ${sponsorBalanceAfter} drops`)
console.log(`Delete transaction (fee paid): ${deleteFee} drops`)
await client.disconnect()