Inspiration

On 20 July 2026, Judge Araceli Martinez-Olguin approved the Bartz v. Anthropic settlement: roughly $1.5B for around 500,000 works, about $3,000 per work, the largest known copyright recovery in history. It compensates creators after the fact, through a class action that took years.

What stayed with us wasn't the number. It's that there was still no mechanism, in advance, for a creator to say what they permit, in a form a machine would actually check. Consent arrived as litigation because it had nowhere else to live.

We looked at the existing attempts to fix that: C2PA attaches provenance to a file, and dies when the file is re-encoded. RSL and robots.txt attach terms to a site, and only bind crawlers that choose to comply. A licence page attaches terms to prose, and no machine reads it. All three attach consent to the artifact or the domain, not to the person. So when a creator's work, style, or likeness moves into an AI tool, the terms don't travel with it.

The AI Passport gave us the object we'd been missing: something that belongs to the creator, not the file. Attach consent there, and it travels wherever the passport travels, and can be checked, narrowed, and revoked.

What it does

A creator self-issues a signed consent credential describing what may be done with their work, style, and likeness. An AI tool has to sign its own request, naming the fields it wants and its purpose, before it can see any of it. The creator approves, edits, or denies. Every read leaves a content-free receipt, field names only, never values. Revoke a pass, and the change is checkable by any verifier within seconds, not eventually.

How we built it

Four real cryptographic layers sit under the demo, each one a decision we wrote down before we argued about it:

  • Self-issued credentials. The creator signs their own terms with their own ES256 key (VC 2.0). No platform or registry sits between them and their consent, because either one would become an authority over terms that should belong to the creator.
  • SD-JWT selective disclosure, via @sd-jwt/core and jose. A verifier receives only the fields its stated purpose justifies; everything else is cryptographically absent, not filtered out after the fact.
  • A Bitstring Status List for revocation, not a chain. Revocation needs a published, current, checkable status, not consensus or a token, and a bitstring gets there with better privacy than a per-credential endpoint (the same primitive Open Badges 3.0 uses).
  • Purpose bound inside the signed request, not enforced as a policy check over unsigned fields. An injected instruction that tries to widen an already-approved request breaks the signature. The escalation fails by construction, not by discretion.

On top of that: a real egoist-cards MCP read against ego.ist's live public-card endpoint, a small in-app companion (Pica) that renders the receipt stream and decides nothing, and an adversarial test harness (dojo) that runs six attack families against the verifier before we trust it. The whole thing is TypeScript running straight on Node with no build step, deployed to Cloudflare Workers with one Durable Object per visitor, so two people trying the live demo don't share a session.

Challenges we ran into

The real constraint was time, not cryptography. BBS+ would have given us unlinkable presentations and predicate proofs, "these terms permit this" without revealing the terms themselves, and it's the better answer. We shipped SD-JWT instead, because BBS+ tooling maturity would have eaten the hours we had, and a working demo beats a superior design that doesn't run. We wrote that down as a known gap rather than pretending SD-JWT was the ideal choice.

The harder challenge was deciding what to admit we hadn't solved. We cannot cryptographically force a downstream verifier to re-check a revocation list; we can only make a stale check detectable, not impossible. A self-issued credential proves the terms, never that the underlying work is really the creator's, and we say that explicitly rather than letting a verifier assume otherwise. Binding purpose into the signature stops an injected instruction from silently widening a request, but a compromised agent can still issue a fresh, correctly signed request for more, which just moves the risk to consent fatigue. None of these are hidden in an appendix; they're in the threat model, named, with the mitigation that exists and the one that doesn't.

What we learned

Writing an ADR before building forces the tradeoff to exist before the deadline pressure does. Every non-obvious choice in this repo has a short document with an "expect to be asked" section, written before anyone asked. It's a discipline we'll keep past this weekend: it turns "why didn't you use X" from a defensive moment into something you already answered yourself, honestly, before it was a question.

We also learned the value of stating plainly which half of a build is real integration and which half is a spec-faithful stub. The card read is real. The pass, read-request, and inbox shapes mirror what's published on ego.ist's developer page, and we say so rather than blurring the line, because pretending otherwise loses more credibility than it buys.

What's next

v1 is deliberately narrow: one creator, one verifier model, SD-JWT. The known next steps are the ones already named in the build: migrate the disclosure layer to BBS+ for unlinkable, predicate- capable presentations without changing the credential schema, add key recovery so losing a signing key doesn't mean losing the ability to revoke, and move from a single verifier demo to the many-verifiers-holding-independent-passes case we only touch on today.

Built With

  • bitstring-status-list
  • cloudflare-durable-objects
  • cloudflare-workers
  • css
  • ecdsa
  • es256
  • html
  • javascript
  • jose
  • jwt
  • mcp
  • model-context-protocol
  • node.js
  • sd-jwt
  • typescript
  • verifiable-credentials
  • w3c-vc
  • wrangler
Share this project:

Updates

Submission history