Why Numex Uses Blockchain

Numex·
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

Integrity proof flow

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 computeIntegrityProof function is public 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.

!Integrity proof flow diagram

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, and seedCommitment -- are written to the NumexPackCommitmentRegistryV2 smart 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, and selectedItemHash, 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

CheckWhat it provesHow
Seed commitmentServer seed was fixed before buyer actedSHA-256(serverSeed) = seedCommitment stored onchain
Deterministic selectionIndex derived fairly from both parties' inputsselectedIndex = uint256(SHA-256(serverSeed:buyerNonce:packId)) mod poolSize
Merkle inclusionSelected item was in the committed poolMerkle proof against onchain merkleRoot
Chain anchorOutcome is permanently recordedrevealSeedHash 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.

!Reveal fairness flow diagram

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)
  • OwnershipVerifier packs the two public inputs and calls UltraVerifier.verify(proof, publicInputs)
  • UltraVerifier performs a BN254 elliptic curve pairing check and returns true or false
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.

!ZK ownership flow diagram

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.

!Hash boundary map

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

ContractAddress
NumexRegistryV20x559eeEcf3d15ad05e9617414dfb044Be8E7067E2
NumexPackCommitmentRegistryV20x4B050Cc6CB1447f6f9d1cE6578de5Cfe1Ac4464e
OwnershipVerifier0xc59c9F8F0F148c6cBd3FFc1a52A1505e3189374a
UltraVerifier0x818c63b56277dA5C4456e3fE27B82c28Ba9B3a1A

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.