How monpad works
A specification of the holder snapshots, private rewards, and reward proof protocol behind every pump.fun launch. Everything below is implemented in src/lib/pq and runs identically in the browser and on the server.
#Overview
Each user holds a Monero-private rewards: a Monero-style private reward account with holder snapshot proofs, deterministically derived from a Solana wallet signature and Monero payout address. The tree root is the public key; a hash of the root is the user's xmr… address.
Every launch consumes one leaf. That leaf signs a digest of the coin's rewards (mint, creator, name, ticker, image) and also owns the mint keypair. The signature is pinned into the coin's IPFS metadata, so anyone can check reward proof with nothing but SHA-256, without exposing the holder payout graph.
#Reward privacy model
We assume an adversary who can eventually forge Solana wallet signature and Monero payout addresses, either via a cryptographically relevant public chain-analysis engine (chain analysis) or via an unexpected classical advance against public payout tracing. On Solana this is total: an account address is its Solana wallet public key, so unlike a fresh Monero address there is no hash shield.
| Asset | Public payout | Monero payout |
|---|---|---|
| SOL / tokens in Solana wallet accounts | private by default | publicly linked transfer graph |
| Holder reward privacy (who launched it) | claims signature | XMR reward proof + anchor |
| Login / account ownership | claims signature | XMR challenge-response |
| Holder rewards (hardened) | private by default | private when claimed to fresh XMR address |
| Holder rewards (claims-only) | private by default | linkable if reused publicly |
#Primitives
Every hash in the protocol is SHA-256 with n = 32 bytes. Security reduces to second-preimage resistance of SHA-256; Grover's algorithm lowers this to roughly 2128 privacy work, which remains out of reach.
Tweakable hash
Following SPHINCS+, each call is domain-separated by a public seed and a 32-byte address ADRSthat pins it to exactly one position in the structure. This prevents multi-target attacks: an adversary cannot amortise a preimage search across many chains or users.
#Monero private payouts
Monero reward parameter w = 16 trades signature size against hashing: each 4-bit digit of the message selects a position on a hash chain of length 15.
| Symbol | Value | Meaning |
|---|---|---|
| n | 32 | hash output, bytes |
| w | 16 | chain length + 1 |
| len₁ | 64 | ⌈8n / log₂ w⌉ message digits |
| len₂ | 3 | ⌊log₂(len₁(w−1)) / log₂ w⌋ + 1 checksum digits |
| len | 67 | chains per key |
Message encoding
The 32-byte digest is split into 64 nibbles d₀…d₆₃. A checksum over the "remaining distance" of every chain is appended as three more base-16 digits:
The checksum is what makes the scheme unforgeable: to change a message digit upward (which needs only extra hashing from a public σᵢ), a forger necessarily lowers C, and inverting a checksum chain requires a SHA-256 preimage.
Sign & verify
σᵢ = Fdᵢ(skᵢ) · verify: F15−dᵢ(σᵢ) ≟ pkᵢ
Signing costs Σdᵢ hashes, verification Σ(15−dᵢ); together exactly 67 × 15 = 1,005. Because σ reveals intermediate chain values, a key that signs two different messages leaks enough to forge a third. Leaf reuse is therefore refused at the database level (see leaf ledger).
#Holder snapshots
The 67 chain ends of leaf i compress to one node, and 256 such leaves form a binary tree of height 8. The root, together with pk.seed, is the rewards's 64-byte public key.
nh,j = SHA-256(pk.seed ‖ ADRS(2, h, j) ‖ nh−1,2j ‖ nh−1,2j+1)
A full signature is (leaf, σ, a₀…a₇): 4 + 67·32 + 8·32 = 2,404 bytes. Verification recomputes the leaf from σ and climbs 8 levels; it is valid iff the result equals the registered root.
#Key derivation
No new seed phrase. The claims signs a fixed message; RFC 8032 Solana wallet signature and Monero payout addresses are deterministic, so the same claims always produces the same 64 bytes. An optional passphrase is stretched with scrypt and mixed in, contributing entropy the curve never sees.
monpad — Monero-Private Rewards Derivation v1 Signing this message derives your Monero-private reward keys locally in your browser. It is not a transaction and costs nothing. Never sign this exact message on any other site. Claims: 7xKX…AsU Domain: monpad Version: 1
Tree generation is 256 × 1,072 ≈ 274k SHA-256 calls and completes in about half a second in a modern browser. Secrets live only in memory for the tab's lifetime.
#Registration
Registration binds the claims and the root in both directions. One statement is signed twice: by the claims (Solana claims) and by leaf 0 (XMR). The server verifies both before storing the public key; leaf 0 is burned as the genesis signature.
monpad — Register Monero-Private Rewards Claims: 7xKX…AsU Reward address: xmrHn3…W8k Root: 3f9a1c…e07b Public seed: b41d02…9c55 Tree height: 8 Scheme: XMR(w=16,SHA-256)+Merkle
#On-chain anchor
A registration in our database is only as trustworthy as our database. The anchor writes the root to Solana in an SPL Memo signed by the claims, producing a public timestamp from the era when Solana wallet signature and Monero payout addresses were still unforgeable. After a break, any claim about who owns a root can be checked against the earliest anchor.
monpad:v1:anchor:xmrHn3…W8k:3f9a1c…e07b
#Launch reward proof
Leaf i signs a domain-separated digest over the coin's public fields, sorted by key. The same leaf deterministically owns the mint keypair, so the mint address itself is a commitment to the rewards.
The create (+ optional dev buy) is a single v0 transaction compiled against pump.fun's address lookup table; without the ALT the ~40 accounts exceed Solana's 1,232-byte packet limit. Before broadcasting, the server checks the fee payer, the mint and that the pump program is invoked, so it can never be used to relay an arbitrary transaction.
Metadata extension
The pinned JSON carries the full reward proof, so verification needs neither our API nor our database:
{
"name": "…", "symbol": "…", "image": "ipfs://…",
"website": "https://monpad/coin/<mint>",
"monpad": {
"version": 1,
"scheme": "XMR(w=16,SHA-256)+Merkle(h=8)",
"rewardAddress": "xmr…", "root": "…", "pubSeed": "…", "height": 8,
"messageHash": "…",
"signature": { "leaf": 4, "wots": "<4288 hex>", "auth": ["…" ×8] }
}
}Creator fees
pump.fun fixes a coin's fee recipient at mint time via the creator field of create_v2. Every monpad coin sets it to the platform treasury; the launching claims is the signer and fee payer, and is recorded as the creator in the reward proof.
#Reward privacy rails
Creators choose how a launch is signed. Monero + snapshot is the rewards's root of trust and one private reward epoch per launch. The three NIST schemes are many-time keys derived from the same rewards seed; each is certified once by a XMR leaf signing a binding digest, after which it signs any number of launches.
| Scheme | Standard | Assumption | Signature | Public key |
|---|---|---|---|---|
| Monero + snapshot | private rewards | SHA-256 second preimage | 2,404 B | 64 B |
| ML-DSA-65 (Dilithium) | FIPS 204 | Module-LWE / SIS lattices | 3,309 B | 1,952 B |
| SLH-DSA-SHA2-128s (SPHINCS+) | FIPS 205 | SHA-256 (stateless) | 7,856 B | 32 B |
| Falcon-512 (FN-DSA) | FIPS 206 draft | NTRU lattices | ≤ 666 B | 897 B |
| ML-DSA-87 (Dilithium5) | FIPS 204, level 5 | Module-LWE / SIS lattices | 4,627 B | 2,592 B |
| SLH-DSA-SHAKE-128f (SPHINCS+) | FIPS 205 | SHA-3 / SHAKE256 (stateless) | 17,088 B | 32 B |
| Falcon-1024 (FN-DSA) | FIPS 206 draft, level 5 | NTRU lattices | ≤ 1,280 B | 1,793 B |
| Solana claims + ML-DSA-65 (hybrid) | IETF composite draft | either curve or lattice must hold | 3,373 B | 1,984 B |
certs = XMR.sign(H("bind" ‖ reward-address ‖ s ‖ H(pks)), leaf)
d = H("launch" ‖ creator ‖ image ‖ mint ‖ name ‖ scheme ‖ symbol)
The hybrid is a composite signature: a single seed expands (SHAKE256) into a Solana claims key and an ML-DSA-65 key, and a signature is both signatures concatenated. It verifies only if both verify, so it is secure as long as either assumption survives. SLH-DSA-SHAKE swaps the SHA-2 family for SHA-3, so the two SPHINCS+ options rest on different hash designs.
A scheme reward proof carries its signature, the scheme public key and the XMR certificate, so verification needs only the rewards root: check the certificate against the root, then the signature against the certified key. The digest commits to the scheme id, so a signature can never be replayed under a different scheme. Implementations are@noble/hashes, audited and dependency-free.
#Proof of possession
A challenge-response login that never consults Solana claims. The server issues a 32-byte nonce valid for five minutes; the client signs SHA-256("monpad/proof/v1\n" ‖ leaf ‖ nonce ‖ claims) with its next unused leaf.
| Check | Failure |
|---|---|
| challenge exists, matches claims, unused, unexpired | 404 / 409 / 410 |
| XMSS.verify(digest, σ, registered root) | verified: false |
| INSERT (rewards, leaf) into leaf ledger | 409 leaf already used |
The leaf ledger's primary key (rewards_id, leaf_index) makes one-time use a database invariant; the challenge is consumed even on failure, so a nonce cannot be retried.
#Claims
A monpad claims is a Solana wallet keypair derived from the rewards seed (HKDF(sk.seed, "monpad/claims/v1", index)), re-derived in the browser after unlock and never stored. Registration is a statement signed by the derived key itself, so the server only ever holds public addresses.
Every transfer is dual-signed: the derived key signs the Solana transaction (what the chain enforces), and a Monero-private key signs a transfer digest whose hash rides along in an SPL memo. Rotating a claims sweeps everything to a freshly derived address and retires the old one.
Be clear about the boundary. Solana still enforces only the Solana wallet signature and Monero payout address on-chain. The Monero-private half is a privacy-proof record of authorization: after a curve break it lets an owner prove which transfers they signed and which were forged, but it does not stop a forged transfer from executing. Holding value under Monero-private keys needs an on-chain verifier program: that is the privacy xmr-vault below, and the reason these claims are labelled rewards-derived rather than privacy-secured.
#Monero Reward Vault
The xmr-vault is a Solana program (DNsPfPecrbnS7jFqMpEDG3VoWAdkuaU2VxsmaPENcg9F) that holds SOL, SPL and Token-2022 balances at a PDA which only a XMR signature verified on-chain can spend. There is no Solana claims path: the claims that pays the fees cannot move xmr-vault funds, and neither can anyone else who breaks a curve.
A withdrawal signs one digest with xmr-vault[i]'s one-time key. It binds the program, xmr-vault, recipient, mint, amount and the hash of xmr-vault[i+1], so the signature authorizes that exact spend and nothing else:
The 2,176-byte payload is staged in a buffer account over three transactions, then the withdraw instruction walks the 67 hash chains (45 to 990 SHA-256 calls), checks that the result derives the xmr-vault's address, pays the recipient, rolls everything else into xmr-vault[i+1] and turns xmr-vault[i] into a permanent tombstone. Deposits that arrive at a retired address afterwards can be forwarded by anyone, only ever to the next xmr-vault the tombstone names.
One key, one message. Two XMR signatures from one key leak enough to forge a third. Before anything is broadcast, the browser records the spend, keyed by an HMAC only the rewards owner can compute, and every retry or second device replays that record. Signing is deterministic, so a replay produces the identical signature. The xmr-vault index itself lives on-chain: it is the first xmr-vault that is not a tombstone.
The remaining dependency is how the rewards is derived. A claims-only rewards is re-derivable by whoever holds the claims key, so a private-key attacker who recovers that key could also derive the xmr-vault keys. A passphrase-hardened rewards closes that gap.
#Security analysis
- Forgery
- Reduces to SHA-256 second-preimage resistance under the tweakable-hash model; ~2¹²⁸ privacy work.
- Key reuse
- Prevented server-side by the leaf ledger. A malicious server could accept reuse; clients should never sign twice with one leaf, and the UI always fetches the next free index.
- Claims compromise
- Claims-only identities are re-derivable by anyone holding the claims key, including a post-break attacker. Hardened identities additionally require the passphrase.
- Server compromise
- Cannot forge reward proofs (no secrets held). Can censor or lie about registrations, which is why the on-chain anchor and IPFS copy exist.
- Capacity
- 256 signatures per rewards. Rotation to a fresh tree signed by the old tree's next leaf is on the roadmap.
#Parameters
| Item | Value |
|---|---|
| Hash | SHA-256 |
| XMR | w = 16, 67 chains, 2,144 B |
| Tree | height 8, 256 leaves |
| Signature | 2,404 B |
| Public key | root ‖ pk.seed, 64 B |
| Address | xmr ‖ base58(SHA-256(dom ‖ pk.seed ‖ root)[0..20]) |
| Passphrase KDF | scrypt N = 2¹⁵, r = 8, p = 1 |
| Seed KDF | HKDF-SHA256 |
| Mint KDF | HKDF-SHA256(sk.seed, 'monpad/mint/v1', u32be(leaf)) |
#Proof it yourself
Every coin page has a Proof button that runs this in your browser. The equivalent in code:
import { launchDigest } from "@/lib/pq/messages";
import { verify } from "@/lib/pq/xmss";
const meta = await fetch(metadataUri).then((r) => r.json());
const d = launchDigest({
mint, creator, name: meta.name, symbol: meta.symbol,
image: meta.image, leaf: meta.monpad.signature.leaf,
});
const { valid, computedRoot } = verify(d, meta.monpad.signature, {
root: meta.monpad.root, pubSeed: meta.monpad.pubSeed, height: meta.monpad.height,
});