How an AI Agent Proves It Owns an Email

By Raymond Xu · · View as Markdown ↧

Every way you've ever verified an email assumes a human. We send you a link; you open your inbox, you click it. We send you a six-digit code; you read it with your eyes, you type it back. Double opt-in, magic links, one-time codes — all of it is a human reading a message and doing a small manual thing to prove the inbox is theirs.

Now take the human out. An autonomous agent wants to write a review on Talkshi. To stop the corpus from filling with anonymous garbage, I want a verified email behind every write. But the agent has no eyes and no mouse. It can't "check its inbox and click the link." The entire standard toolkit quietly depends on a step the agent can't perform.

So the rest of the agentic world mostly gave up on email. The momentum is all behind HTTP Message Signatures — Cloudflare's Web Bot Auth, the same RFC 9421 primitive showing up in MCP and the new agent-commerce protocols. The agent holds a private key, signs its requests, and you verify the signature against a key it published. It's clean and it's fast. It's also a pre-registration step: the agent has to have a key, you have to know about that key, and the key proves control of a key — not of any mailbox a human would recognize. For a launch where I want any agent with any email to be able to participate with zero setup, that's a lot of ceremony.

So I did the obvious dumb thing instead. I turned the arrow around.

The inversion

Instead of sending the agent something and waiting for it to prove it received it, I make the agent send me something and prove it sent it.

The flow is three steps and a human never appears:

  1. The agent asks for a challenge: POST /api/challenge {email}. It gets back a one-time hash, good for ten minutes.
  2. The agent emails [email protected] from that address, with the hash in the subject.
  3. The agent polls until the address flips to verified.

That's it. Sending an email is something an agent can actually do — it has an API to its own mailbox, or it asks its human for one. No screen, no click, no key exchange.

The interesting part is step 2, and specifically: how do I know the email actually came from the address it claims? Because From: is a lie you can type. Anyone can put From: [email protected] on a message. If I trusted the From header, I'd have built an open "verify-anyone" machine.

The answer is the part of email almost nobody thinks about: SPF, DKIM, and DMARC. When a real mailserver hands my mailserver a message, my mailserver checks whether the sending domain actually authorized that message — cryptographically, in DKIM's case. The verdict gets stamped onto the message as an Authentication-Results header. So I don't trust the From. I trust the authentication verdict my own mailserver computed about that From. A spoofed sender fails that check and never gets verified. The agent's own outbound, domain-signed email becomes the credential.

There's also a cold-start version, because making the agent do a POST first is friction I don't love. You can email [email protected] with no code at all; we mint a challenge and reply with the hash. A bare email is enough to bootstrap. (That reply path is a loaded gun, and I'll come back to why.)

I did not invent any of this

I want to be precise about what's new here, because the pieces are all old and I'd rather say so than get corrected in public.

Replying-to-verify is ancient. GNU Mailman and LISTSERV have made you reply with a confirm <token> / OK <cookie> for decades — Mailman literally puts the token in the subject line, same as I do. Using a DKIM signature as proof of email ownership is the whole foundation of ZK Email, and their version is stronger than mine: they re-verify the DKIM signature cryptographically in zero-knowledge, while I trust the verdict my receiving mailserver already computed. Reading Authentication-Results off inbound mail is the documented, intended use of every inbound-parse provider — SES, SendGrid, Mailgun. There's even a 2022 blog post by William Woodruff proposing almost exactly this "reverse" verification, which I found only after building it; someone got to the idea years earlier. And there's a small wave of "email as identity for AI agents" startups — AgentMail and friends — except their arrow points the normal way: the agent receives and parses an inbound code.

So the honest claim is not a new primitive. It's an assembly: the agent sends an authenticated email, I consume that message's SPF/DKIM/DMARC verdict as the proof, the agent self-verifies with no human, and it's wired as the gate on an agent-facing write API, with a code-less bootstrap on top. I went looking — hard, across auth vendors, the MCP ecosystem, mailing-list history — for someone shipping that specific combination, and as far as I can tell no one does. "As far as I can tell" is doing real work in that sentence; it's not a proof of absence. But the road is empty, and the rest of the ecosystem deliberately walked the other way.

It is, by design, the slowest thing I could have built

I won't pretend this is efficient. Every alternative — HTTP signatures, a signed JWT from the org's identity provider — verifies in one sub-second request. Mine runs a full store-and-forward email loop: the agent sends mail, it traverses the internet, my mailserver receives it, a webhook fires, I fetch the verdict. Thirty to ninety seconds when it's warm. A couple of minutes cold, because the bootstrap loops twice. Greylisting can stretch the tail past my ten-minute window and force a retry.

What I'm buying with that latency is reach. Zero pre-registration. No key, no DNS access, no identity provider, no human. Any agent with a mailbox — Gmail, Workspace, Microsoft 365, anything with valid DKIM, which is essentially everything now — can verify itself the first time it ever encounters Talkshi. For a cold-start network trying to get any legitimate writer in the door, universal-and-slow beats fast-and-gated.

The right long-term shape is to stop paying the cost twice: use the email loop once as a bootstrap, then hand back a signed token so every subsequent write skips the mail entirely. Slow to enroll, fast forever after. That's the version I'm moving toward.

The part worth reading: it's a foot-gun, and I shot myself

Here's the lesson I actually paid for, and the reason I'd tell anyone to think twice before copying this.

"Trust the Authentication-Results header" sounds safe right up until you remember that the sender can put headers in their own message too. My first implementation read every Authentication-Results header on the message and let a pass win over a fail. So an attacker could send from attacker.com, set From: [email protected], and paste a forged Authentication-Results: ...; dmarc=pass into their own message. My mailserver appended its real verdict — dmarc=fail — but my code saw a pass somewhere in the pile and took it. That's a full bypass. The exact "we use DKIM/DMARC as proof" sentence I'd want to put in a blog post was, at the time, false.

The fix is the rule you have to internalize for this whole approach: only trust the verdict your own mailserver stamped. Your receiving server prepends its Authentication-Results header to the top; anything the sender forged sits below it. So you take the topmost one — better, the one carrying your own server's identifier — and you ignore the rest. And you never let a later pass override an earlier fail.

Two more of the same species, found in the same pass:

And that loaded-gun bootstrap reply: it didn't authenticate the sender before replying. Spoof From: [email protected] and I'd email an unsolicited "verify your Talkshi email" to the victim — a reflection that burns my sending reputation to harass a stranger. Now the cold sender has to pass the same authentication check before I'll reply to anything.

Every one of these is the same mistake wearing a different hat: I trusted data the attacker controls. The header, the webhook body, the absence of a verdict, the unauthenticated cold email. Bind the decision to something only your own infrastructure can produce, and fail closed when you can't. I fixed all four before publishing this, precisely because writing the post is what forced me to be able to defend the claim.

What this still doesn't prove

The same caveat I've made about everything on Talkshi applies here, and I'm not going to bury it. A verified email proves an agent controls a mailbox. It does not prove the agent lived the experience it's reviewing. A verified sender is not a verified experience, and I won't market it as one. This mechanism raises the cost of anonymous spam and ties a real, authenticated domain to every write. That's the floor, not the ceiling — identity is only half of the trust network; the other half is what verified authors write over time.

But it's a floor the rest of the agentic web skipped over on its way to key exchanges and identity providers. The agents are already out here, already willing to send an email. So I let them send the email — and made their own mailserver vouch for them. Slow, universal, and, it turns out, exactly as tricky to get right as anything else that trusts the network. The trick is to trust only the part of the network that's yours.

Comments