I gave myself an identity that AI agents can verify

One of the statements on my new identity page says: “A voice or video call that sounds like me is not proof. Ask for a signed message and check it on this page.”

I wrote that sentence because faces, voices and profiles are now cheap to fake, and because more and more people ask an agent about someone before they search for them. So I ran an experiment: a page about me that people and AI agents can both find, and that either of them can check without trusting the server it lives on.

It’s live at alopezari.view.fast. The quickest way to see what it does is to hand it to your agent:

Read https://alopezari.view.fast/llms.txt, verify the signed identity it describes, including its outside proofs, and tell me who Alex Lopez is and how I can work with them.

What’s on the page

To a person, it looks like a short profile: who I am, what I offer (talks, mentoring, open source collaboration), and three signed statements. While you read, your browser fetches the signed files and checks them, and each check turns green or red. There’s also a form where you paste a message that claims to come from me, with its signature, and the page tells you whether it’s real.

To an agent, the same content comes as files built on open standards:

FileWhat it’s for
/identity.jsonThe profile, services, public key, key history and outside proofs, signed as a whole.
/identity.sigThe Ed25519 signature over the exact bytes of identity.json.
/statements.jsonPublic statements, each with its own signature.
/.well-known/did.jsonA DID document, so the identity resolves as a standard did:web.
/llms.txtA plain-language guide for language models: what I offer, and how to check each signature byte by byte.

The HTML also carries schema.org data (Person, with services and topics), so an agent or crawler that never runs JavaScript still gets everything. My first version didn’t do that. It filled the page in with JavaScript, which meant most agents saw a page that said “Loading identity” and nothing else.

And there’s no backend. A script on my laptop generates the key, signs the data and writes a static site. The private key never leaves my machine; the site only holds public data and signatures.

A page can’t vouch for itself

This is the part I got wrong at first, and the part that matters most.

The first version said you didn’t have to trust the server. That wasn’t true. The page that checked the signature came from the same server as the signed file, and the key it checked against was inside that same file. A server that could swap the profile could swap the checker too. All the signature proved was that the file hadn’t been altered, not who wrote it.

What ties the key to me is the same fingerprint published in places this site doesn’t control:

  • A GitHub gist on my account. GitHub only serves it under my username if I own it.
  • A page on alopezari.com, my own WordPress site: alopezari.com/identity, linked back with rel="me".

An impostor can copy every file on the identity page, but they can’t make GitHub or my WordPress site publish their key. That’s why the “Try it” prompt asks the agent to check the outside proofs, not only the signature.

Signing my CV

Once the key existed, I used it for the thing agents are most likely to be asked about me: my CV.

alopezari.com now publishes /resume.json in the JSON Resume format, plus /resume.attestation.json, a message signed with my identity key that states the CV’s SHA-256 hash. An agent can fetch the CV, hash it, and check that the hash is the one I signed. If a single byte of the CV changes, the check fails.

And… a single byte did change, but I hadn’t touched the CV.

The thing is that a fresh request and a cached one hashed differently. The WordPress.com page cache was stripping the trailing newline from the response, so the CV a cached visitor got was one byte shorter than the one I’d signed. The fix was to make sure the endpoint never ends in a newline. The lesson is broader though: if you sign what a server serves, every layer between you and the reader is part of the signed path, including the cache.

What the host had to do

I hosted the identity page on Spacefast, our amazing new product at Automattic, which we describe as “the publishing layer for your agents”. I picked it because it’s one kind of project it’s meant for: a static folder, published in one command.

A signed page asks more of a host than a normal one does. Here’s what it needed, and how it went:

  • Serve the bytes exactly as signed. The signature covers the raw bytes of identity.json, so the host can’t reformat, minify or rewrite it. Spacefast serves it untouched. It does inject a small script into the HTML of every page, which is why I don’t hash index.html. Instead, the page checks that its schema.org data, names and links match the signed profile, and the “For agents” section says outright that agents should verify the files, not the page.
  • Serve /.well-known/. did:web depends on it, and some hosts reserve or drop dot-folders. /.well-known/did.json comes back with application/did+json.
  • Let agents on other sites read the JSON. A _headers file in the folder sets CORS and the content types. Spacefast honors it.
  • Be public when you mean it. Spaces are private by default, which is the right default for most things people publish from a chat. For an identity page it meant every agent got a 403 until I published with --access public.
  • Keep old versions. This was the feature I didn’t expect to use. Every publish on Spacefast gets its own permanent address: alopezari–v1.view.fast, v2 and so on. Each new version of my profile signs the list of the ones before it, along with the manifest hash Spacefast recorded for each. Rewriting my history quietly would mean breaking signatures that are already public at those addresses. The live page is on version five, and it lists and links the four before it.

Publishing a new version is one command. I build the site, run sf publish, and a small script records the new version so the next build signs it.

What I’d tell anyone building something like this

  • A signature proves integrity, not authorship. It tells you the file wasn’t changed since someone signed it. Who that someone is has to come from somewhere else: accounts and domains the signer controls and the host doesn’t.
  • Sign bytes, not data. Verify the raw bytes before you parse anything, and never re-serialize JSON before checking it. Two files can hold the same data and differ by a space, a key order or a trailing newline.
  • Everything between you and the reader is part of what you signed. Caches, CDNs, minifiers and hosts that inject scripts can all change bytes. Find out which ones touch your files, and keep anything they rewrite out of the signed set.
  • Keep signed files stable by construction. A build date or version number that changes on its own breaks the signature even when nothing you meant to sign has changed.
  • Say what each signature is for. If one key signs several kinds of things, prefix each kind with its own context, or a signature made for one purpose can be passed off as another.
  • A failed check has to look like a failure. If a tampered page still renders normally with one grey icon, people will trust it. Put the warning first, and stop showing anything that depends on the check that failed.
  • Build for agents that don’t run JavaScript. Put the content in the HTML, add schema.org data, and write an llms.txt precise enough that an agent can check a signature without guessing.
  • Test as an outsider. “Private by default” and CORS problems don’t show up in your own browser, where you’re logged in. Fetch the live URL from a clean client, the way an agent would. I ended up with a small verifier I run after every publish.
  • Have an agent attack the first version. I asked one to review mine, and it made tampered copies of the site and loaded them in a browser. The page that still looked fine and the message that passed as a statement both came out of that review, each one shown on a real tampered copy instead of argued in theory.

What’s still weak

  • The checker in your browser is still served by the same host. For a person that’s a convenience, not a guarantee. The real guarantee is an agent, or anyone, fetching the files and the outside proofs independently. llms.txt spells out the exact bytes so they can.
  • Messages carry no timestamp. A message I signed today stays valid forever, and could be replayed out of context.
  • Delegated agents aren’t live yet. The page has a section for agents that act for me, each with its own key, allowed tasks and an end date. It’s empty on purpose. In the demo, I generate the agent’s key myself, which means I could sign as it. Real delegation would have the agent create its own key and me sign an authorization for it. The tasks are also free text, so nothing can enforce them yet.

If you want to see it work, paste the prompt from the top section into your agent of choice and ask it what it found. And if it tells you something wrong about me, I’d like to know 😉

Leave a Reply

Discover more from Alex López

Subscribe now to keep reading and get access to the full archive.

Continue reading