What inspired this
I kept running into the same uncomfortable question online: is this real?
Not in a philosophical way — in a practical one. A photo shows up in a group chat or a news feed, and there's genuinely no way to check where it came from. The usual answers are all social: does the account look legit, does it feel off, did someone credible share it. That's a terrible foundation, and it gets worse every month as generation gets better.
Meanwhile the people actually making AI media have the opposite problem. If you generate something honestly and disclose it, you have no way to prove the disclosure is accurate — or that the file someone downloaded is the one you made.
Both problems have the same root: the media carries no history. So I built Veritas to give it one.
What it does
Veritas generates AI images and attaches a signed provenance record to each one: which model made it, from what prompt, when, and every edit since. The record is cryptographically signed, and the asset is fingerprinted with SHA-256.
Anyone can verify it afterwards. The app re-fetches the manifest from storage, re-hashes the actual file, checks the signature against the issuer's key, and validates every link in the edit chain. Change a single byte of the image and verification fails — visibly, in red.
Edits don't overwrite history. Each one appends a new signed record that points back to its parent by ID and by hash. So you can't quietly rewrite an image's past: altering any earlier record orphans everything built on top of it.
How I built it
The generation path runs through Genblaze — a pipeline with a fallback chain across FLUX models on NVIDIA NIM, with the asset landing straight in Backblaze B2 via Genblaze's object storage sink.
Then the provenance layer takes over. The backend reads the bytes back out of B2 (not from a local temp copy — I wanted the hash to cover what B2 actually holds), hashes them, builds a manifest recording the provider, model, prompt and timestamp, signs it with Ed25519, and writes the manifest and a thumbnail back to B2 alongside the asset.
B2 does real work here. Manifests live at veritas/manifests/{chain_id}/{seq}.json — the key ordering is the chain ordering, so the storage layout itself encodes the provenance structure. The verify flow reads from B2 as the source of truth.
Stack: FastAPI backend, React + Vite frontend served from the same service, deployed as a single Docker image.
What I learned
Signing the right bytes is harder than signing. My first instinct was to verify each manifest against the public key embedded inside it. That's circular — anyone could alter an image, re-sign the record with their own keypair, and the result would be perfectly self-consistent. Verification has to check against a trusted issuer key, not the one the document conveniently provides. I wrote a test that forges exactly that attack, and watching it get rejected was the moment the whole thing felt real.
I also learned that JSON isn't a stable thing to sign until you make it one. Key order and whitespace have to be pinned down, or a signature that was valid on the way in fails on the way out.
Challenges
The provider plan collapsed mid-build. I'd designed around one provider, then found its credits never arrived. I pivoted to Google — and discovered Imagen now 404s with "no longer available to new users" on freshly issued keys. So I tried the Gemini image models, which my key could list, and hit a 429 whose fine print read limit: 0 — free tier has literally zero image quota. Two providers eliminated in an evening.
Which is the joke of it: the fallback architecture I built to look good against the judging criteria turned out to be the thing that saved the project.
A model that fails by succeeding. NVIDIA NIM occasionally returns HTTP 200, a valid PNG, correct dimensions — completely black. No error, nothing to catch. I now check the pixels and treat a blank output as a provider failure so it triggers fallback like any other error. Ordinary error handling would never have caught it, and I'd have shipped a demo that silently produced black squares.
Small ones that ate real time: Backblaze's master application key doesn't work with the S3-compatible API at all (403, no explanation) — you need a non-master key. And Genblaze's storage sink refuses to transfer files written outside its allowed directories, which is a sensible safety guard that produces a baffling error until you find it.
What's next
- Anchoring manifest hashes to a public transparency log, so when is provable independently, not just who and what
- C2PA-standard export so records open in existing Content Credentials tooling
- Video — the provenance, storage and verify core is already media-agnostic
Log in or sign up for Devpost to join the conversation.