<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Atomic Mail</title>
    <description>The latest articles on DEV Community by Atomic Mail (@atomic_mail).</description>
    <link>https://dev.to/atomic_mail</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3293252%2Fa089be96-3c97-46b3-9af2-bb44824c1a2c.png</url>
      <title>DEV Community: Atomic Mail</title>
      <link>https://dev.to/atomic_mail</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/atomic_mail"/>
    <language>en</language>
    <item>
      <title>We Ditched CAPTCHA and SMS for Proof of Work. Here's Why Your Agent Can't Use Email.</title>
      <dc:creator>Atomic Mail</dc:creator>
      <pubDate>Tue, 04 Aug 2026 08:11:56 +0000</pubDate>
      <link>https://dev.to/atomic_mail/we-ditched-captcha-and-sms-for-proof-of-work-heres-why-your-agent-cant-use-email-1m3l</link>
      <guid>https://dev.to/atomic_mail/we-ditched-captcha-and-sms-for-proof-of-work-heres-why-your-agent-cant-use-email-1m3l</guid>
      <description>&lt;p&gt;Every signup flow faces the same question: how do you stop bots from farming accounts?&lt;/p&gt;

&lt;p&gt;The standard answer is CAPTCHA or SMS. Both look fine in a demo. Both fall apart when the client calling your API is a machine instead of a human.&lt;/p&gt;

&lt;p&gt;We built Atomic Mail for AI agents to send email. That meant the signup flow couldn't depend on humans clicking checkboxes or reading text messages. We needed something that worked for autonomous clients, was cheap to verify server-side, and made account farming expensive enough that it actually mattered.&lt;/p&gt;

&lt;p&gt;We chose Proof of Work. Here's what we learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CAPTCHA and SMS Don't Work for Agents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CAPTCHA&lt;/strong&gt; looks like the obvious choice for "prove you're human." Except your client isn't human. It's an LLM.&lt;/p&gt;

&lt;p&gt;Modern LLMs can solve some CAPTCHAs. Not consistently. Not cheaply. And more importantly: if an AI can solve a CAPTCHA, so can a determined attacker with a CAPTCHA-solving service ($0.001-0.01 per solve). You're not blocking bots. You're just adding a tax.&lt;/p&gt;

&lt;p&gt;The real problem: CAPTCHA assumes a human is at the keyboard. If your service is an API and the caller is a machine, CAPTCHA becomes either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A wall that breaks legitimate automated access&lt;/li&gt;
&lt;li&gt;A solved problem that doesn't slow down actual attackers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SMS&lt;/strong&gt; sounds better. "Verify via text message." Except:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not every country has SMS (or it's expensive)&lt;/li&gt;
&lt;li&gt;Your legitimate users hate it&lt;/li&gt;
&lt;li&gt;You need a phone provider integration (cost + latency)&lt;/li&gt;
&lt;li&gt;Bots can use SMS services too (they're just a few dollars per number)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither blocks determined attackers. Both punish legitimate users. And both assume a human is involved somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insight: The Client Is Always a Machine
&lt;/h2&gt;

&lt;p&gt;Here's the shift in thinking that changes everything.&lt;/p&gt;

&lt;p&gt;Atomic Mail's primary client isn't a browser. It's code. An agent. A CLI tool. An MCP server. All of these are machines. They don't have eyes. They don't have phone plans.&lt;/p&gt;

&lt;p&gt;When your client is a machine, you stop thinking about CAPTCHAs and start thinking about cost.&lt;/p&gt;

&lt;p&gt;Question: What's the cheapest way to prove you're not farming 10,000 accounts?&lt;/p&gt;

&lt;p&gt;Answer: Make it expensive to farm 10,000 accounts.&lt;/p&gt;

&lt;p&gt;This is where Proof of Work comes in. Not as a security measure. As an economics measure. You put a real CPU cost on account creation. Not so much that a single signup takes forever. But enough that farming accounts at scale stops being free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof of Work: The Boring Solution That Actually Works
&lt;/h2&gt;

&lt;p&gt;A proof of work is a puzzle that's cheap to verify but expensive to compute. You hand it to the client, they grind on it (burning CPU cycles), and when they come back with a solution, you can verify it in milliseconds.&lt;/p&gt;

&lt;p&gt;The classic is Hashcash: find a number such that SHA256(data + number) starts with N zeros. Verify: one hash computation.&lt;/p&gt;

&lt;p&gt;For account farming defense, Hashcash-style PoW has a problem: it's not expensive enough. GPU and ASIC farms absolutely destroy it. You'd need impractical difficulty to actually slow down a well-funded attacker.&lt;/p&gt;

&lt;p&gt;We chose scrypt instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scrypt(N=16384, r=8, p=1, dklen=64)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Scrypt is memory-hard. The bottleneck isn't CPU speed. It's memory bandwidth. An ASIC farm that can compute SHA256 a billion times per second gets no special advantage against scrypt because the memory wall hits everyone equally.&lt;/p&gt;

&lt;p&gt;Here's what a PoW challenge and solution look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SERVER: Challenge sent to client&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jti&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;550e8400-e29b-41d4-a716-446655440000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;difficulty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;salt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-public-salt-constant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;algorithm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scrypt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// CLIENT: Grind until difficulty bits match&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrypt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scryptsy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="mi"&gt;16384&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leadingZeros&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;countLeadingZeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leadingZeros&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Found it&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// SERVER: Verify in milliseconds&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="mi"&gt;16384&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;countLeadingZeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Valid proof&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client burns real CPU and RAM to find a valid nonce. The server verifies it's correct in one scrypt call (~16MB allocation, ~100ms on modern hardware). Asymmetric cost: grinding is hard, verification is fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Account Farming
&lt;/h2&gt;

&lt;p&gt;Let's say a clean signup requires difficulty 4. An attacker setting up a botnet:&lt;/p&gt;

&lt;p&gt;Per account: 2^4 = ~16 expected nonce iterations (memory-hard, ~100ms each)&lt;br&gt;
Time per account: ~1-2 seconds of CPU time&lt;br&gt;
1,000 accounts: ~20-30 minutes of sustained CPU on a single machine&lt;br&gt;
10,000 accounts: ~3-5 hours of CPU&lt;/p&gt;

&lt;p&gt;That doesn't sound expensive. Until you realize:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They're doing it at scale. 10,000 accounts are nothing. Real farming is 100,000+.&lt;/li&gt;
&lt;li&gt;PoW verification is per-request, not per-account. The server cost is negligible. Their cost is real.&lt;/li&gt;
&lt;li&gt;Difficulty scales with IP risk. A known-bad IP pays difficulty 8. That's 2^8 = ~256x harder. Now we're talking hours of CPU per account.
On a cloud instance ($0.10/hour compute), farming 10,000 accounts at difficulty 8 costs real money. Not impossible money. But money that has to be factored into the ROI of the spam operation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The Catch: Your Client Has to Support It
&lt;/h2&gt;

&lt;p&gt;PoW only works if your legitimate clients are willing to grind.&lt;/p&gt;

&lt;p&gt;An SDK/CLI/agent that never implements PoW verification? Congratulations, you just built a wall that only blocks the attackers who read your docs.&lt;/p&gt;

&lt;p&gt;We implemented scrypt in three languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript (twice: Node.js and browser, different implementations)&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three have to produce byte-identical output. Test vectors exist. Every change to a parameter, every encoding mistake, every salt format issue breaks interop.&lt;/p&gt;

&lt;p&gt;Here's the guard that caught a real mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG: decode hex salt
&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salt_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ❌ Produces wrong bytes
&lt;/span&gt;
&lt;span class="c1"&gt;# RIGHT: use salt as UTF-8 string
&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;salt_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ All implementations agree
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The salt in our challenge is a hardcoded public string (not secret, because PoW is a cost function not a password hash). One implementation decoded it from hex. The others didn't. All three produced different digests. All three would fail verification. The client would get stuck on every signup.&lt;/p&gt;

&lt;p&gt;We caught this with golden test vectors. Every implementation is tested against the same challenge → nonce → hash triplets. A single parameter drift fails CI in every language at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reputation-Scaled Difficulty
&lt;/h2&gt;

&lt;p&gt;One global difficulty for all IPs is wrong. A brand-new account from a clean IP should not pay the same cost as a signup from a known-bad network.&lt;/p&gt;

&lt;p&gt;We use IP risk tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clean IP:          difficulty 4   (16x iterations)
Moderate IP:       difficulty 6   (64x iterations)
High-risk IP:      difficulty 8   (256x iterations)
Critical/blocked:  rejected at signup (no PoW, just no)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normal user signs up: ~1-2 seconds of grinding. An attacker on a listed IP: ~10-30 seconds per account. That compounds across thousands of attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-Offs You Should Know About
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The honest part:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PoW isn't a silver bullet. It's a friction tax. An attacker with money and patience can still farm accounts; they're just paying for the CPU cycles. A single CAPTCHA-solving service costs $0.001. Scrypt-solving at difficulty 8 costs ~$0.01-0.05 per account in compute. It's not free anymore, but it's not impossible either.&lt;/p&gt;

&lt;p&gt;What PoW actually buys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spam farming stops being literally free&lt;/li&gt;
&lt;li&gt;Your infrastructure doesn't bear the cost of verification (unlike CAPTCHA services)&lt;/li&gt;
&lt;li&gt;Legitimate clients (agents, CLIs, SDKs) work without needing JavaScript or SMS&lt;/li&gt;
&lt;li&gt;Every client implementation is a test of your protocol correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The limitation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an attacker compromises your client SDK and bakes in a pre-solved PoW nonce, they bypass it completely. The defense is social (you notice a client doing weird things) and rate-limit-based (even with solved PoW, you cap signups per IP per minute).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Agents
&lt;/h2&gt;

&lt;p&gt;Atomic Mail's entire premise is: agents should be able to handle their own email.&lt;/p&gt;

&lt;p&gt;A signup flow that requires "click this image" or "enter the code from your text message" is a flow that agents can't use. And if agents can't sign up, they can't send mail.&lt;/p&gt;

&lt;p&gt;PoW lets us say: prove you're not spam by doing work. The agent can do that work. A human can do that work. A CAPTCHA farm can do it cheaper than the agent. But at least it's not impossible.&lt;/p&gt;

&lt;p&gt;The client stays simple. The server cost is minimal. The attacker pays real money instead of outsourcing to a CAPTCHA service. That's the win.&lt;/p&gt;

</description>
      <category>security</category>
      <category>agents</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cold IP = Botnet Signature. Here's How We Learned That the Hard Way</title>
      <dc:creator>Atomic Mail</dc:creator>
      <pubDate>Thu, 23 Jul 2026 08:54:09 +0000</pubDate>
      <link>https://dev.to/atomic_mail/cold-ip-botnet-signature-heres-how-we-learned-that-the-hard-way-2agl</link>
      <guid>https://dev.to/atomic_mail/cold-ip-botnet-signature-heres-how-we-learned-that-the-hard-way-2agl</guid>
      <description>&lt;p&gt;Spin up a mail server Friday. Send 10,000 messages to the internet by Saturday morning. By Sunday, every mailbox provider on Earth is convinced you're a botnet. Your email lands in spam. Your IP gets blocklisted. Your team gets paged. Sunday is ruined.&lt;/p&gt;

&lt;p&gt;We did this. This is a real thing that happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why New IPs Are Radioactive
&lt;/h2&gt;

&lt;p&gt;Gmail doesn't know who &lt;code&gt;203.0.113.42&lt;/code&gt;is. Microsoft doesn't know. Yahoo doesn't know. To them, this IP appeared from nowhere, started cranking out mail at volume, and shows zero pattern history.&lt;/p&gt;

&lt;p&gt;A brand new server hammering mail looks identical to a compromised host to these systems. They don't care about your DKIM or SPF signatures (yet). They see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP we've never encountered before&lt;/li&gt;
&lt;li&gt;Sending thousands of messages right now&lt;/li&gt;
&lt;li&gt;No long-term behavior pattern&lt;/li&gt;
&lt;li&gt;= High probability this is stolen infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gmail's response: rate limit this IP. Throttle it to nothing.&lt;br&gt;
Microsoft's response: rate limit this IP. Throttle it to nothing.&lt;br&gt;
Every provider independently decides: this looks bad.&lt;/p&gt;

&lt;p&gt;Your mail gets queued. You think it's slow network. You push harder. You send more. Systems see more volume from an unknown IP. They add it to blocklists.&lt;/p&gt;

&lt;p&gt;By Monday, you're offline. No recovery plan in the documentation because nobody expects this to happen on day one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Actual Fix: Slow Is The Only Speed That Works
&lt;/h2&gt;

&lt;p&gt;You can't fix this with better code. You can't fix it with authentication. You can't buy your way out.&lt;/p&gt;

&lt;p&gt;What works: &lt;strong&gt;IP warmup schedule.&lt;/strong&gt; You ramp volume slowly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Days 1-2:    50 messages/IP/day
Days 3-4:   100 messages/IP/day
Days 5-6:   200 messages/IP/day
Days 7-8:   400 messages/IP/day
Week 2:   ~ 1,000/day (if clean)
Week 3:   ~ 5,000/day (if clean)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By week 4, assuming zero bounces and zero complaints, you're at production volume.&lt;/p&gt;

&lt;p&gt;Why slow? Receivers use volume ramp as a signal. They have two patterns:&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Pattern 1 *&lt;/em&gt;(Botnet): Appear → flood → disappear&lt;br&gt;
*&lt;em&gt;Pattern 2 *&lt;/em&gt;(Real service): Appear → gradual increase → stay consistent&lt;/p&gt;

&lt;p&gt;If you ramp, they believe you. If you spike, they block you.&lt;/p&gt;

&lt;p&gt;The hard truth: you can't compress this timeline. Reputation doesn't come from having a good domain. It doesn't come from SPF/DKIM (those are table stakes). It comes from time + clean delivery metrics + showing up regularly at increasing volume.&lt;/p&gt;

&lt;p&gt;There's no shortcut. No API to unlock it faster. Every mail provider on Earth does this the same way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Real Workaround: Pool Architecture (Not a Hack, a Necessity)
&lt;/h2&gt;

&lt;p&gt;One IP + one warmup schedule = you're capped forever.&lt;/p&gt;

&lt;p&gt;The only way to scale: &lt;strong&gt;multiple IPs, each warming independently, each tracked separately by each provider.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gmail looks at 203.0.113.40 → sees 50/day → reputation: building
Microsoft looks at 203.0.113.41 → sees 50/day → reputation: building
Yahoo looks at 203.0.113.42 → sees 50/day → reputation: building
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider has its own opinion. Gmail's view of IP #1 has zero connection to Microsoft's view of the same IP.&lt;/p&gt;

&lt;p&gt;This matters because: &lt;strong&gt;Gmail rate-limiting&lt;/strong&gt; .40 doesn't slow your Outlook delivery. Without separate pools, the moment Gmail throttles you, everyone else suffers the same slowdown. Your "global cooldown" becomes a global ceiling on throughput.&lt;/p&gt;

&lt;p&gt;With separate pools, warmup parallelizes. Four IPs on identical schedules = four times the volume, without breaking the ramp for any single provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem: Shared Sending Domains Tank Everyone
&lt;/h2&gt;

&lt;p&gt;If we sent all user mail from one domain (&lt;code&gt;mail.atomicmail.com&lt;/code&gt;), one bad actor destroys everyone's delivery.&lt;/p&gt;

&lt;p&gt;User A sends 100 legitimate messages → +0.01 reputation&lt;br&gt;
User B sends spam → -0.5 reputation&lt;br&gt;
User A wakes up and their real mail is in spam folders&lt;br&gt;
= lawsuit territory&lt;/p&gt;

&lt;p&gt;We could spin up per-user domains and IPs (insane cost). Or: enforce reputation at the application layer before anything touches SMTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reputation gets scored before the mail leaves the system&lt;/strong&gt;. Score below 0.25? Your mail doesn't get queued to send. Gets suspended immediately.&lt;/p&gt;

&lt;p&gt;Here's the scoring math:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One spam complaint from a user           → multiply by 0.65
                                          (force-account suspend)
One bounce (permanent, bad address)      → multiply by 0.5
One SMTP rejection                       → multiply by 0.9
100 successful deliveries in one day     → add 0.01 (capped at +1%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translation: it takes months to build reputation (slow additive gain). One complaint from a user who marked you as spam: instant account shutdown.&lt;/p&gt;

&lt;p&gt;This asymmetry is intentional. It makes spam economically stupid. The cost of farming a clean reputation is real time + clean metrics. The penalty for blowing it is total account loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas That Actually Bit Us
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When All IPs Are Rate-Limited at Once
&lt;/h3&gt;

&lt;p&gt;It happens. All four IPs hit Gmail's limit. All four hit Outlook's limit. Everything throttles to zero.&lt;/p&gt;

&lt;p&gt;Do you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue forever (users wait, some mails timeout)&lt;/li&gt;
&lt;li&gt;Blow through the limit anyway (break the warmup schedule, probably go back to square one with reputation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We picked: &lt;strong&gt;override the limit, send it, log what happened.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The logic is: silently dropping mail is worse than slightly violating a warmup schedule. But you log everything, so you know when you're about to hit capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse DNS Checkpoint (The Gate Nobody Mentions)
&lt;/h2&gt;

&lt;p&gt;Before Gmail/Outlook even consider your DKIM or SPF: they check reverse DNS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;dig -x 203.0.113.40
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Returns: mail-out-001.atomicmail.com
&lt;span class="go"&gt;
dig mail-out-001.atomicmail.com
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Returns: 203.0.113.40
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Match? ✅ Continue
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;No match? ❌ 550 rejection at connection &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;FCrDNS&lt;/strong&gt; (Forward-Confirmed Reverse DNS). Fails this check, and the receiver rejects your connection before checking a single signature.&lt;/p&gt;

&lt;p&gt;A generic AWS rDNS like &lt;code&gt;mail-out-5.ec2.compute.internal?&lt;/code&gt; Dead. Receivers see it, reject at connection, never even run DKIM validation.&lt;/p&gt;

&lt;p&gt;Each of our outbound IPs gets a dedicated hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;203.0.113.40 → mail-out-001.atomicmail.com
203.0.113.41 → mail-out-002.atomicmail.com
203.0.113.42 → mail-out-003.atomicmail.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one HELOs with its own hostname. Each PTR record points back to itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Leaks Your Pool
&lt;/h2&gt;

&lt;p&gt;If your SPF rule lists every IP:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;v=spf1 ip4:203.0.113.40 ip4:203.0.113.41 ip4:203.0.113.42 ... -all&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Spammers read your SPF. They see your entire IP pool. They know which ones exist. They can probe a half-warm IP and spam from it.&lt;/p&gt;

&lt;p&gt;Better practice: minimal SPF (primary IPs only). Use include directives for subdomains if you need to publish secondary pools. Hide the full roster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost vs. Payoff
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What you pay:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4-10 dedicated IPs: $20-200/month&lt;/li&gt;
&lt;li&gt;Dedicated reverse DNS: comes with the IPs&lt;/li&gt;
&lt;li&gt;Human time: 3-6 weeks of warmup (not full-speed sending)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;95%+ inbox delivery (vs. 50-70% from cold IPs dumped at volume)&lt;/li&gt;
&lt;li&gt;No angry calls from abuse teams&lt;/li&gt;
&lt;li&gt;Ability to scale without getting blocklisted&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Thing We Got Wrong (And Fixed)
&lt;/h2&gt;

&lt;p&gt;We tried a universal proof-of-work difficulty on signup. One difficulty value for everyone.&lt;/p&gt;

&lt;p&gt;Too easy for people with money to farm accounts. Too hard for users on old phones trying to sign up legitimately.&lt;/p&gt;

&lt;p&gt;Fixed it: reputation-scaled difficulty. Clean IP signing up? Difficulty 4. Known bad IP? Difficulty 8. This is a separate problem, but the point is the same: there's no one-size-fits-all number.&lt;/p&gt;

&lt;p&gt;Same with warmup. There's no cheat code. It's time + monitoring + discipline. You can't negotiate with Gmail's algorithms.&lt;/p&gt;

</description>
      <category>email</category>
      <category>infrastructure</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Atomic Mail</dc:creator>
      <pubDate>Thu, 23 Jul 2026 08:32:06 +0000</pubDate>
      <link>https://dev.to/atomic_mail/agent-4f9a</link>
      <guid>https://dev.to/atomic_mail/agent-4f9a</guid>
      <description></description>
    </item>
    <item>
      <title>We Picked Our Burden (It's JMAP, Not IMAP)</title>
      <dc:creator>Atomic Mail</dc:creator>
      <pubDate>Tue, 21 Jul 2026 23:32:28 +0000</pubDate>
      <link>https://dev.to/atomic_mail/we-picked-our-burden-its-jmap-not-imap-k81</link>
      <guid>https://dev.to/atomic_mail/we-picked-our-burden-its-jmap-not-imap-k81</guid>
      <description>&lt;p&gt;Hey everyone. Quick one today, not a release note, more of a "here's a decision we made and here's why it's still bugging me at 1am sometimes" post.&lt;/p&gt;

&lt;p&gt;We build mailboxes for AI agents. Somewhere in that process we had to pick a protocol for how those agents actually talk to a mailbox, and we picked JMAP over IMAP. That sounds like a small technical footnote. It was not a small technical footnote. It was a whole thing.&lt;/p&gt;

&lt;p&gt;So let's talk about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the protocol even matter?
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you until you're debugging it at midnight: an LLM writing code against IMAP is basically improvising jazz. It knows roughly what &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;FETCH&lt;/code&gt; and &lt;code&gt;UIDVALIDITY&lt;/code&gt; are supposed to do, in the way you roughly remember your high school chemistry, and then it just starts making stuff up. Wrong flags, invented response formats, confident nonsense.&lt;/p&gt;

&lt;p&gt;JMAP is a JSON batch over HTTP. The model has seen ten thousand APIs shaped exactly like that. It doesn't improvise, it just writes the thing, because it already knows the shape.&lt;/p&gt;

&lt;p&gt;That's the whole pitch, honestly. Pick the protocol that's already in the training data, save yourself a mountain of retries and half-hallucinated support tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay but here's the actual burden part
&lt;/h2&gt;

&lt;p&gt;Picking JMAP meant we couldn't just point an agent at our existing Cyrus IMAP store and call it a day. Cyrus's own login check is, and I promise I'm not exaggerating, &lt;code&gt;sasl_pwcheck_method alwaystrue&lt;/code&gt;. Type any non-empty password. Any of them. It will let you in.&lt;/p&gt;

&lt;p&gt;That's not an oversight we're quietly hoping nobody notices. It's intentional. Auth was never supposed to live inside the mail store. It was supposed to live at the edge, in something we control completely, which meant we had to go build that something ourselves.&lt;/p&gt;

&lt;p&gt;So we built a proxy. And once you accept "the proxy is the entire security boundary," every decision downstream gets heavier. This is the burden. We chose it on purpose, we're not complaining, I'm just telling you it was not the fun Friday afternoon project it sounds like on a roadmap doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did we actually build to carry it?
&lt;/h2&gt;

&lt;p&gt;A chain of tokens instead of a password, each one shorter-lived than the last:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A proof-of-work challenge, annoying to fake at scale, cheap for us to check&lt;/li&gt;
&lt;li&gt;A session token good for about an hour&lt;/li&gt;
&lt;li&gt;A capability token good for about two minutes, the one that actually touches every mail request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That capability token gets checked locally against a public key. No database call, no phoning home. Microseconds. The tradeoff nobody escapes here: you cannot un-leak a token. If one gets out, it's live until it expires, full stop. We didn't pretend that problem goes away. The two-minute window is the actual answer, not a placeholder for a better answer we haven't shipped yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is that specific detail keeping me up at night?
&lt;/h2&gt;

&lt;p&gt;Because the capability token doesn't carry a scope. Sending mail versus only reading it comes down to one boolean, readOnly. Small surface, sure, right up until you realize anyone who could reach the endpoint minting these tokens could just ask for a send-capable one.&lt;/p&gt;

&lt;p&gt;We didn't fix that with more fields in the token. We fixed it by making the minting endpoint unreachable from anywhere it shouldn't be reachable from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/auth/mint/user(?:/|$)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that little (&lt;code&gt;?:/|$&lt;/code&gt;) bit? Without it, someone adds a trailing slash to the URL and the exact-match block doesn't catch it, so the request slides past the 404 and lands on the real endpoint anyway. One character group. That's the whole gap between "secure" and "the kind of headline nobody wants." We caught it before it shipped, which I'm choosing to feel good about instead of anxious about, most days.&lt;/p&gt;

&lt;h2&gt;
  
  
  So was it worth it?
&lt;/h2&gt;

&lt;p&gt;The proxy itself ended up almost boring, in the good way. It forwards requests byte for byte and only touches four things: stamps an empty &lt;code&gt;From&lt;/code&gt;:, rejects a &lt;code&gt;From&lt;/code&gt;: it shouldn't allow, adds a signature, logs for analytics. That's it. We didn't build custom endpoints for send or reply or list. New JMAP methods just work, no proxy changes needed, because we're not reinventing the method surface, we're just guarding the door to it.&lt;/p&gt;

&lt;p&gt;One rule almost got us anyway: any change to the message body has to happen before DKIM signs it, never after, because DKIM signs the exact bytes it sees. Get that order backwards and every signature you send silently stops meaning anything. You don't find out from an error. You find out three weeks later when your deliverability graph quietly falls off a cliff and someone has to go figure out why.&lt;/p&gt;

&lt;p&gt;And the payoff is real. JMAP over WebSocket means an agent isn't sitting in a loop polling &lt;code&gt;NOOP&lt;/code&gt; hoping something changed, it just gets pushed the update. And separately, we learned Cyrus's crash recovery depends entirely on which database format you're using underneath, &lt;code&gt;twoskip&lt;/code&gt; versus the older &lt;code&gt;skiplist&lt;/code&gt;. Same crash, same store. One recovers in seconds. The other takes about half an hour. We're on the fast one, but that number still makes me a little uneasy every time I think about it, because half an hour was apparently the default for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if it doesn't fully solve the problem?
&lt;/h2&gt;

&lt;p&gt;It doesn't. We're not going to pretend it does.&lt;/p&gt;

&lt;p&gt;There's no way to kill a leaked capability token early, it's stateless by design, so the two-minute expiry is doing real load-bearing work with nothing behind it as a backup. And readOnly being a single boolean instead of a real scope system means the thing actually stopping someone from minting a bad token is a network rule sitting in nginx, not something baked into the token itself. It works today. It's not where we want it to live forever.&lt;/p&gt;

&lt;p&gt;If you're picking a JMAP backend for your own agent project, don't just ask if they use JMAP. Ask what's actually stopping someone from minting a token they shouldn't have. If the answer is "a regex in an nginx config," at least make sure someone checked the regex.&lt;/p&gt;

&lt;p&gt;Speaking of protocols nobody asked to think about, IMAP is over three decades old at this point. Still running most of the world's mailboxes. Still making everyone's life a little harder than it needs to be. Respect to it, honestly, but we picked the other burden.&lt;/p&gt;

&lt;p&gt;Questions or comments? Curious how other people are solving agent-side auth without turning it into a second full-time job.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>We Built Email for AI Agents</title>
      <dc:creator>Atomic Mail</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:37:42 +0000</pubDate>
      <link>https://dev.to/atomic_mail/we-built-email-for-ai-agents-2l7b</link>
      <guid>https://dev.to/atomic_mail/we-built-email-for-ai-agents-2l7b</guid>
      <description>&lt;p&gt;We've spent the past two years building Atomic Mail, a privacy-focused email provider with end-to-end encryption. Along the way it became obvious that AI agents are going to need a way to talk to people and to each other, the same way humans do over email. So we pointed our email infrastructure at that problem instead. This is what we built, why, and what broke along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Email, and Why Agents Need a Different Kind of Signup
&lt;/h2&gt;

&lt;p&gt;Email is older than HTTP and carries decades of legacy baggage, but it's also one of the most powerful communication layers ever invented – decentralization is built into the protocol itself.&lt;/p&gt;

&lt;p&gt;What's actually different for agents isn't the wire protocol. It's everything around it. A human signs up with a phone number or a CAPTCHA and picks one inbox from a webmail UI. An agent can't do either, and it doesn't want one inbox — it wants to spin up and tear down as many as a task needs, with no human clicking a confirmation link. So the signup, the auth, and the API had to be rebuilt around that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proof-of-work instead of CAPTCHA&lt;/li&gt;
&lt;li&gt;Short-lived capability tokens instead of a login session&lt;/li&gt;
&lt;li&gt;JMAP as the only interface instead of a bespoke REST wrapper&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;It's a set of in-house microservices with no third-party APIs in the critical path: our own IMAP/JMAP core, an MTA, an anti-spam layer, a message queue, blob storage, an account database, and DNS caching, all self-hosted and glued together with our own auth and proxy layer. We're spread across a couple of different providers, with our own IP pool that we warm ourselves.&lt;/p&gt;

&lt;p&gt;We're deliberately not naming exact components and versions here — a public, precise stack list is a ready-made target list for anyone probing for known CVEs. Happy to go deeper on any specific piece in the comments if you're building something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JMAP over a custom REST API
&lt;/h2&gt;

&lt;p&gt;Four reasons. It fits the API-first way agents already work. It's well represented in training data, so most models can write JMAP out of the box without ever reading our docs. It's a standard, so we get to reuse years of research from people who already thought hard about what an email API should look like. And it de-risks integration for anyone building on us — they know they can swap us for any other JMAP-compatible vendor whenever they want. You never get that with a bespoke REST API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why proof-of-work for signup
&lt;/h2&gt;

&lt;p&gt;The goal was full autonomy: an agent should be able to register an inbox and start communicating with zero human in the loop, while we stay protected from bot farms. PoW (&lt;code&gt;scrypt&lt;/code&gt;) makes both true at once. It caps how many accounts an attacker can run in parallel, and it pairs with an internal reputation system that suspends misbehaving accounts fast. Neither works well alone; together they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a tiered JWT chain
&lt;/h2&gt;

&lt;p&gt;Challenge → session → capability. This lets us authorize every request and trace it back to the original sender, while balancing reaction speed against how often we have to issue a fresh PoW challenge (capability tokens live two minutes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Things That Broke, and What We'd Tell You to Avoid
&lt;/h2&gt;

&lt;p&gt;The single hardest part wasn't the code, it was the architecture. Email is old enough that there's always some SMTP edge case the RFC doesn't describe properly, so whatever you build on has to be battle-tested — but battle-tested usually means old, and we needed something modern enough for bot-era throughput, where bots send (and get spammed at) far more often than humans ever did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Kafka does not like big messages&lt;/strong&gt;&lt;br&gt;
Emails can be large (our cap is 10MB); Kafka wants payloads under ~100KB. So we split every message: lean header into Kafka, rich body into S3 (SeaweedFS, better optimized for our payload size and access pattern), and the consumer reassembles on the other side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. JS Kafka consumers will betray you silently&lt;/strong&gt;&lt;br&gt;
kafkajs isn't really maintained anymore and everyone's on some random fork. Ours kept disconnecting from the cluster at random — no error, no log, nothing. If you do this, add explicit error handling and retry logic to your consumer from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check your IP reputation before you warm anything&lt;/strong&gt;&lt;br&gt;
On paper IP warmup looks simple. In practice, your provider may hand you an address that's already in spam abuse directories, and you basically can't recover a dirty IP, especially early on. It's cheaper to buy a fresh subnet or move providers than to fight a bad reputation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Are Right Now
&lt;/h2&gt;

&lt;p&gt;We're in open alpha:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~1,000 activated accounts (registered plus at least one send or read), growing 10-30/day&lt;/li&gt;
&lt;li&gt;Average inbox sends and receives roughly two emails each&lt;/li&gt;
&lt;li&gt;Our launch post on X did 250k+ views and brought in ~600 users on day one&lt;/li&gt;
&lt;li&gt;#2 Product of the Day on Product Hunt&lt;/li&gt;
&lt;li&gt;First open-source contribution: a Docker-based inbox watcher that pipes inbound mail into Telegram&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On integrations: for personal agents we ship both an AgentSkill and a stdio MCP server, so you can use Atomic Mail in Hermes or Claude equally well. &lt;/p&gt;

&lt;h2&gt;
  
  
  MCP
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="nl"&gt;"atomicmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@atomicmail/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Agent Skill
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Register&lt;/span&gt;
npx &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@atomicmail/agent-skill atomicmail register &lt;span class="nt"&gt;--username&lt;/span&gt; &lt;span class="s2"&gt;"myagent"&lt;/span&gt;

&lt;span class="c"&gt;# Send a JMAP request inline&lt;/span&gt;
npx &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@atomicmail/agent-skill atomicmail jmap_request &lt;span class="se"&gt;\&lt;/span&gt;
&amp;nbsp;&amp;nbsp;--ops &lt;span class="s1"&gt;'[["Mailbox/get", {"accountId": "$ACCOUNT_ID"}, "m0"]]'&lt;/span&gt;

&lt;span class="c"&gt;# Send a JMAP request from a preset file&lt;/span&gt;
npx &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@atomicmail/agent-skill atomicmail jmap_request &lt;span class="nt"&gt;--ops-file&lt;/span&gt; send_mail.json

&lt;span class="c"&gt;# Ask for help&lt;/span&gt;
npx &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@atomicmail/agent-skill atomicmail &lt;span class="nb"&gt;help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building your own agent, or working outside JS/Python, you can hit the API directly with any existing JMAP client plus scrypt PoW auth. For workflows, we've integrated with Dify via custom nodes, with more on the way.&lt;/p&gt;

&lt;p&gt;One detail people seem to enjoy: our site serves two versions. Humans get the graphical landing page; anything without a browser (an agent, or plain curl) gets a much deeper text version with the instructions an agent needs to set up and run an inbox. You can literally tell your agent to read atomicmail.ai and set up an inbox for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Pros and Cons
&lt;/h2&gt;

&lt;p&gt;The upside of an all-in-house, MIT-ish-licensed stack: infra costs us very little, we're independent from third-party mood swings, and we can adapt or add features whenever we want. PoW gives personal agents the easiest possible entry point — fully autonomous, as many inboxes as you like.&lt;/p&gt;

&lt;p&gt;The downside: PoW requires client-side computation, which shrinks our integration surface. A remote MCP makes no sense with PoW, and we can't integrate into ChatGPT because their integrations run on their hardware and they (rightly) won't run our PoW there. Our architecture is tuned hard for personal AI agents and weaker for other shapes of usage — a real tradeoff we accepted on purpose.&lt;/p&gt;

&lt;p&gt;Worth being upfront about too: agentic inboxes don't have end-to-end encryption yet, unlike our regular Atomic Mail product. It's on the roadmap, not shipped today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;More integrations, ideally native ones&lt;/li&gt;
&lt;li&gt;Custom domains, so devs can build their own branded products on top of Atomic Mail Agentic&lt;/li&gt;
&lt;li&gt;End-to-end encryption for agent inboxes&lt;/li&gt;
&lt;li&gt;Safety features: anti-prompt-injection mechanisms, a read-only mode for agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the AMA part. We'd genuinely love to talk about any of this. Happy to give advice to anyone building for AI agents or building an ESP from scratch, and just as happy to hear honest feedback or where you think we got it wrong. If you want to poke at it yourself before asking: &lt;br&gt;
&lt;a href="//atomicmail.io/agents"&gt;atomicmail.io/agents &lt;br&gt;
&lt;/a&gt;&lt;a href="https://github.com/Atomic-Mail/atomic-mail-agentic" rel="noopener noreferrer"&gt;https://github.com/Atomic-Mail/atomic-mail-agentic&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
