Privacy-preserving, reusable credentials for the Stellar ecosystem.
A user holds a credential issued by a trusted party (KYC provider, bank, government, employer), generates a zero-knowledge proof locally in the browser, and submits it once on-chain. Any Stellar protocol can then check the result with a single read-only contract call — verify once, trusted everywhere — and the underlying credential data never touches the chain.
StellarCred is not a KYC app. It's the interoperability layer between issuers and protocols: issuers integrate once, protocols integrate once, and users carry reusable proofs instead of re-submitting personal data to every app.
- Real on-chain ZK verification.
CredentialVerifierruns the host-native BN254 UltraHonk verifier (rs-soroban-ultrahonk), not a stub. The full path — protocol → ProofRegistry → IssuerRegistry + CredentialVerifier → BN254 — is covered by 21 passing contract tests over genuine proofs for all four credential types. - Issuer signature verified in zero-knowledge. Each circuit verifies the
issuer's secp256k1 ECDSA signature over the credential commitment
(
std::ecdsa_secp256k1) inside the proof, and the contract binds that key to the registered issuer — a proof only passes if a registered issuer actually signed the credential. is_verified/check_claimare the on-chain primitives.is_verifiedchecks binary claims (kyc, jurisdiction);check_claim(holder, type, min_threshold)enforces numeric thresholds (age, income, funds) — a proof for ≥200k satisfies a ≥50k gate. One read, no API, no data handling.- Multi-claim issuance. One verification issues every requested credential (KYC, age, jurisdiction, …) in a single call.
- Drop-in integration. The
@stellarcred/sdkgives protocolshasClaim,getClaims, andbuildVerifyUrl, plus a return-URL redirect flow so users verify and bounce straight back.
Issuer ──signs──▶ Credential (held by the user, never on-chain)
(KYC provider, │
bank, gov) │ Noir circuit runs in the browser (WASM)
│ → UltraHonk proof + public inputs
▼
┌──────────────────────────────────────────────────────────────┐
│ Soroban contracts │
│ │
│ IssuerRegistry ──── trust root: which issuers, which types, │
│ and their signing public keys │
│ CredentialVerifier ─ stateless UltraHonk verify (VK / type) │
│ ProofRegistry ────── caches "address X verified until T", │
│ gated on the registered issuer key │
│ GatedPool ────────── demo DeFi pool gated on a KYC proof │
└──────────────────────────────────────────────────────────────┘
▲
│ one read-only call: is_verified(wallet, claim)
Any protocol / the @stellarcred/sdk
Issue → Prove → Verify. The issuer computes commitment = Poseidon2([value, salt]) and signs it server-side. The holder generates a proof locally
(noir_js + bb.js) that proves knowledge of the preimage and the issuer's
signature, then calls ProofRegistry.submit_proof. The registry verifies the
proof once and caches the result; every protocol afterwards reads
is_verified instead of re-running the verifier.
For a detailed architectural description with diagrams, see the Architecture Documentation.
contracts/ Soroban workspace (Rust, soroban-sdk 26)
issuer_registry/ trust root; submit_proof checks is_valid_issuer
credential_verifier/ real UltraHonk verify via host-native BN254 (VK per type)
proof_registry/ caches verifications w/ expiry + TTL; gated on issuer key
gated_pool/ demo DeFi pool gated on a KYC proof
circuits/ Noir circuits (UltraHonk · Noir 1.0.0-beta.9 / bb 0.87.0)
commit/ issuer helper: returns Poseidon2([value, salt], 2)
kyc_proof/ "I hold a credential the issuer committed to"
age_proof/ "I am over N" without revealing date of birth
income_proof/ "my income exceeds T" without revealing it
jurisdiction_proof/ "my country is not restricted" without revealing it
funds_proof/ "my balance exceeds T" without revealing it
scripts/build.sh compile + prove + stage circuit JSON for the frontend
fixtures/<type>/ real vk / proof / public_inputs per type (contract tests)
frontend/ Next.js 14 app (App Router)
app/ landing, holder, verify, issuer, apps, developers, docs
app/api/issue/ server-side credential issuance, via @stellarcred/issuer
packages/sdk/ @stellarcred/sdk — hasClaim / getClaims / buildVerifyUrl
packages/issuer/ @stellarcred/issuer — server-only issuance (value/salt/commitment/sig)
lib/ proof.ts (noir_js + bb.js), contracts.ts (stellar-sdk), wallet
scripts/deploy.sh deploy + wire + register issuer + install all VKs on testnet
scripts/benchmark.sh measure instruction budget for every public function on testnet
BENCHMARKS.md per-function instruction counts, ledger I/O, and fee estimates
All five credential circuits share one commitment scheme,
commitment = Poseidon2([value, salt], 2), so the issuer derives every
commitment with a single commit helper and each type is independently issuable
and provable.
Install the SDK:
npm install @stellarcred/sdkFull SDK docs:
frontend/packages/sdk/README.md· npm
Protocols never handle credential data - they ask the on-chain registry one question: has this wallet proven the claim I require?
import { StellarCred } from "@stellarcred/sdk";
// Binary claim — KYC, jurisdiction
const canDeposit = await StellarCred.hasClaim(wallet, "kyc");
// Threshold claim — enforce minimum on-chain
const canAccessVault = await StellarCred.hasClaim(wallet, "funds", { minThreshold: 50000 });
const canTrade = await StellarCred.hasClaim(wallet, "age", { minThreshold: 21 });
const canAccessVault = await StellarCred.hasClaim(wallet, "funds", {
minThreshold: 50000,
});
const canTrade = await StellarCred.hasClaim(wallet, "age", {
minThreshold: 21,
});If the user hasn't verified yet, send them to StellarCred and get them back automatically:
const verifyUrl = StellarCred.buildVerifyUrl({
returnUrl: "https://yourapp.xyz/deposit",
claim: "kyc",
});
window.location.href = verifyUrl;
// StellarCred returns the user to returnUrl with ?sc_verified=true&sc_wallet=…Two entry points, one backend:
- User-first — a user verifies at StellarCred before ever visiting your app; their credentials work everywhere immediately.
- Protocol-first — your app shows a "Verify" button, the user verifies, and returns automatically.
Prefer Soroban directly? Read ProofRegistry.is_verified from your own contract;
no SDK required. See /developers for the
full reference.
| Claim | Proves | Private input kept hidden |
|---|---|---|
kyc |
Identity verified | The credential secret |
age |
Age ≥ threshold | Date of birth |
income |
Income ≥ threshold | Actual income |
jurisdiction |
Country not restricted | Country code |
funds |
Balance ≥ threshold | Exact balance (from Plaid) |
- Issuer ↔ commitment binding is cryptographic. The issuer signs the
commitment with secp256k1; each circuit verifies that signature in-circuit
(
std::ecdsa_secp256k1) against the issuer public key (a public input), andProofRegistrychecks that public-input key equals the issuer's registered key. A proof is accepted only if a registered, trusted issuer actually signed the credential.prehash: falseis required when signing — Noir uses the raw 32-byte commitment as the message digest directly.
- The signing key is server-side only. Issuance runs in the
/api/issueroute handler and signs withISSUER_PRIVATE_KEY(never prefixedNEXT_PUBLIC_, never shipped to the browser). A production issuer would hold this key in an HSM or secrets manager. With no key set, the route runs a clearly-logged demo fallback. - Attestation relay. Issuance can be gated on a real KYC provider — the route integrates Persona's sandbox and only signs credentials after a positive result. Identity fields are sent once to the provider and never stored.
- Proof expiry.
ProofRegistryuses persistent storage with an explicitexpiry(checked against ledger time) plus TTL extension. - Contract upgradeability.
ProofRegistrysupports an admin-controlled upgrade path using Soroban's nativeupdate_current_contract_wasmcapability. The administrative key is initialized at deployment time and can be subsequently transferred to a multisig wallet or DAO.
- Rust (nightly ok) + the
wasm32v1-nonetarget:rustup target add wasm32v1-none - Stellar CLI v26+ to deploy (BN254 protocol); verified on 27.0.0
- Noir
nargo1.0.0-beta.9 (noirup -v 1.0.0-beta.9) and Barretenberg bb v0.87.0 (bbup -v 0.87.0) — the versions must match or the VK won't validate - Node 20+ and pnpm
# Contracts — real proof verification in tests
cargo test # 21 tests, incl. genuine BN254 verification
stellar contract build # wasm artifacts → target/wasm32v1-none/release
# Circuits — compile, prove, and stage circuit JSON for the frontend
./circuits/scripts/build.sh # all circuits
./circuits/scripts/build.sh kyc_proof # one
# Frontend
cd frontend && pnpm install && pnpm devA public record of deployed contract IDs on testnet and mainnet, along with instructions to verify the bytecode integrity from source, is maintained in DEPLOYMENTS.md.
Already have the toolchain installed and contracts deployed? Run the end-to-end demo — it seeds a wallet, issues mock credentials, generates proofs, submits them on-chain, and prints the verified claims table:
./scripts/demo.shWhat it does: creates a fresh testnet wallet → issues KYC, age, and funds credentials in mock mode (no real KYC provider) → generates UltraHonk proofs locally → submits them to ProofRegistry → prints the verified claims.
Prerequisites:
stellarCLI,node,bb(Barretenberg),pnpm installin frontend/, and deployed contracts (run./scripts/deploy.shonce).The script is idempotent — safe to re-run. Fails clearly if anything is missing.
One-time toolchain (macOS):
brew install stellar-cli # need v26+; verified on 27.0.0
rustup target add wasm32v1-none
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
noirup -v 1.0.0-beta.9 # nargo
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/next/barretenberg/bbup/install | bash
bbup -v 0.87.0 # bbDeploy and wire the contracts, then point the frontend at them:
# 1. Create and fund a deployer identity (friendbot)
stellar keys generate --global deployer --network testnet --fund
# 2. Build, deploy all 4 contracts, wire them, and register the VKs.
# Set ISSUER_PRIVATE_KEY so the registered issuer key matches the one the
# app signs with (a 64-char hex secp256k1 key).
ISSUER_PRIVATE_KEY=<hex> SOURCE=deployer ./scripts/deploy.sh
# 3. Configure the frontend
cp frontend/.env.example frontend/.env.local
# Paste the printed NEXT_PUBLIC_* contract ids, set NEXT_PUBLIC_ISSUER_ADDRESS,
# and set ISSUER_PRIVATE_KEY (server-only). Persona vars are optional.
# 4. (optional) regenerate circuit artifacts + VKs
./circuits/scripts/build.sh
# 5. Run the app
cd frontend && pnpm install && pnpm devIn the browser: install a Stellar wallet (Freighter, Albedo, xBull, and others via Stellar Wallets Kit are supported), switch it to testnet, and fund the account (https://lab.stellar.org or friendbot).
- Verify — pick one or more claims; the app issues the credentials to your wallet (saved locally, never server-side).
- Holder — connect the wallet → "Generate proof" (proves locally, ~seconds) → submit to Stellar (signs + submits).
- Apps — three demo protocols (LendFi, FundVault, AgeGate) each gated on a
different claim type; watch access denied → granted as
is_verifiedflips.
Rotating the issuer key doesn't require a redeploy — generate a new key and
call register_issuer on the existing IssuerRegistry with the new public key.
In-browser proving uses cross-origin isolation (COOP/COEP headers in
next.config.mjs) for multithreading, falling back to single-threaded.
Deploy and wire the contracts on the Stellar Mainnet:
-
Prepare a funded mainnet identity: Import your funded mainnet deployment account into the Stellar CLI:
stellar keys import deployer --private-key <your-secret-key>
-
Deploy, wire, and register VKs: Run the mainnet deployment script (providing your private issuer key in the environment for registration):
ISSUER_PRIVATE_KEY=<hex> SOURCE=deployer ./scripts/deploy-mainnet.sh
-
Configure the frontend: Copy the printed environment variables to your production environment configuration (e.g.,
frontend/.env.localfor local production builds).
- ZK verification is real, on soroban-sdk 26 with host-native BN254
(
soroban_sdk::crypto::bn254) — on-chain verification fits the resource budget (~0.014 XLM/verify on testnet). See BENCHMARKS.md for the full per-function instruction budget and fee breakdown. - All public functions benchmarked.
submit_proofuses ~13.5M instructions (~13.5% of the 100M per-transaction budget), confirming the protocol fits comfortably within Soroban's limits. Read-only functions (is_verified,check_claim) use <400K instructions (<0.4%). See BENCHMARKS.md. - 21 contract tests pass, including real proof verification for all credential types, in-circuit ECDSA, untrusted-issuer and wrong-issuer-key rejections, and a proof-expiry test that advances ledger time.
- Toolchain is pinned: Noir
1.0.0-beta.9, Barretenbergbb 0.87.0, matching the verifier crate; the VK is deterministic from the circuit. - Server-side issuance, multi-claim flow, the return-URL redirect, the
@stellarcred/sdk, and the Persona relay are wired and build-clean.
- ZK on Stellar — https://developers.stellar.org/docs/build/apps/zk
- UltraHonk Soroban verifier — https://github.com/yugocabrio/rs-soroban-ultrahonk
- Noir — https://noir-lang.org/docs
- Stellar Wallets Kit — https://stellarwalletskit.dev
Contributions are welcome. See CONTRIBUTING.md for setup instructions, development workflow, and areas open for contribution.
MIT © 2026 Samuel Dahunsi