Back to app

ProofMatch Docs

Everything a collaborator needs to understand, run, and contribute to ProofMatch — a zero-knowledge proof-of-funds system on Stellar.

Introduction

ProofMatch generates a zero-knowledge proof that a Stellar wallet holds at least a given balance — verified on-chain by a Soroban smart contract — without ever revealing the actual balance. A user shares a single link; anyone who opens it sees one verified fact ("holds ≥ X") and nothing else.

Beyond a point-in-time solvency proof, ProofMatch includes a second circuit that proves a wallet held ≥ X for 7 consecutive days, defeating the borrow-prove-return gaming a single snapshot proof is vulnerable to. Every proof's commitment is permanently anchored on-chain via a dedicated Soroban registry contract.

Live demo: proofmatch-stellar.fly.dev — try the whole flow with a real Freighter wallet or the built-in demo account.

Architecture

ProofMatch is split into three repos, each with a focused responsibility:

proofmatch-circuits

Circom circuits (Groth16 over BLS12-381), trusted setup artifacts, and the proof-formatting scripts that convert snarkjs output into Soroban's expected hex layout.

proofmatch-contracts

The Soroban verifier contract and the on-chain proof registry (Rust). Deployed and live on Stellar testnet.

proofmatch-app

The web app (React + TypeScript frontend, Node/Express backend), REST API, and embeddable SDK. Vendors the compiled circuit artifacts and calls the deployed contracts.

How they connect: circuits are compiled once and vendored into proofmatch-app; the app generates proofs off-chain and submits them to the verifier contract, then anchors a commitment in the registry contract — both via the Stellar CLI, serialized through a single queue so concurrent proofs never race the deployer account's sequence number.

How solvency proofs work

The circuit (proof_of_funds.circom) enforces one constraint: balance ≥ threshold. threshold is public; balance is a private witness never sent anywhere.

Why the ZK is load-bearing, not decorative

If the balance doesn't meet the threshold, no valid witness or proof can be generated at all — the proving step fails outright. There is no "false" result to fake; a proof existing is itself the guarantee.

Two ways to generate the proof

  • Server-side — the app fetches the wallet's balance from Horizon and proves server-side. Fast, but the server briefly sees the real balance in memory.
  • Privacy mode (browser-side) — the proof is generated entirely in a Web Worker running snarkjs. The balance never leaves the user's device; only the 192-byte proof is sent to the server.

How 7-day stability proofs work

A single snapshot proof can be gamed: borrow funds right before proving, return them right after. The stability circuit closes this gap by proving balance ≥ threshold held true on all 7 of the last 7 days — a chain of comparators where every day must pass.

Daily balances are reconstructed by walking the account's Horizon effects history backwards from its current balance to each of the last 7 UTC-midnight boundaries. All 7 values stay private witnesses; only the pass/fail result and threshold are public.

Wallet connect & ownership verification

Wallets connect via Freighter. By default, a proof shows that some wallet holds the funds — not that the person sharing the link controls it. Optional SEP-53 signed-message ownership verification closes that gap: the server issues a single-use challenge, Freighter signs it with the account's key, and the server verifies the signature against the claimed account before marking the proof ownershipVerified: true.

On-chain contracts (Stellar testnet)

Verifier CB33TGWZROSRQRKFVZGZV74SKFGSDC6HOEN366PF2S2WD7ESRCHIR6VB

Verifies Groth16 proofs on-chain using Stellar's native BLS12-381 host functions. Exports verify_proof(vk, proof, pub_signals) → bool.

Registry CCB7YWB42PZNXNCQVAUNX7LWYDEAAJ6YSLMCK6LPDDNMPMQEMDTMZGKL

Permanently anchors each proof's commitment. Exports store, exists, and get.

REST API

Full endpoint reference (request/response shapes, the embeddable widget, and the SDK) lives in the API Reference. The core flow, in short:

POST /api/prove-from-account // solvency proof, server-fetched balance POST /api/verify-proof // solvency proof, browser-generated (privacy mode) POST /api/prove-stability // 7-day streak proof GET /api/proof/:id // fetch a stored proof's public record GET /v/:id // human-readable share/verify page

Contributing

All three repos are open source (MIT) with scoped issues ready to pick up — spanning security hardening, new circuit features, contract improvements, testing, and CI/CD. Every PR is functionally tested before merging (we run the real code — spin up the server, submit real testnet transactions — not just review the diff), and we aim to review promptly.

Issues labeled good first issue are self-contained and low-risk. Higher-context issues (circuit changes, contract security) have more detail in their description — read the linked file/function before starting.

Known limitations

Documented honestly, not hidden — several are already open as issues for contributors.
  • Wallet ownership isn't proven by default. Anyone can generate a proof for any public address unless SEP-53 ownership verification is used.
  • Single-contribution trusted setup. Both circuits' Groth16 ceremonies have one contribution — fine for a hackathon, not production.
  • Balance witness trusts the server's Horizon fetch rather than an in-circuit signature check — blocked today on regenerating Poseidon round constants for BLS12-381 (circomlib hardcodes BN254).
  • Registry has no access control — anyone can write a commitment, including squatting a proof ID before the legitimate one lands.

FAQ

Is this running on mainnet?

No — testnet only. Contracts, wallets, and all demo flows use Stellar testnet.

Can I self-host this?

Yes — see DEPLOY.md in proofmatch-app for the Fly.io deployment used for the live demo, plus a Cloudflare-tunnel option for quick local sharing.

Where do I start as a new contributor?

Pick an open issue labeled good first issue in whichever repo matches your interest (circuits/ZK, contracts/Rust, or app/TypeScript), read its linked code pointers, and open a PR against that repo's main.