Operations

Funding and Recovery

Last updated August 26, 2026

MPL-Distro separates token funding in the distribution vault from optional SOL funding for claim-receipt rent.

Summary

The authority deposits SPL tokens into the distribution's associated token account and may fund the distribution PDA with SOL when receipt subsidies are enabled.

  • Deposit enough token base units to cover every Merkle allocation.
  • Budget one claim-receipt rent payment per expected successful claim.
  • Monitor both the recorded totalAmount and the actual vault token balance.
  • Withdraw unclaimed tokens and unused subsidy SOL only when the distribution is inactive.

Quick Start

MPL-Distro funding and recovery follows four operational steps.

  1. Sum all allocation amounts and deposit that many token base units.
  2. When receipt subsidies are enabled, transfer the expected receipt-rent budget to the distribution PDA.
  3. Monitor the actual vault balance, distribution SOL, and claim totals.
  4. After the window ends, withdraw unclaimed tokens and unused subsidy SOL.

Deposit Distribution Tokens

The deposit instruction transfers tokens from the depositor's account to the distribution PDA's canonical associated token account. The current distribution authority must sign every deposit, even when a different wallet supplies the tokens.

fundDistribution.ts
1import {
2 deposit,
3 mplDistro,
4} from '@metaplex-foundation/mpl-distro'
5import { publicKey } from '@metaplex-foundation/umi'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi(
9 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
10).use(mplDistro())
11
12// The Umi identity must be the current distribution authority.
13
14const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
15const mint = publicKey(process.env.TOKEN_MINT!)
16const totalAmount = 350_000n
17
18await deposit(umi, {
19 distribution,
20 mint,
21 amount: totalAmount,
22}).sendAndConfirm(umi)
23
24// The distribution ATA contains 350000 base units.

The SDK defaults depositor, payer, and authority to the Umi payer and derives both associated token accounts. Supply a separate depositor signer when another wallet owns the source tokens, and still pass the current distribution authority.

depositFromSeparateWallet.ts
1import { deposit, mplDistro } from '@metaplex-foundation/mpl-distro'
2import { publicKey } from '@metaplex-foundation/umi'
3import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
4
5const umi = createUmi(
6 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
7).use(mplDistro())
8
9const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
10const mint = publicKey(process.env.TOKEN_MINT!)
11const amount = 350_000n
12const treasurySigner = umi.identity
13const feePayer = umi.payer
14const distributionAuthority = umi.identity
15
16await deposit(umi, {
17 distribution,
18 mint,
19 depositor: treasurySigner,
20 payer: feePayer,
21 authority: distributionAuthority,
22 amount,
23}).sendAndConfirm(umi)
24
25// Tokens move from the treasury ATA to the distribution vault.

The program increments totalAmount after each deposit. It does not compare that value with the sum of allocations committed by the Merkle root.

Calculate the Token Deposit

The required token deposit is the sum of all allocation amounts expressed in the mint's base units.

calculateDeposit.ts
1import { publicKey } from '@metaplex-foundation/umi'
2
3const allocations = [
4 { address: publicKey(process.env.RECIPIENT_1!), amount: 100_000n },
5 { address: publicKey(process.env.RECIPIENT_2!), amount: 250_000n },
6]
7
8const totalAmount = allocations.reduce(
9 (total, allocation) => total + BigInt(allocation.amount),
10 0n
11)
12console.log(totalAmount)
13
14// 350000

Deposit a deliberate buffer only when the authority accepts that it must recover the excess later. A valid proof fails with InsufficientFunds when the recorded balance is below its allocation, and the SPL transfer can also fail if the actual vault balance is lower.

Fund Claim Receipt Subsidies

Receipt subsidies let the distribution PDA reimburse the transaction payer for the rent used to create each claim receipt.

Enable subsidizeReceipts during createDistribution, calculate rent through the RPC, and transfer SOL directly to the distribution PDA:

fundReceiptSubsidy.ts
1import { getClaimReceiptSize, mplDistro } from '@metaplex-foundation/mpl-distro'
2import { transferSol } from '@metaplex-foundation/mpl-toolbox'
3import { multiplyAmount, publicKey } from '@metaplex-foundation/umi'
4import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
5
6const umi = createUmi(
7 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
8).use(mplDistro())
9
10const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
11const expectedClaimCount = 2
12
13const receiptRent = await umi.rpc.getRent(getClaimReceiptSize())
14const budget = multiplyAmount(receiptRent, expectedClaimCount)
15
16await transferSol(umi, {
17 destination: distribution,
18 amount: budget,
19}).sendAndConfirm(umi)
20
21// The distribution PDA holds extra SOL for claim-receipt rent.

Subsidy Budget Boundary

The distribution must retain its own rent-exempt minimum. A claim fails with InsufficientFundsToSubsidizeReceipts when the remaining SOL cannot cover both the distribution rent and one receipt reimbursement.

MPL-Distro Funding Quick Reference

Claim costs are split among a fixed protocol fee, Solana transaction costs, and account rent.

CostDefault payerReceipt subsidy covers it
Protocol fee (0.002 SOL)Claim transaction payerNo
Transaction feeClaim transaction payerNo
Claim receipt rentClaim transaction payerYes, when enabled and funded
Recipient ATA rentClaim transaction payerNo

Recover Unclaimed Tokens

The distribution authority recovers unclaimed or excess tokens with withdraw before the start time or after the end time.

recoverFunds.ts
1import {
2 DISTRIBUTION_SIZE,
3 fetchDistribution,
4 mplDistro,
5 withdraw,
6 withdrawSubsidy,
7} from '@metaplex-foundation/mpl-distro'
8import { publicKey } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi(
12 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
13).use(mplDistro())
14
15// The Umi identity must be the current distribution authority.
16
17const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
18const mint = publicKey(process.env.TOKEN_MINT!)
19
20// Token and subsidy withdrawals succeed only outside the active window.
21await withdraw(umi, {
22 distribution,
23 mint,
24 amount: 50_000n,
25}).sendAndConfirm(umi)
26
27const distributionAccount = await fetchDistribution(umi, distribution)
28if (distributionAccount.subsidizeReceipts) {
29 const balance = await umi.rpc.getBalance(distribution)
30 const rent = await umi.rpc.getRent(DISTRIBUTION_SIZE)
31 const unusedSubsidy = balance.basisPoints - rent.basisPoints
32
33 if (unusedSubsidy > 0n) {
34 await withdrawSubsidy(umi, {
35 distribution,
36 recipient: umi.identity.publicKey,
37 amount: unusedSubsidy,
38 }).sendAndConfirm(umi)
39 }
40}
41
42// 50000 token base units and any unused receipt subsidy are returned.

The active interval is inclusive. A withdrawal is rejected when startTime <= clusterTime <= endTime.

Recover Unused Subsidy SOL

The authority recovers unused receipt subsidy with withdrawSubsidy only when subsidies are enabled and the distribution is inactive.

withdrawSubsidy transfers a requested lamport amount while preserving the distribution account's rent-exempt minimum. Determine the safe amount from the current account balance instead of assuming every expected claim occurred.

Monitor Distribution Balances

Production systems should compare program bookkeeping with the actual SPL and SOL account balances.

ValueSourceMeaning
distribution.totalAmountDistribution accountDeposits minus withdrawals recorded by the program; claims do not decrement it
Vault token amountDistribution associated token accountTokens actually available for transfer
Distribution lamportsDistribution PDA accountRent reserve plus optional unused receipt subsidy
claimCountDistribution accountNumber of recorded successful claims
claimAmountDistribution accountSum of recorded claimed token base units

The token withdrawal bookkeeping uses saturating subtraction, so integrations should not assume totalAmount can never diverge from the SPL vault balance.

Notes

Funding operations require authority controls and explicit balance monitoring.

  • Only the current distribution authority can authorize a deposit.
  • Deposits are allowed before, during, and after the claim window.
  • Token and subsidy withdrawals are blocked throughout the active window.
  • Anyone can transfer SOL directly to the distribution PDA, but only the authority can withdraw subsidy through the program.
  • Receipt rent remains allocated because claim receipts cannot currently be closed.

FAQ

Can the authority withdraw tokens while claims are active?

No. Token withdrawals are rejected from the start timestamp through the end timestamp, inclusive.

What costs does subsidizeReceipts reimburse?

It reimburses claim-receipt rent only, not the protocol fee, transaction fee, or recipient token-account rent.

Can more tokens be deposited after claims start?

Yes. Deposits are not time-gated, so the authority can replenish an underfunded vault.

Can a treasury wallet deposit without the distribution authority?

No. The current authority must sign deposit, even when a separate depositor supplies the tokens.