omar espejel

Private Transfers and ZK Credentials on Starknet

Time to complete: 30 minutes (Codespaces) or 1 hour (local setup)

TLDR. Privacy on Starknet is finally practical. Tongo gives you encrypted balances. Garaga + Noir lets you prove arbitrary facts about private data in ~10 lines of circuit code. The infrastructure handles the hard parts. Fork this repo and build what's missing.


Repo here: https://github.com/starknet-edu/starknet-privacy-toolkit

Blockchain privacy is broken. Every transfer, every balance — permanently visible. This is great for trustlessness but catastrophic for real world use. What follows is a working toolkit that changes this: private transfers with selective disclosure.

I built it as a donation badge system. But the pattern is general. Age verification, credit scores, token holdings, salary proofs. Same infrastructure, different constraints.

Code and live app

  1. Repo
  2. Live app
  3. Web UI code
  4. Proof API server
  5. Tongo client
  6. Quickstart template
  7. Minimal snippet
  8. Noir circuit
  9. Cairo badge contract

If this helps you, please star the repo.

Gemini_Generated_Image_mcq002mcq002mcq0

Deployed contracts

  1. Verifier: 0x022b20...2669 on Sepolia
  2. Badge: 0x077ca6...7010 on Sepolia
  3. Tongo mainnet: 0x00b921...0a16 USDC
  4. Tongo Sepolia: 0x00f34d...5f8a STRK

The fastest way to use this

gh repo fork starknet-edu/starknet-privacy-toolkit
git clone https://github.com/starknet-edu/starknet-privacy-toolkit.git
cursor starknet-privacy-toolkit

Then tell the AI what you want: "Change this donation badge to verify credit scores above 700" or "Make this prove someone is over 18 without revealing their birthdate."

The circuit is 10 lines. The contract is 30 lines. Everything else is plumbing. You modify the small parts, the plumbing stays the same.

1. The privacy problem

Every transfer, every balance, every interaction. Permanently public. This is great for trustlessness but terrible for privacy. You can't donate to politically sensitive causes without that being traceable. You can't pay for sensitive services privately. Your entire financial life is an open book.

Zero knowledge proofs fix this. As Eli Ben-Sasson (StarkWare cofounder) puts it: "ZK has two super powers: Privacy and Scalability." The elegant part: you can prove statements about data without revealing the data itself. "I donated at least $100" is provable without showing that you actually donated $847.

Two pieces make this work:

  1. Tongo handles private transfers. You deposit tokens, and they become encrypted. You transfer them, amounts stay hidden. Only you and the recipient know.

  2. ZK Credentials let you prove things about your private data. Built with Noir (circuit language), Barretenberg (proof generator), and Garaga (Starknet verifier).

2. What you can build

The donation badge is just one example. The same pattern works for anything where you want to prove X without revealing Y:

  1. Donation badge: prove you gave above a threshold, hide the exact amount.
  2. Credit score: prove score above 700, hide the actual score.
  3. Age verification: prove over 18, hide birth date.
  4. Accredited investor: prove net worth above 1M, hide actual wealth.
  5. Token holder: prove you hold more than 100 tokens, hide exact balance.
  6. Salary proof: prove you earn above 50k per year, hide actual salary.

For the donation badge example, I set up three tiers:

  1. Bronze: ten dollars
  2. Silver: one hundred dollars
  3. Gold: one thousand dollars

But again, this is just one instantiation. The infrastructure is what matters.

3. How ZK proofs actually work

Let me break this down because it's actually pretty elegant once you see it.

Step 1: commit

You take your private data and hash it with a random secret:

commitment = Poseidon(private_data, secret)

Think of this like sealing a letter in an envelope. Everyone can see the envelope exists, but nobody can read the contents.

This commitment goes on chain. It's public. But here's the thing: hash functions are one way. Nobody can reverse engineer your data from the commitment. They can verify the commitment later if you reveal the inputs, but they can't figure out the inputs just from seeing the commitment.

Step 2: prove

Now comes the clever part. You run a prover locally. This thing takes about 30 to 60 seconds and produces about eight KB of data. This proof says:

  1. I know values that hash to that commitment.
  2. Those values satisfy a constraint, for example amount above a threshold.

The beautiful thing: the proof reveals nothing about the actual values. Not even a hint. It's not encrypted data that could theoretically be decrypted. It's a completely different object that proves knowledge without conveying it.

If commitments are sealed envelopes, proofs are like showing a bouncer a wristband. You can prove you are allowed in without revealing your ID.

Step 3: verify

A smart contract checks the proof. If it's valid, you get your credential. The contract learns that the statement is true. It never learns the underlying data.

This is the core loop. Everything else is implementation details.

4. The ZK pipeline

diagram-zk-pipeline

Three tools, each doing one thing well. Noir compiles constraints into arithmetic circuits. Barretenberg takes the circuit plus private inputs and produces a proof. Garaga converts that proof into Starknet calldata. As the Starknet blog explains: "Using Garaga, Noir developers can compile their programs to automatically generate a Cairo verifier, deploy it on Starknet, and verify proofs without writing any Cairo code."

OpenZeppelin puts it crisply:

"Noir abstracts low level cryptographic complexities, allowing developers to focus on logic rather than circuit optimization. Unlike earlier frameworks like Circom or ZoKrates, Noir leverages Rust like syntax and tooling to reduce boilerplate code and human error."
OpenZeppelin, A Developer’s Guide to Building Safe Noir Circuits

One gotcha: versions matter a lot. These tools are tightly coupled. Use exactly these versions or proofs will fail:

  1. Noir: 1.0.0 beta 1
  2. Barretenberg: 0.67.0
  3. Garaga: 0.15.5

This isn't a bug. It's cryptography. The verifier is compiled for a specific proof format. Different versions = different formats = verification fails.

5. Setup

Why Codespaces:

  1. Noir, BB, and Garaga are sensitive to OS versions and system libraries.
  2. Codespaces gives a clean Linux environment that matches the toolchain.
  3. Fewer local setup surprises means faster onboarding for hackers.

Step by step:

  1. Create a GitHub Codespace on the repo. codespace

  2. Install the toolchain (setup-codespace.sh):

    chmod +x setup-codespace.sh
    ./setup-codespace.sh
    

codespaces-install

  1. Start the proof API:

    source garaga-env/bin/activate
    bun run api
    

    You should see: Proof API running on http://localhost:3001

  2. Expose the API port:

    1. In Codespaces, make port 3001 Public so the web UI can call it.
  3. (Optional) Start the web UI in another terminal:

    bun run dev:web
    

    Open the 5173 port URL in your browser.

That's it. Everything is installed. If the script fails, use the Manual installation steps below.

If the UI shows "Proof Server: Offline":

  1. Make sure port 3001 is Public in Codespaces.
  2. Paste your Codespaces URL (e.g. https://your-codespace-3001.app.github.dev/) into the Proof API URL field in the UI and click Save.

What to do next in 5 minutes

  1. Start the API: run source garaga-env/bin/activate then bun run api
  2. Start the UI: bun run dev:web and open the 5173 URL.
  3. In Step 6, paste your Codespaces URL, click Save, then generate a proof with the defaults (amount 150, secret 12345678).

proof

Other useful scripts: bun run check:health (verify setup), bun run template:quickstart (run minimal example), bun run tongo:init (initialize Tongo client).

Manual installation

If you want to run locally:

# Noir
curl -L https://noirup.dev | bash
source ~/.bashrc
noirup --version 1.0.0-beta.1

# Barretenberg
curl -L https://bbup.dev | bash
source ~/.bashrc
bbup --version 0.67.0
sudo apt-get install -y libc++-dev libc++abi-dev

# Garaga (needs Python 3.10 specifically)
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt-get install -y python3.10 python3.10-venv python3.10-dev
python3.10 -m venv garaga-env
source garaga-env/bin/activate
pip install garaga==0.15.5

# Bun
curl -fsSL https://bun.sh/install | bash

If noirup.dev or bbup.dev fails, use these mirrors instead:

curl -fsSL https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
curl -fsSL https://raw.githubusercontent.com/AztecProtocol/aztec-packages/master/barretenberg/bbup/install | bash

Verify your versions:

nargo --version  # should say 1.0.0-beta.1
bb --version     # should say 0.67.0

6. The circuit

Here's the entire circuit for the donation badge:

use std::hash::poseidon::bn254::hash_2;

fn main(
    threshold: pub u64,
    commitment: pub Field,
    donation_amount: u64,
    donor_secret: Field
) {
    let computed = hash_2([donation_amount as Field, donor_secret]);
    assert(computed == commitment);
    assert(donation_amount >= threshold);
}

That's it. 10 lines.

The pub keyword means "public input". These values are visible to the verifier. threshold and commitment are public. donation_amount and donor_secret are private. They're used to generate the proof but never revealed.

Two assertions:

  1. The hash of (amount, secret) must equal the commitment
  2. The amount must be at least the threshold

If both pass, you get a valid proof. If either fails, no proof.

This is what you modify for different use cases. Want age verification? Change donation_amount >= threshold to birth_year <= current_year - 18. Want credit score checks? Change it to credit_score >= 700. The pipeline stays the same.

Testing the circuit

cd zk-badges/donation_badge
nargo test

Compiling the circuit

The circuit config is in Nargo.toml:

nargo compile

Output: target/donation_badge.json


7. Generating a proof

Let's walk through the actual commands.

1. Compute your commitment

Use computecommitment.js:

cd zk-badges
node computecommitment.js 15000 "mysecret123"
# Output: 0x1abc...def (your commitment)

2. Create prover.toml

Replace the placeholder values with your actual commitment from step 1:

threshold = "1000"
commitment = "0x..."  # paste your commitment from step 1
donation_amount = "15000"
donor_secret = "0x..."  # paste the hex of your secret

3. Generate the proof

nargo execute witness

bb prove_ultra_keccak_honk \
  -b ./target/donation_badge.json \
  -w ./target/witness.gz \
  -o ./target/proof

bb write_vk_ultra_keccak_honk \
  -b ./target/donation_badge.json \
  -o ./target/vk

This takes 30 to 60 seconds depending on your hardware. It's doing real cryptography, generating a proof that passes certain mathematical properties without revealing the inputs.

4. Convert to calldata

garaga calldata \
  --system ultra_keccak_honk \
  --vk ./target/vk \
  --proof ./target/proof \
  --format starkli \
  > ../calldata.txt

Or just use generate-proof.sh

./generate-proof.sh --amount 15000 --threshold 1000 --donor-secret "mysecret123" --tier 1

8. The smart contract

Garaga generates the verifier contract automatically from your circuit. You can see the generated honk_verifier.cairo in the repo. You write the application logic around it.

Here's the badge contract:

#[starknet::contract]
mod DonationBadge {
    use starknet::ContractAddress;
    use starknet::get_caller_address;

    #[storage]
    struct Storage {
        verifier_address: ContractAddress,
        user_badges: LegacyMap<(ContractAddress, u8), bool>,
        user_max_tier: LegacyMap<ContractAddress, u8>,
        used_commitments: LegacyMap<u256, bool>,
    }

    #[external(v0)]
    fn claim_badge(
        ref self: ContractState,
        full_proof_with_hints: Span<felt252>,
        threshold: u256,
        donation_commitment: u256,
        badge_tier: u8
    ) -> bool {
        let caller = get_caller_address();
        
        // Prevent replay attacks
        assert(!self.used_commitments.read(donation_commitment), 'Used');
        
        // Can only upgrade, not downgrade
        assert(badge_tier > self.user_max_tier.read(caller), 'No upgrade');
        
        // Verify the proof
        let verifier = IUltraKeccakHonkVerifierDispatcher {
            contract_address: self.verifier_address.read()
        };
        assert(verifier.verify_ultra_keccak_honk_proof(full_proof_with_hints), 'Invalid');
        
        // Record the badge
        self.used_commitments.write(donation_commitment, true);
        self.user_badges.write((caller, badge_tier), true);
        self.user_max_tier.write(caller, badge_tier);
        
        true
    }
}

Key things happening here:

  1. Replay protection: each commitment can only be used once.
  2. Upgrade only: you can go Bronze to Silver to Gold, but not backwards.
  3. Proof verification: delegates to the Garaga generated verifier.

Build it with Scarb: scarb build


Security and Audits

  1. Garaga publishes a CryptoExperts audit from June 2025 that covers core cryptographic primitives and verifier logic. Read the security page and audit report.
  2. Barretenberg, the proof backend, has been audited by Veridise. See the Bigfield audit.
  3. Poseidon hash, used for commitments in the circuit, is designed for ZK efficiency. The original paper reports up to 8× fewer constraints per message bit than Pedersen hash in circuit form. Read the paper.

Audits reduce risk, but they don’t eliminate it. Always review versions and understand the cryptographic assumptions you’re leaning on.


9. Claiming your badge

sncast --profile sepolia invoke \
  --contract-address 0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010 \
  --function claim_badge \
  --calldata $(cat calldata.txt) 1000 0xCOMMITMENT 1

Check your badge:

sncast --profile sepolia call \
  --contract-address 0x077ca6f2ee4624e51ed6ea6d5ca292889ca7437a0c887bf0d63f055f42ad7010 \
  --function get_badge_tier \
  --calldata 0xYOUR_ADDRESS

10. Common errors

Run the health check to diagnose issues: bun run scripts/health-check.ts

  1. could not satisfy constraint: your amount is below the threshold. Use a higher amount or a lower threshold.
  2. Invalid proof: version mismatch. Run bb and confirm the output shows 0.67.0.
  3. Commitment already used: this commitment was claimed before. Generate a new commitment with a different secret.
  4. nargo: command not found: Noir is not installed. Run noirup and confirm it installs version 1.0.0 beta 1.
  5. bb: libc++.so.1 not found: missing C++ runtime. Install the libc++ dev packages, then retry.
  6. Garaga: Requires Python <3.11: wrong Python version. Use python3.10 with the venv module.

11. Tongo: the private transfer layer

The badge proves you donated enough. But there's a problem: if the donation itself is a public transfer, your privacy is only half-protected. Everyone still sees the exact amount you sent.

Tongo solves this. It's a private transfer protocol for Starknet. See the full integration in tongo-client.ts and tongo-service.ts.

One detail that matters: Tongo is setup free. The protocol's cryptography relies on the discrete logarithm assumption over the Stark curve and does not require a trusted ceremony or toxic waste. This is a big difference from SNARK systems that depend on a CRS. Read more about Tongo's cryptography.

How it works

Four operations:

  1. Fund. Convert public tokens to encrypted balance. This step is public and people see how much you deposit.
  2. Transfer. Send encrypted amounts. Amount and recipient are hidden from everyone except the recipient.
  3. Rollover. Claim incoming transfers. Your balance increases but the amounts stay hidden.
  4. Withdraw. Convert back to public tokens. This step is also public.

diagram-tongo-architecture

Key concepts

  1. Tongo private key. A 256 bit secret. This is not your wallet key. It is a separate key for Tongo. Lose it and you lose your funds.
  2. Tongo public key. Derived from the private key. Share this to receive transfers.
  3. Encrypted balance. Your balance is stored on chain but encrypted. Only you can read it.
  4. Pending balance. Incoming transfers land here first. You use rollover to claim them.

Generate a key

See tongo-key-manager.ts for the full implementation:

const key = '0x' + crypto.randomBytes(32).toString('hex');

Store this somewhere safe. It's the only way to access your Tongo funds.

Privacy guarantees

This is important to understand:

Fund is public. Everyone sees how much you deposit. This is unavoidable because the tokens have to come from somewhere.

Transfer is private. Amount and recipient are hidden. Only you and the recipient know.

Rollover is private. You're claiming your pending balance. The amount stays hidden.

Withdraw is public. The amount becomes visible again.

The pattern: public → private → private → public. Your privacy window is between fund and withdraw.


12. The web interface

Screenshot 2026-01-18 at 10

If you don't want to use the command line:

  1. Connect wallet (Braavos or ArgentX)
  2. Choose network (Mainnet or Sepolia)
  3. Fund your Tongo balance
  4. Transfer privately
  5. Claim your badge

The proof generation happens on a backend server. Make sure it's running and the UI will show the connection status. The frontend calls the badge-service.ts which coordinates everything. If the badge panel says "Proof Server: Offline", check that port 3001 is public in Codespaces, and paste your Codespaces URL into the Proof API URL field in the UI.

proof


13. Putting it together

Untitled-2025-02-24-1516

The full flow:

  1. Fund: deposit tokens into Tongo (public entry, everyone sees)
  2. Transfer: send to recipient privately (amount hidden)
  3. Prove: generate ZK proof showing amount > threshold
  4. Claim: mint badge on chain

What the world sees: you deposited some amount, you have a badge. What they don't see: the exact amount you transferred, who received it.


14. What's actually protected

Let me be clear about the threat model.

Protected:

  1. Transfer amounts, encrypted on chain.
  2. Exact donation amounts, only the threshold is revealed.
  3. Commitment binding, you cannot reuse proofs.

Not protected:

  1. Wallet addresses, still public.
  2. Transaction timing, visible on chain.
  3. Badge ownership, the badge itself is public.

If you need stronger privacy, use fresh wallets, don't reuse secrets, and add delays between operations.


15. The bigger picture

This toolkit is part of a larger privacy stack being built on Starknet:

  1. Private balances: Tongo, live.
  2. ZK credentials: Garaga, live.
  3. Unlinkable addresses: stealth addresses, coming.
  4. Transaction mixing: MIST.cash, in development.

16. Resources

The tools:

  1. Noir: circuit language
  2. Barretenberg: proof backend
  3. Garaga: Starknet verifiers
  4. Our circuit: the Noir code in this repo

Tongo:

  1. Tongo: protocol docs
  2. Tongo SDK: TypeScript integration
  3. tongo-config.ts: network configuration in this repo

Starknet:

  1. Starknet: the L2
  2. Cairo Book: contract language
  3. Starknet Foundry: dev tools
  4. Voyager: block explorer

Wallets:

  1. Braavos
  2. ArgentX

17. Build your own

This is a template. Fork it. Feed it to your LLM. Build your own thing. Start with the template folder which has minimal examples. The donation badge is one instantiation. You should fork it and build something else.

gh repo fork starknet-edu/starknet-privacy-toolkit
git clone https://github.com/starknet-edu/starknet-privacy-toolkit.git
cursor starknet-privacy-toolkit

Tell the AI:

  1. "Change this to verify users are over 18"
  2. "Adapt the circuit for credit score verification"
  3. "Create a token gated membership proof"
  4. "Build salary verification for loan applications"

The circuit is 10 lines. The contract is 30 lines. The infrastructure handles the hard parts.

Ideas:

  1. Age verification for age restricted content
  2. Credit score attestations for DeFi lending
  3. Accredited investor verification for token sales
  4. Token gated access without revealing holdings
  5. Salary verification for apartment rentals
  6. DAO membership proofs
  7. Private voting systems
  8. Anonymous whistleblower credentials

The primitives are here. Private transfers work. ZK credentials work. Now build something useful with them. Strap in.

Omar Espejel · X · GitHub