Streaming Accuracy
Streaming Accuracy binds a proof to a hidden artefact in content creation with a Pedersen commitment anyone can recognise but nobody can open.
The primitive.
Game designers hash the artefact behind content creation and the proof carries only its commitment — a public handle anyone can recognise later, that nobody can open to see what it stands for.
Why this primitiveCommitment binding suits content creation in Game Design & Interactive Media because the same hidden artefact — a take, a contract, a master file — has to be referenced again and again, and a deterministic commitment gives it a public handle without publishing its contents.
Pick how the proof is generated.
Start here. It is the only mode that reliably fits a one-shot, 5-credit Lovable build, needs no accounts or secrets, and it is the most convincing demo: the judge watches their own machine produce the proof.
No keys required.
The browser prover runs entirely inside the app — Noir compiles and Barretenberg proves client-side. Paste the prompt and build; nothing to configure.
Switch to a native or on-chain mode above when the proof has to be verified outside the browser.
The build prompt.
Paste into a fresh Lovable project. The prompt below is written for the Browser prover mode. read the build strategy →
Build "Streaming Accuracy" as a ONE-SHOT Lovable build.
Single-page TanStack Start app with a real Noir zero-knowledge proof. Cut scope ruthlessly.
CONCEPT
Streamers can prove their viewer counts for sponsorships without sharing their entire streaming history.
Discipline: Game Design & Interactive Media (content creation).
Recipe: Commitment Binding (hash a secret, publish the handle) as the single Noir primitive.
Why zero-knowledge: Commitment binding suits content creation in Game Design & Interactive Media because the same hidden artefact — a take, a contract, a master file — has to be referenced again and again, and a deterministic commitment gives it a public handle without publishing its contents.
LOVABLE BUDGET (HARD CAP: ONE-SHOT, ~5 CREDITS TOTAL):
This prompt MUST ship a working demo on the FIRST message with zero follow-ups.
- ONE TanStack Start app, ONE route (`src/routes/index.tsx`). No auth, no nav.
- ONE circuit module `src/lib/circuit.ts` holding the Noir source as a string.
- ONE client-only prover component wired to a form + result panel.
- NO database, NO Lovable Cloud, NO server functions, NO file uploads.
The whole proof happens in the browser; there are no secrets to hide.
- Libraries: template defaults + the three proving packages below. Nothing else.
- If a feature is not in the user flow below, do not build it.
NOIR PRIMER (assume the reader has never written a circuit)
- Noir is a Rust-like DSL for zero-knowledge circuits. You write assertions over
private inputs; the prover produces a succinct proof that they hold; a verifier
checks the proof without seeing the inputs. Docs: https://noir-lang.org/docs/
- Function parameters are PRIVATE by default. Mark a parameter `pub` to make it
public. The return value is ALWAYS public.
- `assert(...)` is the security statement. Anything your UI claims must map to an
assert or to a value the circuit returns.
- Proving here uses Barretenberg's UltraHonk backend (`@aztec/bb.js`), the same
backend `bb` uses on the command line.
- Fields are BN254 (~254 bits). A 256-bit hash does NOT fit in one Field — split
it into two 128-bit limbs.
- Nothing is uploaded and nothing is stored: the private input never leaves the
page, so this build needs no backend at all.
RUNTIME CONVENTIONS (this template — follow exactly)
- Stack: TanStack Start v1 + React 19 + Vite 7 + Tailwind v4 (`src/styles.css`).
There is NO `src/App.tsx`, no react-router-dom, no `src/pages`.
- Routes live in `src/routes`; the home page is `src/routes/index.tsx` and uses
`createFileRoute("/")({ component: Page, head: () => ({ meta: [...] }) })`.
Never edit `src/routeTree.gen.ts`.
- SSR runs every module import. The prover must live in a CLIENT-ONLY entry:
`<ClientOnly>` + `React.lazy(() => import("@/components/prover-entry"))`, and the
three proving packages must be imported INSIDE the handler with `await import(...)`,
never at module scope. `crypto.subtle`, `PerformanceObserver` and WebAssembly
streaming do not exist during SSR.
- Never name an SSR-reachable module `*.client.*` — the SSR build rejects the
pattern even behind a dynamic import.
- TypeScript runs with `exactOptionalPropertyTypes`: pass optional props as
`...(x ? { prop: x } : {})`, not `prop: x ?? undefined`.
- Toasts: `sonner` (`import { toast } from "sonner"`), and render `<Toaster />`
once in `src/routes/__root.tsx`. `@/hooks/use-toast` does NOT exist here.
- Colours come from semantic tokens in `src/styles.css` — no hardcoded
`text-white` / `bg-black` / `bg-[#hex]` in components.
- Give `src/routes/index.tsx` its own `head()` with a real title and description.
PACKAGES AND VITE CONFIG (copy this exactly — it is the whole trap)
Install:
@noir-lang/noir_wasm@1.0.0-beta.26 # Noir source -> ACIR
@noir-lang/noir_js@1.0.0-beta.26 # witness generation
@aztec/bb.js@5.1.0 # UltraHonk prove/verify
vite.config.ts:
```ts
import { defineConfig } from "vite";
import path from "node:path";
import fs from "node:fs";
/** Serve noir_wasm's prebuilt browser bundle verbatim; the bundler must not touch it. */
function noirWasmVendorAsset() {
const file = path.resolve("node_modules/@noir-lang/noir_wasm/web/main.mjs");
const url = "/vendor/noir_wasm/main.mjs";
return {
name: "noir-wasm-vendor-asset",
configureServer(server) {
server.middlewares.use(url, (_req, res) => {
res.setHeader("content-type", "text/javascript");
res.end(fs.readFileSync(file));
});
},
generateBundle() {
this.emitFile({
type: "asset",
fileName: "vendor/noir_wasm/main.mjs",
source: fs.readFileSync(file),
});
},
};
}
export default defineConfig({
plugins: [noirWasmVendorAsset() /* , ...template plugins */],
optimizeDeps: {
exclude: ["@noir-lang/noir_wasm", "@noir-lang/noir_js", "@aztec/bb.js"],
},
});
```
Load the compiler through that URL so nothing rewrites it:
```ts
const noirWasm = await import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs") as {
compile: (fm: unknown) => Promise<unknown>;
createFileManager: (root: string) => { writeFile: (p: string, s: ReadableStream) => Promise<void> };
};
```
MODE — BROWSER PROVER (compile and prove client-side, no secrets, no backend)
Everything runs in the visitor's browser. Nothing is uploaded; there is no server
function, no database and no API key anywhere in this build.
PACKAGES (pin these exact versions together — a skew breaks witness generation)
@noir-lang/noir_wasm 1.0.0-beta.26 # Noir source -> ACIR
@noir-lang/noir_js 1.0.0-beta.26 # witness generation
@aztec/bb.js 5.1.0 # Barretenberg UltraHonk prove/verify
VITE CONFIG — BOTH RULES ARE MANDATORY
1) Never let the dep optimizer pre-bundle the proving stack:
optimizeDeps: { exclude: ["@noir-lang/noir_wasm", "@noir-lang/noir_js", "@aztec/bb.js"] }
esbuild rewrites the wasm-bindgen glue; the dynamic import still resolves, then
wasm instantiation HANGS WITH NO ERROR. A step that never completes and never
throws is always this.
2) Serve noir_wasm's published browser bundle verbatim. Rolldown re-processing it
emits `const Infinity = Infinity` inside its vendored @ltd/j-toml, so importing
the compiler throws "Cannot access 'Infinity' before initialization" (minified:
"Cannot access 'j' before initialization"). PRODUCTION ONLY — preview looks fine
and the published site dies. Add a small Vite plugin that exposes
node_modules/@noir-lang/noir_wasm/web/main.mjs at /vendor/noir_wasm/main.mjs
(dev: server.middlewares; build: this.emitFile in generateBundle, client env
only) and load it with:
import(/* @vite-ignore */ "/vendor/noir_wasm/main.mjs")
SSR
The compiler, crypto.subtle, PerformanceObserver and WebAssembly streaming are all
browser-only. Put the prover in a client-only entry: <ClientOnly> + React.lazy, and
import the three packages INSIDE the handler, never at module scope.
COMPILE IN THE BROWSER
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; // beta drift
Nargo.toml needs [package] name/type = "bin" and an empty [dependencies].
PROVE AND VERIFY (single-threaded — no COOP/COEP headers needed)
const api = await bb.Barretenberg.new({ threads: 1 });
const backend = new bb.UltraHonkBackend(program.bytecode, api);
const { witness } = await noir.execute(inputs);
const proof = await backend.generateProof(witness);
const ok = await backend.verifyProof(proof);
threads > 1 needs SharedArrayBuffer and cross-origin isolation, which breaks
third-party embeds. Stay single-threaded.
UX BUDGET (mid-range mobile) — wrap EVERY phase in its own timeout with a
phase-specific message:
load 90s (tens of MB, then HTTP-cached) · compile seconds · witness 30s ·
prove 180s (dominates) · verify 60s
The libraries fetch their wasm internally, so fetch progress is unavailable: use a
PerformanceObserver on "resource" entries filtered to .wasm/.mjs and report
transferSize. Cache { program, noir, backend } in a ref; clear it on any failure so
Retry re-downloads instead of reusing a half-initialised backend. Offer "Retry" and
"Reload the prover", and say plainly that ad-blockers, offline mode and strict
proxies can block wasm.
HONESTY REQUIREMENT
Show the circuit source verbatim and include a tamper button that flips one byte of
proof.proof and re-verifies — verifyProof must return false. State what the circuit
does NOT prove (e.g. it does not check an issuer signature unless the circuit does).
MODE GOTCHAS
Both Vite rules are mandatory — a missing optimizeDeps.exclude hangs with no error, and a re-bundled noir_wasm breaks only in production. Keep noir_wasm and noir_js on the same beta. Never import the proving packages at module scope. Bound every phase with its own timeout; proving with one thread can exceed a minute on mobile.
PROVING PIPELINE (client-only component, inside the handler)
```ts
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");
// 1. compile
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;
// 2. backend (single-threaded — works on any origin)
const api = await bb.Barretenberg.new({ threads: 1 });
const backend = new bb.UltraHonkBackend(program.bytecode, api);
// 3. witness -> proof -> verify
const noir = new Noir(program);
const { witness } = await noir.execute(inputs);
const proof = await backend.generateProof(witness); // { proof, publicInputs }
const ok = await backend.verifyProof(proof); // true
// 4. tamper check — the only convincing evidence the proof is real
const bad = { ...proof, proof: (() => { const c = proof.proof.slice(); c[0] ^= 1; return c; })() };
const shouldBeFalse = await backend.verifyProof(bad);
```
NARGO_TOML = `[package]\nname = "app"\ntype = "bin"\n\n[dependencies]\n`
CIRCUIT — Commitment binding (hash the secret, publish the handle)
```rust
// src/main.nr
fn main(secret_lo: Field, secret_hi: Field, threshold: pub u32, value: u32) -> pub Field {
assert(value <= threshold);
std::hash::pedersen_hash([secret_lo, secret_hi])
}
```
Build the two limbs in TypeScript from a SHA-256 digest of the document — a
256-bit hash does not fit in one BN254 Field:
```ts
async function binding(text: string): Promise<{ lo: string; hi: string }> {
const d = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
const hex = [...d].map((b) => b.toString(16).padStart(2, "0")).join("");
return { lo: "0x" + hex.slice(0, 32), hi: "0x" + hex.slice(32, 64) };
}
```
Pedersen is deterministic, so the same document always yields the same public
commitment: two proofs can be linked to one hidden source without revealing it.
USER FLOW (build exactly this, nothing more)
1. The user pastes or types the artefact (text is fine) plus the private value.
2. Press "Prove" -> the circuit asserts the bound and returns the commitment.
3. Show the public commitment as the headline result, with a copy button.
4. Prove twice: identical artefact -> identical commitment; changed artefact -> different one.
DESIGN
Editorial, high-contrast, generous whitespace. One accent colour used sparingly.
Show the cryptography honestly: render the circuit source verbatim, the per-phase
timings, the public inputs and the raw proof in a collapsible panel so a judge can
see there is no trick. Truncate proofs, hashes and commitments in prose
(first 12 + last 6) with the full value behind a copy button. Proving can take a
minute on a phone — show phase-by-phase progress, never a bare spinner.
Mobile first — a judge will open this on a phone.
MARKET (for the pitch slide, not the UI)
TAM $8B — global game development software market · SAM $1B — interactive fiction and text adventure engines · SOM $25M — AI prose pacing directors
GOTCHAS (universal — every one of these has burned a build)
- optimizeDeps.exclude is MANDATORY for all three proving packages. If esbuild
pre-bundles them it rewrites the wasm-bindgen glue and wasm instantiation HANGS
WITH NO ERROR. A step that never finishes and never throws is always this.
- noir_wasm's published browser bundle must be served VERBATIM. Re-bundling its
vendored @ltd/j-toml emits `const Infinity = Infinity`, so importing the compiler
throws "Cannot access 'Infinity' before initialization" (minified: "Cannot access
'j' before initialization"). It breaks the PRODUCTION build only — preview looks
fine and the published site dies.
- Pin @noir-lang/noir_wasm and @noir-lang/noir_js to the SAME beta, and @aztec/bb.js
to a version that speaks their ACIR. A skew shows up as an opaque
witness-generation failure on inputs you know are valid.
- `Barretenberg.new({ threads: 1 })`. More threads need SharedArrayBuffer, which
needs COOP/COEP headers, which break third-party embeds.
- `compile()` returns `{ program }` on some betas and the program directly on
others. Normalise: `const program = "program" in compiled ? compiled.program : compiled`.
- Nargo.toml is required in the virtual file manager: `[package] name/type = "bin"`
plus an empty `[dependencies]`. Omitting it fails with a path error.
- Wrap EVERY phase in its own timeout with a phase-specific message
(load 90s · compile 30s · witness 30s · prove 180s · verify 60s). An unbounded
await is indistinguishable from the silent optimizer hang above.
- The libraries fetch their wasm internally, so `fetch` progress is unavailable.
Use a PerformanceObserver on "resource" entries filtered to .wasm/.mjs and report
`transferSize` if you want real byte counts.
- Cache `{ program, noir, backend }` in a ref and CLEAR it on failure, so Retry
re-downloads instead of reusing a half-initialised backend. Say plainly that
ad-blockers, offline mode and strict proxies can block wasm.
- Never print a raw proof, hash or commitment inline in prose: truncate
(first 12 + last 6) with the full value behind a copy button or a JSON panel.
- Be honest about the trust boundary. Hashing a value and asserting a range proves
knowledge of a preimage plus the range — it does NOT prove who issued the value
unless the circuit verifies a signature. Say so in the UI.
REFERENCE MATERIAL (if you need more than the above)
- Noir docs: https://noir-lang.org/docs/
- Full machine-readable Noir brief: https://noir-lang.org/docs/llms-full.txt
- Noir repo: https://github.com/noir-lang/noir
- Barretenberg / bb.js: https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg
- Awesome Noir (examples, tooling): https://github.com/noir-lang/awesome-noir
- This catalog as one text file: https://identusprompts.lovable.app/llms-full.txt
DELIVERABLE
A working single-page demo where a real proof is generated and verified in the
browser end-to-end, plus a one-paragraph README naming the Noir primitive used,
what the circuit asserts, and what it deliberately does NOT prove.
Built for the Noir zero-knowledge catalyst — organised by StreetKode Fam during Indian Krump Festival 14.Market sizing.
Indicative figures for hackathon pitches — refine with your own research before raising.