Distribution Types
Wallet Distribution
Last updated August 27, 2026
Wallet distributions assign fixed token amounts to public keys and verify each allocation through distribute.
Summary
A wallet distribution uses a wallet or other public key as the Merkle leaf identity and always transfers the allocation to that identity's associated token account.
- Use
prepareDistributionto generate compatible roots and proofs. - Set a nonce when duplicate recipient-and-amount allocations must remain distinct.
- Select a distributor mode that matches the application's signing model.
- Preserve every proof because proofs cannot be reconstructed from the on-chain root alone.
Wallet Allocation Shape
Each wallet allocation contains an address, an amount in token base units, and an optional unsigned 64-bit nonce.
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 contributorA = publicKey(process.env.RECIPIENT_1!)
10const contributorB = publicKey(process.env.RECIPIENT_2!)
11
12const allocations = [
13 { address: contributorA, amount: 1_000_000n, nonce: 0n },
14 { address: contributorB, amount: 2_500_000n, nonce: 0n },
15]
16
17const { root, proofs, treeHeight } = prepareDistribution(allocations)
18console.log(root, proofs.length, treeHeight)
19
20// Merkle root, two proofs, and treeHeight for the wallet allocations
Amounts must be greater than zero. A nonce defaults to zero and should change only when two leaves would otherwise contain the same address and amount.
MPL-Distro Merkle Format
MPL-Distro hashes allocation data with Keccak-256 and sorted internal node pairs.
| Element | Encoding |
|---|---|
| Leaf data | `recipient_pubkey[32] |
| Leaf hash | `keccak256("claim" |
| Internal node | `keccak256(0x01 |
| Odd node | Paired with itself |
| Proof item | One 32-byte sibling hash |
| Maximum configured height | 64 |
Use the SDK helper instead of implementing this format independently. A proof generated with SHA-256, big-endian integers, unsorted pairs, or a different domain prefix fails with InvalidClaimProof.
Tree Height Is a Proof Bound
The on-chain treeHeight limits proof length; it does not independently verify totalClaimants. Pass the value returned by prepareDistribution.
Wallet Claim Submission Modes
The allowedDistributor setting determines which signer may submit distribute.
Permissionless Wallet Claims
Permissionless claims let any funded payer submit a valid proof while the program sends tokens only to the committed recipient.
Use this mode for a recipient-paid claim page, or a relayer that pays SOL when someone actually claims. Do not use Distro to push every allocation from a backend; that is usually more expensive than direct SPL transfers.
Recipient-Signed Wallet Claims
Recipient claims require the committed recipient to sign the transaction.
Use this mode when the beneficiary must explicitly accept the allocation or when proof access alone must not authorize submission.
Permissioned Wallet Claims
Permissioned claims require the configured permissionedDistributor signer.
Use this mode when one backend controls release timing within the broader on-chain claim window. The authority can change the permissioned distributor later.
Set the Permissioned Distributor at Creation
createDistribution defaults permissionedDistributor to the System Program public key. Pass the real distributor address when allowedDistributor is Permissioned, or every claim fails with InvalidDistributor.
Submit a Wallet Claim
The distribute instruction verifies the proof, creates the associated token account when necessary, transfers tokens, and records the receipt atomically.
1import {
2 distribute,
3 mplDistro,
4 prepareDistribution,
5} from '@metaplex-foundation/mpl-distro'
6import { publicKey } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi(
10 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
11).use(mplDistro())
12
13// The payer may be the recipient or a third-party distributor, depending on
14// the distribution's allowedDistributor setting.
15
16const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
17const mint = publicKey(process.env.TOKEN_MINT!)
18const recipients = [
19 { address: publicKey(process.env.RECIPIENT_1!), amount: 100_000n },
20 { address: publicKey(process.env.RECIPIENT_2!), amount: 250_000n },
21]
22const recipientIndex = 0
23const { proofs } = prepareDistribution(recipients)
24
25await distribute(umi, {
26 distribution,
27 mint,
28 recipient: recipients[recipientIndex].address,
29 amount: recipients[recipientIndex].amount,
30 proof: proofs[recipientIndex],
31 nonce: 0,
32}).sendAndConfirm(umi)
33
34// The recipient ATA receives 100000 base units and a claim receipt is created.
The payer pays transaction fees, the 0.002 SOL protocol fee, and account rent. See Funding and Recovery for optional claim-receipt rent subsidies.
Wallet Claim Receipt
The claim receipt prevents one exact allocation from being processed more than once.
| Field | Value |
|---|---|
| PDA seeds | ["claim_receipt", distribution, recipient, amount_le, nonce_le] |
| Stored distribution | Distribution PDA |
| Stored recipient | Wallet or public key from the leaf |
| Stored amount | Claimed token base units |
| Stored nonce | Leaf nonce |
| Account size | 88 bytes |
Claim receipts are permanent in the current program and do not have a close instruction.
Claim to a Core Asset Signer
distributeToAssetAndClaim claims a Wallet allocation into an MPL Core asset-signer PDA, then uses Core Execute to move the tokens to the current owner.
Build the Merkle leaves from each asset's signer PDA, not from owner wallets. The helper then transfers the claimed tokens out of that PDA's associated token account.
1import { findAssetSignerPda } from '@metaplex-foundation/mpl-core'
2import {
3 distributeToAssetAndClaim,
4 mplDistro,
5 prepareDistribution,
6} from '@metaplex-foundation/mpl-distro'
7import { publicKey } from '@metaplex-foundation/umi'
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
9
10const umi = createUmi(
11 process.env.RPC_URL ?? 'https://api.devnet.solana.com'
12).use(mplDistro())
13
14const distribution = publicKey(process.env.DISTRIBUTION_ADDRESS!)
15const mint = publicKey(process.env.TOKEN_MINT!)
16const asset = publicKey(process.env.CORE_ASSET!)
17const currentOwner = publicKey(process.env.CORE_ASSET_OWNER!)
18
19const [assetSigner] = findAssetSignerPda(umi, { asset })
20const allocations = [{ address: assetSigner, amount: 100_000n }]
21const { proofs } = prepareDistribution(allocations)
22
23await distributeToAssetAndClaim(umi, {
24 distribution,
25 mint,
26 asset,
27 recipient: currentOwner,
28 amount: allocations[0].amount,
29 proof: proofs[0],
30 nonce: 0,
31}).sendAndConfirm(umi)
32
33// Tokens are claimed to the asset-signer PDA, then transferred to the current owner.
This helper is a Wallet distribution flow. It is not a LegacyNft claim and does not validate Core collection membership on-chain.
Wallet Distribution Security Checklist
A production wallet distribution should validate allocation integrity before publishing the root.
- Confirm the sum of allocations does not exceed the planned deposit.
- Reject zero, negative, or out-of-range amounts before calling the SDK.
- Assign deterministic nonces and store them with proofs.
- Test random proofs and every edge allocation against the final root.
- Keep authority and permissioned-distributor keys outside browser applications.
- Confirm cluster timestamps and leave operational time around the start and end boundaries.
Notes
Wallet distributions can use any public key as a leaf identity, but the default destination is its SPL token associated token account.
- Core asset claims use
distributeToAssetAndClaimand require the asset-signer PDA in the Merkle leaf. totalClaimantsis not an on-chain claim cap.- A valid proof can still fail when the vault lacks tokens.
- Claims are accepted at both exact boundary timestamps:
startTime <= now <= endTime.
FAQ
Can a backend submit a claim without the recipient signing?
Yes. A Permissionless distribution lets a relayer pay SOL and submit the proof. Tokens still go to the leaf address. Use this so recipients without SOL can claim, not to replace a bulk SPL transfer.
What prevents the same wallet allocation from being claimed twice?
A deterministic claim receipt PDA records each unique distribution, recipient, amount, and nonce tuple.
Does totalClaimants limit successful claims?
No. totalClaimants is metadata; Merkle inclusion and available vault funds determine whether an allocation can claim.
What address belongs in a Core asset allocation leaf?
Use the Core asset-signer PDA. distributeToAssetAndClaim then moves the tokens to the current owner.
