Pin the criteria to the wall before opening claims
This explains the design. Not a product pitch, not a fundraising slide. How the mechanism runs, what we traded for what, and where there is still no answer.
1 · The order is backwards
The usual sequence:
- Export the wallet list with a hand-written script.
- Build a merkle tree with a throwaway tool.
- Push an unreviewed claim contract.
- Publish the criteria.
Look at step 4. It comes after step 1. When criteria are written after the list is sealed, nobody — not even the most honest operator — can prove the list matched the criteria rather than the criteria being written to fit the list. The community asks "why isn't my wallet on the list", and the best the team has is an explanation. Explanations cannot be verified.
Steps 1–3 break in fixable ways. Step 4 being in the wrong place is not fixable with engineering — it is an ordering problem, so the fix is an ordering constraint enforced by a contract.
Momentum is 35/100 and only 4 of CoinGecko's 15 trending slots hold a meme token. Thin attention means a broken drop has no incoming crowd to cover it. Meanwhile the segment is $30.51B across 166 tokens still doing real volume — $2.33B in 24 hours. The top ten already hold 76.6% of that market cap. Real tokens still need distributing; the gap is not in issuing new ones, it is in the infrastructure for the ones that exist.
2 · Anchor first, snapshot after
The operator writes a rule set. It is normalised — addresses lower-cased, exclusions sorted and de-duplicated — and rendered into a canonical text document with a fixed field order. The same rules always produce the same bytes.
keccak256 of that document is the rules hash. It goes on-chain via RulesAnchor.anchor(), together with the token, the snapshot block, and the claim deadline. The call reverts if the snapshot block is not still in the future.
The chain reaches the committed block. Nothing happens on our side; there is nothing to decide.
The engine replays every Transfer event on the token from its deployment block to the snapshot block, rebuilding each balance and the continuous-holding window as it goes. It then samples balances and compares them against eth_call balanceOf at the same block. A mismatch voids the run.
Eligible wallets are allocated pro-rata by balance, with per-wallet cap overflow redistributed until nobody is over. All arithmetic is in base units; integer-division dust is handed out by largest remainder so the total is exact to the last unit.
A merkle tree is built with double-hashed leaves and sorted pairs. The root goes into an immutable in the claim contract, alongside the deadline and the treasury. Neither the operator nor we can change it afterwards.
3 · Trade-offs we chose
this is the real part. every choice above costs something.
An explorer's holder list tells you who holds now. A drop needs who held at the snapshot block, sellers included. So the engine walks the whole transfer history. That is slower, and for a token with millions of transfers it needs an archive endpoint you supply — the free tier caps out. We took the slowness: a list nobody can reproduce is not worth serving fast.
The window check could be done by reading a handful of past balances. Instead the balance timeline is reconstructed from the same log replay, so a wallet that dipped below the minimum for one block inside the window is caught. Cost: the whole history has to be walked even for a 7-day window.
LP attribution works for V2-shaped pools, where an LP token maps linearly onto reserves. V3 and V4 positions are NFTs with tick ranges; any single number we invented for them would be a guess wearing a proof's clothing. Those pools come back marked unsupported.
No pause, no root update, no emergency withdraw, no proxy. That means a genuine mistake in a rule set cannot be corrected after anchoring — the drop has to be abandoned and re-run. We think a contract that cannot be quietly edited is worth more than one that can be rescued.
Every page reconciles its stored row against the chain on load and prints the disagreement when there is one. The database exists to make the registry fast, not to make claims.
4 · Where the contracts live
There is no address hard-coded for any chain. An instance is only able to anchor on a chain where someone actually deployed RulesAnchor and set the matching environment variable.
Rule writing and dry runs work — they only read public chain data. Anchoring needs a deployment:
5 · Still open
no answers here. posted because hiding it would be weird.
On the free public endpoints, eth_getLogs is capped at 10,000-block ranges and the engine refuses to silently truncate. A token with hundreds of thousands of transfers therefore needs XEROXDROP_RPC_* pointed at an archive node. There is no free path around this that we have found.
The contracts are small and have tests that run the real bytecode in a local EVM, including forged-proof and internal-node-as-leaf cases. That is not an audit. Fee tiers and audit scope: not determined.
Anyone can replay and verify the root independently — that is settled. But whoever runs the instance still decides when to publish. The root going on-chain via publishRoot() bounds the damage, it does not remove the dependency.
A rule set covers one token on one chain. Tokens bridged to several chains would need a merged snapshot with a shared canonicalisation, and we have not designed that.
6 · In short
This product does not make your drop fairer. It makes your criteria appear first, in full, where everyone can read them, and impossible to edit afterwards — and it makes the list you produce reproducible by anyone with an RPC URL.
Pin it to the wall. Then open the door.
Disclosure: to be completed by legal. Snapshot & claim contract service · no custody · fee per drop.
