Why Numex Uses Blockchain
Why Numex Uses Blockchain
Three cryptographic layers for three trust problems
Numex is a marketplace for graded, vaulted coins. Every coin on the platform sits inside a third-party vault, authenticated by a professional grading company, and is traded entirely online. That creates three distinct trust gaps:
- Provenance. How does a buyer know the on-screen listing corresponds to a real, specific physical coin? Grading companies issue certificates, but certificates can be duplicated, mismatched, or fabricated. There is no native digital binding between a physical coin and a digital token.
- Fairness. Numex sells curated "packs" -- randomized bundles of coins. How does the buyer know the pack outcome was not pre-selected to dump low-value inventory? Traditional marketplaces ask you to trust the operator. We would rather prove it.
- Privacy. Ownership of rare coins is sensitive. Portfolio values signal wealth. Specific holdings invite targeted offers -- or worse. How can a collector prove they own something without revealing what?
Each gap requires a different cryptographic tool. A commitment scheme ties coins to tokens. A commit-reveal protocol proves reveal fairness. A zero-knowledge proof (a proof that reveals nothing except that a statement is true) provides private ownership attestations. Three layers, three hash functions, one chain.
We deploy on Base, Coinbase's Layer 2 rollup secured by Ethereum. Transactions settle in seconds for a fraction of a cent. Most buyers never interact with the chain directly -- the blockchain is infrastructure, not interface. But every proof is independently verifiable by anyone who cares to check.
This post walks through exactly what each layer does, what it proves, and -- critically -- what it does not prove. Every contract is live on Base Mainnet with links to Basescan so you can verify the code yourself.
Tying Physical Coins to Onchain Tokens
The integrity proof
Every coin on Numex has a human-readable identity string called a fingerprint. It encodes every fact that identifies that specific physical object:
Numex Coin | PCGS 12345678 | 1921 D Morgan Dollar | MS65 | vault-item-abc123
That string includes the grading company and certificate number, the year, mint mark, denomination, series, grade, and the vault's internal item identifier. Given a fingerprint, anyone can look up the certificate on the grading company's website, confirm the grade, and match it to the vault record.
The fingerprint alone is not enough. We need a deterministic, tamper-evident link to an onchain token. Here is the formula:
function computeIntegrityProof(
string memory fingerprint,
bytes32 salt
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(fingerprint, salt));
}
function tokenIdForProof(bytes32 integrityProof) public pure returns (uint256) {
return uint256(integrityProof);
}
keccak256 is the hash function built into every Ethereum Virtual Machine. It is implemented as a native opcode (0x20), making it the cheapest hash to compute onchain. We hash the fingerprint concatenated with a random salt (a 256-bit random value) that prevents brute-force reversal). The resulting 32-byte digest is the integrity proof and, cast to a uint256, becomes the ERC-721 token ID.
This means:
- Deterministic. Given the same fingerprint and salt, any party recomputes the same proof and arrives at the same token ID. There is no ambiguity about which onchain token represents which coin.
- Tamper-evident. Change a single character in the fingerprint -- alter the grade, swap the cert number -- and the hash changes completely. The token ID no longer matches. Verification fails.
- Onchain. The
computeIntegrityProoffunction ispublic pure. Anyone can call it from Basescan, a script, or their own contract.
What this is NOT: This is a verifiable commitment, not a zero-knowledge proof. The fingerprint and salt are known to the verifier. There is no hidden information. The cryptography here provides binding (you cannot change what the token represents after minting) and integrity (any modification is detectable), not privacy.
The integrity proof is stored as an ERC-721 non-fungible token on the NumexRegistryV2 contract. Ownership transfers, custody status transitions (Vaulted to RedemptionPending to Redeemed), and burns are all mediated by onchain role-gated functions with a strict state machine.
Live contract: NumexRegistryV2 on Basescan
Proving Pack Reveals Are Not Rigged
The commit-reveal protocol
When Numex assembles a coin pack, the server knows which coins are in the pool before any buyer opens a pack. Without a fairness mechanism, the server could cherry-pick outcomes. The commit-reveal protocol eliminates this by splitting the process into two tamper-proof phases.
Phase 1: Commit (before any buyer acts)
- The server generates a
serverSeed-- a cryptographically random value. - The server builds a SHA-256 Merkle tree from the pool items. Each leaf is the SHA-256 hash of a coin's identifying data. The tree root commits to the exact set of items in the pool.
- The server computes
seedCommitment = SHA-256(serverSeed)-- a commitment that locks the server seed without revealing it. - All three values --
poolId,merkleRoot, andseedCommitment-- are written to theNumexPackCommitmentRegistryV2smart contract. Once onchain, they are immutable.
function commitPoolV2(
bytes32 poolId,
uint256 poolVersion,
uint256 poolSize,
bytes32 merkleRoot,
bytes32 serverSeedCommitment,
string calldata metadataUri
) external onlyRole(OPERATOR_ROLE) whenNotPaused
Phase 2: Reveal (after the buyer acts)
- The buyer provides a
buyerNonce-- their own random contribution to the selection process. - The server computes the reveal seed:
revealSeed = SHA-256(serverSeed:buyerNonce:packId). Because the server seed was committed before the buyer nonce existed, the server cannot manipulate the outcome retroactively. - The selected item index is derived deterministically:
selectedIndex = uint256(hash(revealSeed)) mod poolSize. - The server provides a Merkle inclusion proof showing the selected item was a leaf of the committed tree.
- The chain records the
openingId,revealSeedHash, andselectedItemHash, anchoring the outcome permanently.
Why SHA-256? This Merkle tree operates offchain in TypeScript. There is no EVM gas constraint, so we use the industry-standard hash. The onchain contract stores these hashes as opaque bytes32 values -- the chain anchors the commitment; the verification logic runs client-side.
Four verification checks
| Check | What it proves | How |
|---|---|---|
| Seed commitment | Server seed was fixed before buyer acted | SHA-256(serverSeed) = seedCommitment stored onchain |
| Deterministic selection | Index derived fairly from both parties' inputs | selectedIndex = uint256(SHA-256(serverSeed:buyerNonce:packId)) mod poolSize |
| Merkle inclusion | Selected item was in the committed pool | Merkle proof against onchain merkleRoot |
| Chain anchor | Outcome is permanently recorded | revealSeedHash and selectedItemHash stored in RevealRecordV2 |
What this does NOT prove
Honesty requires stating the limits:
- Pool completeness. The Merkle tree commits to a set of items, but nothing proves the server included all available inventory. The server could have withheld premium coins from the pool. You trust that the committed set is the advertised set.
- Pool fairness / odds. If a pack advertises "1 in 10 chance of a key date," that ratio is a product decision, not a cryptographic guarantee. The protocol proves the selection mechanism is fair, not that the pool composition is favorable.
- Server seed entropy. The commitment proves the seed was fixed before reveal, not that it was random. A weak seed still produces a deterministic, verifiable outcome -- just a predictable one. In practice we use a CSPRNG (cryptographically secure pseudorandom number generator), but the protocol does not enforce this.
This is NOT zero-knowledge. Every input -- the server seed, the buyer nonce, the Merkle tree -- is visible to the verifier after reveal. The cryptography here provides fairness (neither party can rig the outcome), not privacy.
Live contract: NumexPackCommitmentRegistryV2 on Basescan
Proving You Own a Coin Without Revealing Which One
Zero-knowledge ownership proofs
This is the only actual zero-knowledge component in the Numex system.
A collector may want to prove they own a coin graded MS67 or above, or that their portfolio contains at least one key date Morgan Dollar, without revealing which specific tokens they hold. Traditional blockchain ownership is fully transparent -- your wallet, your tokens, your entire portfolio are public. Zero-knowledge proofs invert this.
The Numex ownership circuit is written in Noir, a Rust-like domain-specific language for ZK circuits. It compiles to an UltraPlonk arithmetic circuit and is proved by Barretenberg, Aztec's proving backend. The structured reference string (SRS) comes from the Aztec Connect trusted ceremony.
Circuit inputs:
Circuit logic (four constraints):
// 1. Compute leaf from private inputs
let leaf = pedersen_hash(user_secret, asset_commitment);
// 2. Verify nullifier binds to this secret and this root
assert(nullifier == pedersen_hash(user_secret, ownership_root));
// 3. Walk Merkle path and verify root
assert(verify_merkle_path(leaf, merkle_path, merkle_indices, ownership_root));
// 4. Path indices must be binary
assert(merkle_indices[i] == 0 || merkle_indices[i] == 1);
The hash function inside the circuit is Pedersen hash, not keccak256 or SHA-256. Pedersen hashing is native to the BN254 elliptic curve used by Barretenberg. It requires far fewer arithmetic constraints than keccak256 would inside a circuit, keeping proof generation fast -- under two seconds on commodity hardware.
The tree depth is 16, supporting up to 65,536 ownership commitments. The nullifier (derived from the user's secret and the current root) prevents proof replay: once a nullifier is consumed onchain, the same proof cannot be reused.
Verification chain:
The collector generates a proof locally and submits it to the smart contract. The call path is:
NumexRegistryV2.verifyOwnership(root, nullifier, proof)- The contract delegates to
OwnershipVerifier.verifyProof(root, nullifier, proof) OwnershipVerifierpacks the two public inputs and callsUltraVerifier.verify(proof, publicInputs)UltraVerifierperforms a BN254 elliptic curve pairing check and returnstrueorfalse
function verifyOwnership(
bytes32 root,
bytes32 nullifier,
bytes calldata proof
) external view returns (bool) {
if (address(ownershipVerifier) == address(0)) revert VerificationFailed();
if (root != ownershipRoot) revert VerificationFailed();
return ownershipVerifier.verifyProof(root, nullifier, proof);
}
Use cases: Private portfolio attestations ("I own a coin graded above MS66"), gated access to collector communities, anti-front-running protection for high-value trades where revealing intent would move the market.
Live contracts: OwnershipVerifier on Basescan | UltraVerifier on Basescan
Verify It Yourself
Three cryptographic layers. Three hash functions. Each one chosen for its domain:
- keccak256 runs onchain in Solidity. It powers integrity proofs, token ID derivation, and the V2 Merkle verification inside
NumexPackCommitmentRegistryV2. It is the cheapest hash the EVM offers. - SHA-256 runs offchain in TypeScript. It powers the reveal fairness protocol -- seed commitments, reveal seeds, and the pack Merkle trees. It is the standard hash for general-purpose cryptographic commitments.
- Pedersen runs inside the ZK circuit in Noir. It powers ownership commitments, nullifiers, and the ownership Merkle tree. It is native to BN254 and minimizes constraint count.
None of these layers require trust in Numex. The contracts are verified on Basescan. The circuit source is open. The verification scripts run locally.
Live contracts on Base Mainnet
| Contract | Address |
|---|---|
| NumexRegistryV2 | 0x559eeEcf3d15ad05e9617414dfb044Be8E7067E2 |
| NumexPackCommitmentRegistryV2 | 0x4B050Cc6CB1447f6f9d1cE6578de5Cfe1Ac4464e |
| OwnershipVerifier | 0xc59c9F8F0F148c6cBd3FFc1a52A1505e3189374a |
| UltraVerifier | 0x818c63b56277dA5C4456e3fE27B82c28Ba9B3a1A |
Verify locally
npm run verify:reveal-proofs # Replays reveal fairness proofs against chain data
npm run verify:chain-integrity # Checks integrity proofs match onchain token IDs
Every claim in this post is falsifiable. That is the point.