🎵 Music & Sound Design · music samples

Sample Usage Approval Check

Sample Usage Approval Check proves a bound holds for music samples — the verifier sees the verdict, never the number.

Private Predicate· assert a bound, reveal a verdict
Section · Noir

The primitive.

full primer →

Musicians enter the sensitive number once, on their own device, and the app proves it clears the bar for music samples — the gate turns green on the verdict alone, and the number is never sent anywhere.

Why this primitiveA private predicate fits music samples in Music & Sound Design because the gate only ever needed a yes or no — old enough, qualified enough, paid enough — and copying the underlying number into another database is pure liability.

Primitive
a Noir circuit takes the sensitive value as a private input and the bound as a `pub` input, then states the claim as an assertion — `fn main(dob_year: u32, threshold: pub u32) { assert(dob_year <= threshold) }`. A proof exists only when the assertion holds, so the verifier learns the verdict and nothing else: no age, no salary, no score, no location
Drives the UI as
a single gate that answers one yes/no question — old enough, rich enough, close enough, qualified enough — while the underlying number never leaves the holder's device
Appendix · Build mode

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.

Appendix · Secrets

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.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. The prompt below is written for the Browser prover mode. read the build strategy →

Build "Sample Usage Approval Check" as a ONE-SHOT Lovable build.
Single-page TanStack Start app with a real Noir zero-knowledge proof. Cut scope ruthlessly.

CONCEPT
Prove that sample usage is approved for a track without sharing the sample's identity with listeners.
Discipline: Music & Sound Design (music samples).
Recipe: Private Predicate (assert a bound, reveal a verdict) as the single Noir primitive.
Why zero-knowledge: A private predicate fits music samples in Music & Sound Design because the gate only ever needed a yes or no — old enough, qualified enough, paid enough — and copying the underlying number into another database is pure liability.

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 — Private predicate / range check
```rust
// src/main.nr
fn main(secret_value: u32, threshold: pub u32) -> pub bool {
    assert(secret_value >= threshold);
    true
}
```
`secret_value` never leaves the browser; `threshold` is public so the verifier
knows which bound was checked. A proof exists only when the assertion holds, so
its mere existence IS the verdict. Compare with the naive version you are
replacing: a form that uploads the number and trusts the server to forget it.

USER FLOW (build exactly this, nothing more)
1. The user enters the private value and picks the public bound.
2. Press "Prove" -> compile, witness, prove, verify, all in the browser.
3. Show a large PASS gate, the public inputs, and the truncated proof bytes.
4. A "tamper" button flips one proof byte and shows verification failing.

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 $40B — global film and television production market · SAM $3B — trailer house and promotional audio industry · SOM $50M — AI trailer sound design tools

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.
Appendix · Market

Market sizing.

TAM
$40B
global film and television production market
SAM
$3B
trailer house and promotional audio industry
SOM
$50M
AI trailer sound design tools

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.