Introduction
Production Delivery
Last updated August 27, 2026
MPL-Distro stores only the Merkle root on-chain. A production airdrop persists each allocation's proof off-chain and gives recipients a way to submit it.
Summary
Production delivery is the off-chain work around an MPL-Distro distribution: store claim records, serve them to the right claimant, and recover leftovers when the window ends.
- Create and fund the distribution with the Getting Started flow or the Metaplex CLI.
- Persist
address,amount,nonce, andprooffor every allocation before claims open. - Choose Permissionless, Recipient, or Permissioned submission to match who should sign.
- Recover unclaimed tokens with Funding and Recovery after the window ends.
No Hosted Claim Interface
MPL-Distro does not ship a claim website or email, SMS, or Discord identity. The Metaplex CLI can create, fund, inspect, and recover a distribution; it does not generate Merkle proofs or submit claims. Notify users through any channel you already use; the on-chain leaf is still a wallet or legacy NFT mint.
Jump to: Prerequisites · Submission Mode · Persist Records · Deliver Proofs · Recover Tokens
Quick Start
A production MPL-Distro airdrop has five delivery steps around the on-chain program.
- Build a complete allocation list and generate the root with
prepareDistribution. - Persist one claim record per allocation, then create and fund the distribution.
- Serve each record from a claim page or lookup API keyed by wallet or NFT mint.
- Submit
distributeordistributeToLegacyNftwith the stored amount, nonce, and proof. - After
endTime, withdraw unclaimed tokens and unused receipt-rent subsidy.
Prerequisites
Production delivery starts from an existing SPL token mint and a finished allocation list.
- A Getting Started distribution (or the same create and deposit steps in your backend)
- Durable storage for claim records (database, object store, or downloadable file)
- A claim transaction payer funded for rent, network fees, and the 0.002 SOL protocol fee
- A distribution type: wallet or legacy NFT
Allocation amounts are token base units. For a 6-decimal mint, 1.0 token is 1_000_000.
Choose a Claim Submission Mode
allowedDistributor decides who may submit a valid proof; it does not change where the tokens go.
| Mode | Who signs the claim | Typical production shape |
|---|---|---|
Permissionless | Any funded payer | A claim page where the user or a relayer pays; tokens still go to the leaf |
Recipient | The leaf wallet or current NFT owner | A claim page where the beneficiary must approve the transaction |
Permissioned | The configured permissionedDistributor | One backend is the only signer allowed to submit proofs |
Tokens always arrive at the leaf's canonical associated token account (or the current NFT owner's ATA for LegacyNft). Permissionless submission cannot redirect funds to the payer.
Keep the distribution authority and any permissioned-distributor key outside browser applications.
Persist Allocation Records
Each claim needs the same address, amount, nonce, and proof that prepareDistribution used for that leaf. The on-chain account cannot reconstruct those values from the root.
Start from a complete list, then store the proof at the same index:
[
{
"address": "8SoWVrwJ6vPa3rcdNBkhznR54yJ6iQqPSmgcXVGnwtEu",
"amount": "10000000",
"nonce": "0"
},
{
"address": "GjwcWFQYzemBtpUoN5fMAP2FZviTtMRWCmrppGuTthJS",
"amount": "5000000",
"nonce": "0"
}
]
1import { mplDistro, prepareDistribution } 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 allocations = [
10 { address: publicKey(process.env.RECIPIENT_1!), amount: 100_000n, nonce: 0n },
11 { address: publicKey(process.env.RECIPIENT_2!), amount: 250_000n, nonce: 0n },
12]
13
14const { root, proofs, treeHeight } = prepareDistribution(allocations)
15
16const claimRecords = allocations.map((allocation, index) => ({
17 address: allocation.address,
18 amount: allocation.amount.toString(),
19 nonce: (allocation.nonce ?? 0n).toString(),
20 proof: proofs[index],
21}))
22
23// Persist claimRecords with the distribution PDA after createDistribution.
24console.log(root, treeHeight, claimRecords.length)
25
26// One durable record per allocation: address, amount, nonce, and proof.
After createDistribution, store the distribution PDA on every record. The claim transaction needs that address plus mint, amount, nonce, and proof.
| Field | Required for | Notes |
|---|---|---|
address | Leaf identity | Wallet public key or legacy NFT mint |
amount | Leaf data | Token base units as a string or bigint |
nonce | Leaf data | Defaults to 0; required when the same address and amount appear twice |
proof | distribute | One 32-byte sibling hash per tree level, in SDK order |
distribution | distribute | PDA from findDistributionPda after create |
Store Proofs Before Opening Claims
The authority cannot change the Merkle root, tree height, start time, or claimant count while startTime <= now <= endTime. Back up the full allocation file before the window starts.
Deliver Merkle Proofs
The application looks up one stored record and passes it to distribute or distributeToLegacyNft. MPL-Distro does not index recipients.
Common delivery shapes:
- Claim page. The user connects a wallet, pays network fees, and submits their stored proof.
- Lookup API. A service maps
address→{ amount, nonce, proof, distribution }for your frontend or relayer. - Sponsored claim. The recipient (or an eligibility check) still triggers the claim. A relayer pays SOL so the user does not need a funded wallet. Tokens still go to the leaf ATA.
Sponsored claims are not a substitute for sending every allocation in one backend loop. Each Distro claim still pays the 0.002 SOL protocol fee. If every recipient will receive tokens immediately with no claim step, use direct SPL token transfers.
Use Distro when some allocations may go unclaimed, when you need a public Merkle commitment and time window, or when a relayer should pay only for people who actually claim.
For LegacyNft, key the lookup by NFT mint. Resolve the current owner at claim time; do not freeze a snapshot owner into the leaf unless you intended a wallet distribution instead.
Do not rebuild proofs from the on-chain root. A proof generated with a different hash, byte order, or leaf set fails with InvalidClaimProof.
Open the Claim Window
Claims succeed only when the cluster time is inside the inclusive startTime–endTime window and the vault holds enough tokens.
Create, deposit, and submit the first test claim with the Getting Started flow before opening the list to every recipient. Confirm:
- A sample proof from the persisted file matches
distribute. - The protocol fee payer has SOL for the 0.002 SOL fee plus receipt rent, or receipt subsidies are funded.
- Authority keys are not exposed to the claim frontend.
Monitor Claims
A successful claim creates a permanent claim-receipt PDA. Fetch that account, or compare claimCount / claimAmount on the distribution, to know which allocations are done.
Treat AlreadyClaimed as success for that exact (distribution, recipient, amount, nonce) tuple. Ownership transfer of a LegacyNft mint does not reset the receipt.
Recover Unclaimed Tokens
The distribution authority withdraws leftover tokens and unused subsidy SOL only when the distribution is inactive: before startTime or after endTime.
See Funding and Recovery for withdraw and withdrawSubsidy. Leave operational margin around the end timestamp so the last claims are not racing a recovery transaction.
Production Delivery Checklist
Validate the off-chain file against the on-chain root before users rely on it.
- Sum of
amountvalues is covered by the vault deposit. - Every persisted proof is the
prepareDistributionoutput for that same list, in the same order. Recipientmode is used when a leaked proof must not be enough to submit.- Claim frontends never hold the distribution authority.
- Unclaimed tokens have an owner who can call
withdrawafterendTime.
Notes
MPL-Distro does not replace your allocation database, notification channel, or claim UI.
totalClaimantsis metadata and does not cap successful proofs.- Claim receipts are not closed, so receipt rent stays allocated.
- Large lists should be built in a controlled Node.js process;
prepareDistributionswitches implementation at 1,000 leaves.
FAQ
Does MPL-Distro host a claim website?
No. The program stores only the Merkle root. The application must persist proofs and provide a claim page or API.
Can an email or Discord handle be the Merkle leaf?
No. Leaves are wallet public keys or legacy NFT mints. Off-chain channels can notify users, but they are not on-chain identities.
Is it safe to make Merkle proofs public?
In Permissionless mode, anyone with a valid proof can submit the claim; tokens still go to the leaf address. Use Recipient mode when proof access alone must not authorize submission.
Should a backend submit every Merkle proof itself?
No. Submitting every proof from a backend is usually more expensive than SPL token transfers because each Distro claim pays the protocol fee. Use a relayer so users without SOL can still claim, or use Distro when some allocations may go unclaimed and you need the Merkle window.
When can unclaimed tokens be recovered?
The authority can withdraw tokens before the start timestamp or after the end timestamp. Withdrawals are rejected while startTime <= clusterTime <= endTime.
