Skip to content
Last updated

Manage a Sponsorship Pool

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...

Goals

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.

Prerequisites

To complete this tutorial, you should:

Source Code

You can find the complete source code for this tutorial's example in the code samples section of this website's repository.

Steps

1. Install dependencies

From the code sample folder, use npm to install dependencies:

npm install

2. Set up the client

Import 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. SponsorshipSetFlags holds 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()

3. Create the wallets

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}`)

4. Create the pool

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}`)
Warning

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.

5. Spend part of the pool

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.

6. Top up the pool

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.

Note

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}`)

7. Delete the pool

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}`)
Note

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.

8. Verify the sponsor reclaimed XRP

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()

See Also