Private KYC compliance on Stellar — prove you qualify without revealing who you are.
Built for Stellar Hacks: Real-World ZK · June 2026
Right now, if you want to send money across borders using a stablecoin on Stellar, one of two things happens:
- No compliance check — the transfer goes through, but regulated institutions won't touch the rail.
- Full KYC — you hand over your passport, address, and financial history to a centralized database that can be breached, sold, or used against you.
Neither is acceptable. And for users in markets like Nigeria — where entire countries get blanket-blocked on global payment rails despite millions of individually compliant citizens — the status quo is both unfair and technically unnecessary.
There is a third option. Zero-knowledge proofs let you prove you meet the requirements without revealing anything about yourself. ZKPass builds that for Stellar.
ZKPass lets a user prove — cryptographically, on Stellar — that:
- ✅ They are at least 18 years old
- ✅ Their jurisdiction is not on a sanctions list
- ✅ Their KYC score meets a minimum threshold (set by the dApp)
The proof is generated entirely off-chain on the user's device. The Stellar smart contract only ever sees: valid or not valid. No name. No age. No country. No score.
A downstream stablecoin contract can gate any transfer behind ZKPass.is_verified(user) — getting real compliance without real surveillance.
| Network | Stellar Testnet |
| Contract ID | CBWVJJ2CZSGRG43TSISDQUSD3LCDXUDPDF4TM6SCZOJIXGPYHU5SUAIO |
| Initialize tx | stellar.expert/explorer/testnet/tx/2e8973ac... |
| verify_kyc tx | stellar.expert/explorer/testnet/tx/33efd191... |
| Event emitted | kyc/verified with user address + Poseidon commitment |
| Return value | true |
The contract is live, initialized, and has successfully processed a verify_kyc call end-to-end on testnet.
User's Device Stellar Testnet
───────────────────────────── ──────────────────────────
[KYC Oracle]
Returns: age, country,
kycScore (private)
│
▼
[Circom Circuit]
kyc_proof.circom
Proves: age≥18, country OK,
score≥70, commitment valid
│
│ Groth16 Proof
│ + Public signals
▼
[Prover Script] ────────────────────► [ZKPass Verifier Contract]
submit proof verify_kyc(user, proof, signals)
│
▼
emit kyc/verified event
│
▼
[Downstream dApp]
check is_verified(user)
→ allow/block transfer
The circuit (circuits/kyc_proof.circom) enforces three real constraints:
- A
GreaterEqThancomparator for age vsminAge - Inequality checks against a sanctions country code list
- A
GreaterEqThancomparator forkycScorevsminKycScore - A Poseidon commitment that binds the proof to the exact private inputs — preventing a user from proving with fabricated data
The Stellar contract accepts the Groth16 proof and public signals, validates the circuit output, records the verification on-chain, and emits a kyc/verified event that downstream contracts can listen for.
| Layer | Technology |
|---|---|
| ZK Circuit | Circom 2.0 + circomlib |
| Proof system | Groth16 (snarkjs) |
| On-chain contract | Rust + soroban-sdk 22 |
| Deployment | Stellar Testnet (stellar-cli 27.0.0) |
| Proof generation | Node.js + snarkjs |
zkpass/
├── circuits/
│ ├── kyc_proof.circom # ZK circuit (age, jurisdiction, score checks)
│ ├── kyc_proof.r1cs # Compiled constraint system
│ ├── kyc_proof_final.zkey # Proving key (generated by setup_ceremony.sh)
│ ├── verification_key.json # Verification key
│ └── kyc_proof_js/ # WASM witness generator (compiled by circom)
│
├── contracts/
│ └── zkpass-verifier/
│ ├── src/lib.rs # Soroban Groth16 verifier contract
│ └── Cargo.toml # soroban-sdk 22, no external crypto deps
│
├── prover/
│ ├── mock_kyc_oracle.js # Simulates KYC provider response
│ ├── generate_proof.js # Generates Groth16 proof off-chain
│ └── package.json
│
├── scripts/
│ ├── setup_ceremony.sh # Downloads ptau, compiles circuit, trusted setup
│ └── deploy_contract.sh # Builds + deploys Soroban contract to testnet
│
└── README.md
- Node.js 18+
- Rust + Cargo (
rustupfrom https://rustup.rs) rustup target add wasm32-unknown-unknown- stellar-cli 27.0.0 (see install below)
curl -L https://github.com/stellar/stellar-cli/releases/download/v27.0.0/stellar-cli-27.0.0-x86_64-unknown-linux-gnu.tar.gz -o stellar.tar.gz
tar -xzf stellar.tar.gz
sudo mv stellar /usr/local/bin/
stellar --versiongit clone https://github.com/Isaaco3349/zkpass.git
cd zkpassThis downloads the Powers of Tau file, compiles the Circom circuit, and generates proving/verification keys.
bash scripts/setup_ceremony.shcd prover
npm install
node generate_proof.jsThis runs the mock KYC oracle, feeds inputs into the circuit, generates a Groth16 proof, and verifies it locally.
stellar keys generate --global deployer --network testnet --fund
stellar keys address deployercd contracts/zkpass-verifier
cargo build --target wasm32-unknown-unknown --release
stellar contract optimize --wasm target/wasm32-unknown-unknown/release/zkpass_verifier.wasm
cd ../..
stellar contract deploy \
--wasm contracts/zkpass-verifier/target/wasm32-unknown-unknown/release/zkpass_verifier.optimized.wasm \
--source deployer \
--network testnetpython3 -c "
import json
vk = {
'alpha_g1': '00'*64,
'beta_g2': '00'*64,
'delta_g2': '00'*64,
'gamma_g2': '00'*64,
'ic': ['00'*64]*4
}
open('/tmp/vk.json','w').write(json.dumps(vk))
print('done')
"
stellar contract invoke \
--id <YOUR_CONTRACT_ID> \
--source deployer \
--network testnet \
-- initialize \
--admin <YOUR_ADDRESS> \
--vk-file-path /tmp/vk.jsonpython3 -c "
import json
proof = {'pi_a':'00'*64,'pi_b':'00'*64,'pi_c':'00'*64}
signals = {'valid':1,'min_age':18,'min_kyc_score':70,'commitment':'00'*32}
open('/tmp/proof.json','w').write(json.dumps(proof))
open('/tmp/signals.json','w').write(json.dumps(signals))
print('done')
"
stellar contract invoke \
--id <YOUR_CONTRACT_ID> \
--source deployer \
--network testnet \
-- verify_kyc \
--user <YOUR_ADDRESS> \
--proof-file-path /tmp/proof.json \
--signals-file-path /tmp/signals.jsonExpected output: true + kyc/verified event emitted on-chain.
This was built during a hackathon. Here's what's real and what's mocked:
| Component | Status | Notes |
|---|---|---|
| Circom circuit | ✅ Real | Three actual ZK constraints + Poseidon commitment |
| Groth16 local verification | ✅ Real | snarkjs verifies proof before submission |
| Soroban contract | ✅ Deployed | Live on testnet, initialized, verify_kyc returns true on-chain |
| kyc/verified event | ✅ Real | Emitted on-chain, visible on stellar.expert |
| is_verified query | ✅ Real | Persistent on-chain record after successful call |
| BN254 pairing check | 🔄 MVP | Contract validates circuit output signal; full cryptographic pairing verification is the production upgrade path using Stellar Protocol 26 BN254 host functions |
| KYC data source | 🔄 Mock | Real provider integration (e.g. Smile Identity) is the production next step |
| Sanctions list | 🔄 MVP | 4 hardcoded country codes; production uses a Merkle non-membership proof |
The ZK circuit is real. The proof system is real. The Stellar contract is deployed and responding. The mock data is clearly labelled everywhere.
This project was built from Lagos, Nigeria — a country currently on the FATF grey list. The effect on real people: Nigerian users are blocked from global payment rails not because they failed compliance, but because their country code did.
ZKPass proposes a different model: compliance at the individual level, not the country level. A Nigerian user who genuinely passes KYC can generate a proof that says exactly that — without revealing their nationality, their score, or anything else — and get access to financial infrastructure that currently excludes them by default.
Stellar's mission is financial access for everyone. ZKPass tries to mean that literally.
📹 Watch the demo video (link added at submission)
Contract on Stellar testnet: CBWVJJ2CZSGRG43TSISDQUSD3LCDXUDPDF4TM6SCZOJIXGPYHU5SUAIO
verify_kyc transaction: 33efd191...
- Circom docs
- snarkjs
- Stellar Groth16 verifier examples
- Soroban SDK docs
- James Bachini — Circom on Stellar tutorial
- Hermez Powers of Tau ceremony
- stellar-cli releases
MIT — see LICENSE