<?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: Lars Winstand</title>
    <description>The latest articles on DEV Community by Lars Winstand (@lars_winstand).</description>
    <link>https://dev.to/lars_winstand</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3908932%2Feb8bc1ff-405f-4ef0-8204-ba1ed7caa59f.jpeg</url>
      <title>DEV Community: Lars Winstand</title>
      <link>https://dev.to/lars_winstand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lars_winstand"/>
    <language>en</language>
    <item>
      <title>I found the dumbest way to burn 500 LLM calls a day: polling an inbox every 5 minutes</title>
      <dc:creator>Lars Winstand</dc:creator>
      <pubDate>Sat, 02 May 2026 13:44:12 +0000</pubDate>
      <link>https://dev.to/lars_winstand/i-found-the-dumbest-way-to-burn-500-llm-calls-a-day-polling-an-inbox-every-5-minutes-2m1o</link>
      <guid>https://dev.to/lars_winstand/i-found-the-dumbest-way-to-burn-500-llm-calls-a-day-polling-an-inbox-every-5-minutes-2m1o</guid>
      <description>&lt;p&gt;If your OpenClaw agent checks an email inbox every 5 minutes, you’re probably paying for idle paranoia.&lt;/p&gt;

&lt;p&gt;That’s not a theoretical complaint. In an r/openclaw thread about triggering jobs from email, one user described an MS365 setup like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"At the moment, I have Openclaw job where agent checks its ms365 mailbox every 5 minutes... Wasted calls to LLM (nearly 500 calls to LLM per day)"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is such a painfully real failure mode.&lt;/p&gt;

&lt;p&gt;The demo works. The cron job looks harmless. Then a month later your agent is re-checking old mail, occasionally double-processing messages, and quietly spending model calls on nothing.&lt;/p&gt;

&lt;p&gt;If you’re building always-on agents, this is exactly the kind of bug that turns “cool automation” into “why is this thing flaky and expensive?”&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern everyone starts with
&lt;/h2&gt;

&lt;p&gt;Usually it looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect OpenClaw to a mailbox&lt;/li&gt;
&lt;li&gt;Poll every 5 minutes with IMAP or Microsoft Graph&lt;/li&gt;
&lt;li&gt;If there’s a new message, send it to GPT-5.4, Claude Opus 4.6, or whatever model you’re using&lt;/li&gt;
&lt;li&gt;Try not to process the same email twice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a proof of concept, that’s fine.&lt;/p&gt;

&lt;p&gt;If it’s one internal mailbox, low volume, and you have a tiny dedupe store in SQLite, polling can be good enough.&lt;/p&gt;

&lt;p&gt;But once the workflow matters, polling starts failing in boring and expensive ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you keep checking when nothing changed&lt;/li&gt;
&lt;li&gt;you burn LLM calls on already-seen messages&lt;/li&gt;
&lt;li&gt;you introduce delays by design&lt;/li&gt;
&lt;li&gt;you get duplicate processing when scans overlap&lt;/li&gt;
&lt;li&gt;you miss messages when state gets out of sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another user in that same r/openclaw discussion put it even more bluntly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I abandoned the interval based scanning... if the scan got out of sync I had repeated responses (more wasted calls) or ignored mails. I failed to get it to be reliable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s the actual problem.&lt;/p&gt;

&lt;p&gt;Polling doesn’t just waste money. It makes the agent feel unreliable.&lt;/p&gt;

&lt;p&gt;And unreliable is worse than expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft and Google are both telling you to stop polling
&lt;/h2&gt;

&lt;p&gt;This part is worth emphasizing: the anti-polling advice is not just random architecture purism.&lt;/p&gt;

&lt;p&gt;Microsoft Graph supports change notifications so apps can react to mailbox changes instead of hammering the API on a timer.&lt;/p&gt;

&lt;p&gt;Gmail push notifications exist for the same reason. Google says push eliminates the extra network and compute cost of polling resources to see if they changed.&lt;/p&gt;

&lt;p&gt;If both mailbox providers are nudging you toward push, that’s a clue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What production intake should look like
&lt;/h2&gt;

&lt;p&gt;There are a few sane ways to do inbound email for agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gmail API watch + Google Cloud Pub/Sub&lt;/li&gt;
&lt;li&gt;Microsoft Graph change notifications&lt;/li&gt;
&lt;li&gt;Twilio SendGrid Inbound Parse Webhook&lt;/li&gt;
&lt;li&gt;an email-native service like AgentMail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common idea is simple:&lt;/p&gt;

&lt;p&gt;The provider tells your system that mail arrived.&lt;/p&gt;

&lt;p&gt;Your system does not keep asking if anything changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gmail: watch the inbox instead of polling it
&lt;/h2&gt;

&lt;p&gt;For Gmail, the production path is Gmail API watch on the inbox, then Pub/Sub delivers notifications to your webhook.&lt;/p&gt;

&lt;p&gt;Example request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://gmail.googleapis.com/gmail/v1/users/me/watch
Content-Type: application/json
Authorization: Bearer &amp;lt;access_token&amp;gt;

{
  "topicName": "projects/myproject/topics/mytopic",
  "labelIds": ["INBOX"],
  "labelFilterBehavior": "INCLUDE"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google returns a history ID and an expiration time.&lt;/p&gt;

&lt;p&gt;That means two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;you need to process changes based on history&lt;/li&gt;
&lt;li&gt;you need to renew the watch before it expires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is cleaner than polling, but it is not zero-maintenance.&lt;/p&gt;

&lt;p&gt;You still need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Pub/Sub topic&lt;/li&gt;
&lt;li&gt;a subscription&lt;/li&gt;
&lt;li&gt;IAM configured correctly&lt;/li&gt;
&lt;li&gt;watch renewal logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip the lifecycle work, your “event-driven” setup becomes a very fancy outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft 365: use Graph change notifications
&lt;/h2&gt;

&lt;p&gt;For Microsoft 365, use Microsoft Graph subscriptions for Outlook messages.&lt;/p&gt;

&lt;p&gt;Example subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://graph.microsoft.com/v1.0/subscriptions
Content-Type: application/json
Authorization: Bearer &amp;lt;access_token&amp;gt;

{
  "changeType": "created",
  "notificationUrl": "https://your-app.example.com/webhooks/graph",
  "resource": "/me/mailFolders('Inbox')/messages",
  "expirationDateTime": "2026-05-03T00:00:00Z",
  "clientState": "openclaw-mailbox-prod"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;subscription renewal&lt;/li&gt;
&lt;li&gt;clientState verification&lt;/li&gt;
&lt;li&gt;dedupe after notification delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again: more setup than polling, much better behavior in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  SendGrid is the cleanest mental model
&lt;/h2&gt;

&lt;p&gt;If you want the simplest model for inbound email to HTTP, SendGrid Inbound Parse is hard to beat.&lt;/p&gt;

&lt;p&gt;Email arrives.&lt;/p&gt;

&lt;p&gt;SendGrid parses it.&lt;/p&gt;

&lt;p&gt;SendGrid POSTs the content to your endpoint.&lt;/p&gt;

&lt;p&gt;Minimal example in Node:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencoded&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;extended&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/inbound-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;messageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Message-ID: &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&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="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. dedupe check&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. persist event&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. enqueue background processing&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;messageId&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;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening on :3000&lt;/span&gt;&lt;span class="dl"&gt;"&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;The nice part is the delivery contract.&lt;/p&gt;

&lt;p&gt;If your endpoint returns 5XX, SendGrid retries.&lt;br&gt;
If your endpoint returns 2XX, retries stop.&lt;/p&gt;

&lt;p&gt;That is a much sharper failure model than “cron ran, maybe.”&lt;/p&gt;

&lt;p&gt;There are constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total message size limit&lt;/li&gt;
&lt;li&gt;dedicated receiving subdomain setup&lt;/li&gt;
&lt;li&gt;MX record configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still better than burning cycles forever because polling was easier on day one.&lt;/p&gt;
&lt;h2&gt;
  
  
  n8n helps, but it does not magically fix polling
&lt;/h2&gt;

&lt;p&gt;This comes up a lot: “Can’t I just use n8n?”&lt;/p&gt;

&lt;p&gt;You can absolutely use n8n to improve the workflow.&lt;/p&gt;

&lt;p&gt;But if you use the n8n Email Trigger over IMAP, you are still doing mailbox-checking infrastructure. It’s just nicer mailbox-checking infrastructure.&lt;/p&gt;

&lt;p&gt;That matters.&lt;/p&gt;

&lt;p&gt;n8n gives you useful features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mailbox selection&lt;/li&gt;
&lt;li&gt;mark as read&lt;/li&gt;
&lt;li&gt;attachment handling&lt;/li&gt;
&lt;li&gt;custom search rules&lt;/li&gt;
&lt;li&gt;reconnect controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot better than a hand-rolled cron script.&lt;/p&gt;

&lt;p&gt;But it does not change the trigger model.&lt;/p&gt;

&lt;p&gt;If the source of truth is still “go ask the mailbox if anything happened,” you still have polling-shaped failure modes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Polling vs push
&lt;/h2&gt;

&lt;p&gt;Here’s the tradeoff in plain English:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What you’re really signing up for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Poll mailbox with IMAP or cron&lt;/td&gt;
&lt;td&gt;Easy setup, delayed reactions, duplicate checks, wasted model calls, awkward dedupe logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n8n Email Trigger (IMAP)&lt;/td&gt;
&lt;td&gt;Better operational ergonomics, but still polling underneath&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail watch / Graph notifications / SendGrid webhook&lt;/td&gt;
&lt;td&gt;More setup, much lower idle waste, faster reactions, better delivery semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not really “simple vs advanced.”&lt;/p&gt;

&lt;p&gt;It’s demo-friendly vs production-friendly.&lt;/p&gt;
&lt;h2&gt;
  
  
  What your OpenClaw email pipeline should actually do
&lt;/h2&gt;

&lt;p&gt;If I were building this today, I’d split it into two layers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 1: intake
&lt;/h3&gt;

&lt;p&gt;Pick one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SendGrid Inbound Parse if you want email -&amp;gt; HTTP&lt;/li&gt;
&lt;li&gt;Gmail watch + Pub/Sub if you’re on Google Workspace&lt;/li&gt;
&lt;li&gt;Microsoft Graph notifications if you’re on Microsoft 365&lt;/li&gt;
&lt;li&gt;n8n IMAP only for a fast proof of concept&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Layer 2: idempotent processing
&lt;/h3&gt;

&lt;p&gt;No matter how the event arrives, your OpenClaw job should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;extract a stable message ID&lt;/li&gt;
&lt;li&gt;check a dedupe store before calling any model&lt;/li&gt;
&lt;li&gt;persist processing state&lt;/li&gt;
&lt;li&gt;acknowledge receipt quickly&lt;/li&gt;
&lt;li&gt;do the expensive work asynchronously&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is where people get into trouble.&lt;/p&gt;

&lt;p&gt;Do not do all processing inside the webhook request.&lt;/p&gt;

&lt;p&gt;Accept the event.&lt;br&gt;
Store it.&lt;br&gt;
Deduplicate it.&lt;br&gt;
Then hand it off.&lt;/p&gt;

&lt;p&gt;That’s how you survive retries without duplicate replies.&lt;/p&gt;
&lt;h2&gt;
  
  
  A minimal queue-based pattern
&lt;/h2&gt;

&lt;p&gt;Here’s a practical shape for the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;email-webhook -&amp;gt; postgres&lt;span class="o"&gt;(&lt;/span&gt;inbox_events&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; job queue -&amp;gt; OpenClaw worker -&amp;gt; reply/send action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pseudo-schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;inbox_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;external_message_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_message_id&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;Worker logic:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processInboxEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByProviderAndMessageId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_message_id&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&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;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processing_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runOpenClawAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;That is much less exciting than prompt tricks.&lt;/p&gt;

&lt;p&gt;It is also the difference between a system that feels solid and one that occasionally replies twice at 3 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost side gets ugly fast
&lt;/h2&gt;

&lt;p&gt;If your agent is always on, wasted checks become real money or real usage pressure.&lt;/p&gt;

&lt;p&gt;This is where pricing model matters.&lt;/p&gt;

&lt;p&gt;Per-token billing makes polling bugs feel worse because every pointless re-check and duplicate pass looks like another tiny leak. You start optimizing prompts and reducing context not because it improves quality, but because you’re trying to contain operational sloppiness.&lt;/p&gt;

&lt;p&gt;That’s backwards.&lt;/p&gt;

&lt;p&gt;If you’re running OpenClaw agents continuously, predictable flat-rate compute is a much better fit than watching token spend all day. Standard Compute is built for exactly that: OpenAI-compatible API access for OpenClaw agents, flat monthly pricing, and dynamic routing across models like GPT-5.4, Claude Opus 4.6, and Grok 4.20.&lt;/p&gt;

&lt;p&gt;So yes, fix the architecture first.&lt;/p&gt;

&lt;p&gt;But also: if your agents run 24/7, stop pairing always-on automation with pricing that punishes every extra call.&lt;/p&gt;

&lt;h2&gt;
  
  
  When polling is still okay
&lt;/h2&gt;

&lt;p&gt;Polling is not always wrong.&lt;/p&gt;

&lt;p&gt;Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you have one internal mailbox&lt;/li&gt;
&lt;li&gt;volume is low&lt;/li&gt;
&lt;li&gt;a few minutes of delay is fine&lt;/li&gt;
&lt;li&gt;you have dedupe in SQLite or Postgres&lt;/li&gt;
&lt;li&gt;nobody will care if you rebuild it later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a proof of concept.&lt;/p&gt;

&lt;p&gt;Just be honest that it is a proof of concept.&lt;/p&gt;

&lt;p&gt;The mistake is pretending that a polling loop is production architecture for a customer-facing or always-on agent.&lt;/p&gt;

&lt;p&gt;It isn’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual line between toy and production
&lt;/h2&gt;

&lt;p&gt;The interesting distinction is not whether OpenClaw can read email.&lt;/p&gt;

&lt;p&gt;Of course it can.&lt;/p&gt;

&lt;p&gt;The distinction is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the email arrives&lt;/li&gt;
&lt;li&gt;whether processing is idempotent after it arrives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A toy automation asks the mailbox every few minutes if anything happened.&lt;/p&gt;

&lt;p&gt;A production agent gets an event, validates it, records it once, and processes it once.&lt;/p&gt;

&lt;p&gt;That sounds boring.&lt;/p&gt;

&lt;p&gt;It’s also the difference between “works in a demo” and “still works three months later.”&lt;/p&gt;

&lt;p&gt;If your OpenClaw workflow still polls an inbox every 5 minutes, I wouldn’t call it broken.&lt;/p&gt;

&lt;p&gt;I’d call it unfinished.&lt;/p&gt;

&lt;p&gt;And once you’ve seen nearly 500 LLM calls per day wasted on mailbox checks, it’s hard to unsee.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
