Real proofs, one prompt, one build.
Every entry in this catalog compiles down to the same shape: a small Noir circuit, a client-only prover, and one product surface on top. It's the only pattern that ships a working zero-knowledge demo in one shot, inside the 5-credit budget.
Why a circuit and not an API check?
An API check means handing over the data and trusting whoever holds it. A circuit proves the statement and nothing else: the private inputs stay in the browser, the verifier gets a proof plus a public commitment. Nothing about it is a token, a coin, or a speculation.
Three modes, one circuit
Start in the browser — compile Noir at runtime, prove with UltraHonk, zero setup. Ship a precompiled ACIR artifact when you want a faster first load, or verify natively and on-chain when the proof has to travel. The circuit never has to change.
// src/components/zk/zk-circuit.ts — the whole security statement
// Built for the Noir Catalyst — StreetKode Fam · Indian Krump Festival 14
export const NARGO_TOML = `[package]
name = "catalyst"
type = "bin"
[dependencies]`;
export const CIRCUIT_SOURCE = `
fn main(
dob_year: u32,
binding_lo: Field,
binding_hi: Field,
threshold_year: pub u32,
) -> pub Field {
// private predicate: the year never becomes public
assert(dob_year <= threshold_year);
// commitment binding: links the proof to one secret source
std::hash::pedersen_hash([binding_lo, binding_hi])
}
`;// vite.config.ts — the two traps that decide whether this ships
export default defineConfig({
optimizeDeps: {
// esbuild rewrites the wasm-bindgen glue: proving then HANGS with no error
exclude: ["@noir-lang/noir_wasm", "@noir-lang/noir_js", "@aztec/bb.js"],
},
plugins: [
// Serve node_modules/@noir-lang/noir_wasm/web/main.mjs VERBATIM at a stable
// URL and import it with import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs").
// Re-bundling it emits `const Infinity = Infinity` and the production build
// dies with "Cannot access 'j' before initialization" — preview looks fine.
serveNoirWasm(),
],
});// client-only entry — nothing here may run during SSR
const { compile, createFileManager } = await import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs");
const { Noir } = await import("@noir-lang/noir_js");
const bb = await import("@aztec/bb.js");
const fm = createFileManager("/");
await fm.writeFile("./src/main.nr", new Blob([CIRCUIT_SOURCE]).stream());
await fm.writeFile("./Nargo.toml", new Blob([NARGO_TOML]).stream());
const compiled = await compile(fm);
const program = "program" in compiled ? compiled.program : compiled;
const api = await bb.Barretenberg.new({ threads: 1 }); // >1 needs COOP/COEP
const backend = new bb.UltraHonkBackend(program.bytecode, api);
const noir = new Noir(program);
const { witness } = await noir.execute({
dob_year: 1994, binding_lo, binding_hi, threshold_year: 2007,
});
const proof = await backend.generateProof(witness);
const ok = await backend.verifyProof(proof); // true
// the only convincing evidence: flip a byte, expect false
const bad = { ...proof, proof: tamper(proof.proof) };
const shouldBeFalse = await backend.verifyProof(bad);// src/lib/binding.ts — SHA-256 does not fit one Field (BN254 ≈ 254 bits)
export async function credentialBinding(secret: string) {
const digest = new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)),
);
const limb = (bytes: Uint8Array) =>
"0x" + [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
return {
binding_lo: limb(digest.slice(0, 16)), // two 128-bit limbs
binding_hi: limb(digest.slice(16, 32)),
};
}# Browser prover — no secrets at all. Start here. # Noir + Barretenberg run client-side; the secret never leaves the page. # Precompiled circuit — ship the ACIR artifact, skip the compiler download: # nargo compile -> target/catalyst.json -> import as a static asset # Native + on-chain — verify the proof outside the browser: ZK_RPC_URL=https://<chain-rpc> ZK_VERIFIER_ADDRESS=0x<deployed-verifier> # Whatever the mode, the mega-prompt then: # - keeps every private input inside the circuit # - reports per-phase progress with per-phase timeouts # - proves the proof is real by failing a tampered one
Rules of the build.
- · Pick one idea, pick one mode, paste its mega-prompt. That's the whole build.
- · Add all three ZK packages to
optimizeDeps.exclude— otherwise proving hangs silently. - · Keep the prover in a client-only entry;
crypto.subtleand WebAssembly need a browser. - · Pin
noir_wasmandnoir_jsto the same beta; the ACIR format shifts between them. - · Use
threads: 1unless you can afford COOP/COEP isolation. - · Split any 256-bit digest into two 128-bit
Fieldlimbs. - · Wrap every phase in its own timeout — an unbounded await looks identical to a hang.
- · Show the tamper check and the timings — judges want evidence the proof is real.
- · Say plainly what the circuit does not prove: issuer signatures verified outside stay outside.
- · Add the footer credit: "Built for the Noir Catalyst — StreetKode Fam · Indian Krump Festival 14".