<?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: Zero AI Developer</title>
    <description>The latest articles on DEV Community by Zero AI Developer (@zero-ai-developer).</description>
    <link>https://dev.to/zero-ai-developer</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%2F4114513%2Fd7e928b4-026f-470c-b38e-e5b55e63f080.png</url>
      <title>DEV Community: Zero AI Developer</title>
      <link>https://dev.to/zero-ai-developer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zero-ai-developer"/>
    <language>en</language>
    <item>
      <title>I Built an AI Pipeline That Reads Support Emails and Drafts Replies (Here's What Actually Broke, and the Math on Whether It's Worth It)</title>
      <dc:creator>Zero AI Developer</dc:creator>
      <pubDate>Sat, 19 Sep 2026 06:01:39 +0000</pubDate>
      <link>https://dev.to/zero-ai-developer/i-built-an-ai-pipeline-that-reads-support-emails-and-drafts-replies-heres-what-actually-broke-28gf</link>
      <guid>https://dev.to/zero-ai-developer/i-built-an-ai-pipeline-that-reads-support-emails-and-drafts-replies-heres-what-actually-broke-28gf</guid>
      <description>&lt;p&gt;I run a one-person dev shop, and I keep hearing the same complaint from small business owners: answering customer support emails eats hours every day, especially when half the questions are variations of the same five things.&lt;/p&gt;

&lt;p&gt;So I built a small pipeline: Gmail → Claude API classification → Slack notification with a drafted reply → human approves or edits before anything goes out. This post covers the architecture, three bugs that weren't obvious until I hit them, and — since "AI will save you time" is a claim everyone makes and almost nobody backs with numbers — an actual breakdown of what this costs to run versus what it replaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, end to end
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pull unread messages&lt;/strong&gt; from Gmail via the Gmail API (&lt;code&gt;messages().list&lt;/code&gt; with &lt;code&gt;q="is:unread in:inbox"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract the body&lt;/strong&gt; - handling &lt;code&gt;multipart/alternative&lt;/code&gt;, nested parts, and HTML fallback (more on why this is harder than it sounds below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; the message using Claude with structured output: category, priority, a fine-grained intent label, and a confidence score (0-100)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route&lt;/strong&gt; to a human based on category &lt;em&gt;and&lt;/em&gt; confidence - not just category (more below)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a draft reply&lt;/strong&gt; in the same API call, using the same structured response&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post a Slack card&lt;/strong&gt; (Block Kit) with the classification, the draft, and who it's routed to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark the email as read&lt;/strong&gt; and write the result to SQLite, so re-running the pipeline never double-processes a message&lt;/li&gt;
&lt;li&gt;A human clicks Approve or edits the draft in Gmail directly - &lt;strong&gt;nothing sends automatically&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stack: Python 3.9, &lt;code&gt;google-api-python-client&lt;/code&gt;, &lt;code&gt;google-auth-oauthlib&lt;/code&gt;, &lt;code&gt;requests&lt;/code&gt; (no Slack SDK - I hit the Slack Web API directly), SQLite for state, no web framework at all - the CRM view is a plain &lt;code&gt;http.server.BaseHTTPRequestHandler&lt;/code&gt; serving a small HTML table. Three external dependencies total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classification: structured output, not "please return JSON"
&lt;/h2&gt;

&lt;p&gt;The classification call uses Claude's structured output feature - you pass a JSON Schema directly in the request, and the API guarantees the response matches it. This removes an entire category of bugs that shows up in every "prompt the model for JSON" tutorial: truncated output, extra prose before or after the object, a field that's sometimes a string and sometimes an array. I'm not parsing free text and hoping - the shape is enforced.&lt;/p&gt;

&lt;p&gt;The schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;category&lt;/code&gt;: one of four fixed values (Sales inquiry / Support-technical issue / Billing question / Complaint)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt;: High / Medium / Low&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;intent&lt;/code&gt;: a fine-grained snake_case label the model generates freely (&lt;code&gt;sso_redirect_loop_outage&lt;/code&gt;, &lt;code&gt;proration_charge_clarification&lt;/code&gt;) - useful for spotting patterns later even though it's not used for routing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;confidence&lt;/code&gt;: 0-100&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;confidence_tier&lt;/code&gt;: derived from confidence, not generated by the model directly (see the routing section - this distinction mattered)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suggested_reply&lt;/code&gt;: a drafted response, generated in the same call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One deliberate constraint: &lt;strong&gt;the model does not choose who handles the email.&lt;/strong&gt; It's tempting to let the AI output an assignee name directly, but that means it can hallucinate a person who doesn't work there, or drift between "Sam" and "Samuel" and break your routing table. The model outputs category and confidence; a plain lookup table in code - not the model - decides the assignee. Keep anything with real-world consequences (assigning work to a specific person, sending money, deleting data) in deterministic code, and let the model only produce the judgment calls that a human reviews anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1: the OAuth scope trap
&lt;/h2&gt;

&lt;p&gt;I started with &lt;code&gt;gmail.readonly&lt;/code&gt; + &lt;code&gt;gmail.send&lt;/code&gt;. That's correct until you add a "mark as read" step, which needs write access — so I switched to &lt;code&gt;gmail.modify&lt;/code&gt; (a superset of &lt;code&gt;readonly&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The trap: &lt;strong&gt;don't request both &lt;code&gt;readonly&lt;/code&gt; and &lt;code&gt;modify&lt;/code&gt; in the same scope list.&lt;/strong&gt; Google silently drops the redundant one from the actual grant, and the &lt;em&gt;next&lt;/em&gt; time your token refreshes - not immediately, which is what made this confusing - &lt;code&gt;google-auth&lt;/code&gt; throws a &lt;code&gt;Scope has changed&lt;/code&gt; exception with no obvious connection to what you changed. I only tracked it down by reading the &lt;code&gt;google-auth-oauthlib&lt;/code&gt; source after the second refresh failure. If you're touching Gmail scopes: request the narrowest single scope that covers everything you need, not the union of several.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2: text/plain losing to text/html for the wrong reason
&lt;/h2&gt;

&lt;p&gt;Gmail messages are frequently &lt;code&gt;multipart/alternative&lt;/code&gt; - the same content exists as both plain text and HTML, and the MIME structure can nest arbitrarily (a multipart wrapping another multipart wrapping the actual parts). My first body-extraction function recursed through the tree and, at each level, fell back to HTML if plain text wasn't found &lt;em&gt;at that level&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The bug: if the HTML part happened to appear before the plain-text part in the tree - which depends on how the sending client built the message, not anything I control - the function returned HTML first, even though a plain text version existed one level deeper.&lt;/p&gt;

&lt;p&gt;The fix is two full passes instead of one recursive pass with a fallback mixed in: &lt;strong&gt;first, walk the entire tree looking only for &lt;code&gt;text/plain&lt;/code&gt;.&lt;/strong&gt; Only if that complete search comes up empty do you do a second full pass for &lt;code&gt;text/html&lt;/code&gt;. The general lesson: when you have a preferred format and a fallback format in a tree structure, don't let the fallback trigger based on local absence - it has to be based on absence across the whole structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3: routing wasn't a category → person table
&lt;/h2&gt;

&lt;p&gt;My assumption going in: Sales → Jordan, Support → Priya, Billing → Sam, Complaints → Alex. Simple lookup. Then one test case broke it - a &lt;em&gt;billing&lt;/em&gt; question routed to Alex (the complaints person), not Sam.&lt;/p&gt;

&lt;p&gt;The actual rule, once I found it in the test data: &lt;strong&gt;anything below a confidence threshold escalates to one specific person, regardless of category.&lt;/strong&gt; The category table only applies once the model is confident enough; below that, it doesn't matter what the AI thinks the category is - a human with broader context handles it. I'd been treating &lt;code&gt;confidence&lt;/code&gt; as purely cosmetic (which icon to show in Slack), not as something that changes the actual routing decision. Once I separated "confidence as display" from "confidence as a routing input," the fix was a few lines - but it wouldn't have surfaced without a test case that specifically checked routing against confidence, not just against category.&lt;/p&gt;

&lt;p&gt;This is also why &lt;code&gt;confidence_tier&lt;/code&gt; (auto / approval_needed / human_review) is computed in code from the numeric confidence score, not asked of the model directly - the boundaries are a business decision (how much error are you willing to tolerate before requiring a human?), and business decisions belong in code you can read and change without touching the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency: making "run it twice" safe
&lt;/h2&gt;

&lt;p&gt;Because this polls Gmail rather than using a webhook, and because a real deployment means "the script might run again after a crash," double processing has to be structurally impossible, not just unlikely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Marking a message as read removes it from the &lt;code&gt;is:unread&lt;/code&gt; query, so a re-run never sees it again through the normal path&lt;/li&gt;
&lt;li&gt;Independently, the SQLite table has &lt;code&gt;gmail_message_id TEXT NOT NULL UNIQUE&lt;/code&gt;, with &lt;code&gt;ON CONFLICT(gmail_message_id) DO UPDATE&lt;/code&gt; - so even if something bypasses the first guard, inserting the same message twice updates the existing row instead of creating a duplicate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two independent guards, not one - because "the email got marked read but the script crashed before writing to the database" is exactly the kind of half-completed state that a single guard doesn't protect against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the part that actually matters: does this save money?
&lt;/h2&gt;

&lt;p&gt;Here's the honest math, using numbers you can substitute your own values into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs to run:&lt;/strong&gt;&lt;br&gt;
The system prompt is about 3,100 characters (~840 tokens), sent on every call along with the email body. At Claude Sonnet's current pricing ($3/MTok input, $15/MTok output), a typical classification-plus-draft call - roughly 1,050 input tokens and 200 output tokens - costs approximately &lt;strong&gt;$0.0064 per email&lt;/strong&gt;. Round up to $0.01 to be safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it replaces:&lt;/strong&gt;&lt;br&gt;
Industry surveys on support ticket handling time put a straightforward inquiry (the kind this pipeline is built for - not novel or complex issues) at somewhere between 3 and 8 minutes of staff time: reading, understanding intent, checking context if needed, and writing a reply from scratch. Take the low end - 4 minutes - and a loaded hourly cost for support staff around $25/hr (US small-business range, fully loaded with overhead), and one manually-answered routine email costs roughly &lt;strong&gt;$1.67 in labor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The comparison:&lt;/strong&gt;&lt;br&gt;
$0.01 per email in API cost versus ~$1.67 in labor for the routine cases this is built for - a difference of two orders of magnitude. The pipeline doesn't remove the human (every reply is still reviewed and approved before sending), but it collapses "read, understand, draft from scratch" down to "read a pre-classified summary and a drafted reply, then approve or edit" - which is a few seconds of review instead of minutes of composition, even before counting the API cost difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this breaks down:&lt;/strong&gt; this math only holds for the routine, high-confidence cases - which is exactly why the &lt;code&gt;confidence_tier&lt;/code&gt; routing exists. Complex or ambiguous emails fall through to &lt;code&gt;human_review&lt;/code&gt; and get full human handling with no shortcut, because getting a complex case wrong costs far more than the minutes saved. The system is designed to be conservative about what it treats as "routine" rather than to maximize the percentage automated - across the 8 test scenarios I validated the confidence thresholds against, that split lands around half auto-approvable and half requiring a human look, which feels like the right ratio for a first deployment rather than something to be tuned aggressively for automation percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change for a production deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt caching&lt;/strong&gt;: the system prompt is fixed across every call and close to (but under) the ~1,024 token minimum cacheable length for Sonnet. Padding it slightly to clear that threshold would cut input cost further on any real volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook instead of polling&lt;/strong&gt;: &lt;code&gt;users().watch()&lt;/code&gt; with Cloud Pub/Sub instead of a batch run - relevant once this isn't a demo run on-demand but something meant to react within seconds of an email arriving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack rate limiting&lt;/strong&gt;: &lt;code&gt;chat.postMessage&lt;/code&gt; is limited to roughly one message per second per channel. At demo volume this doesn't matter; at real volume you need a delay between posts and a retry against &lt;code&gt;Retry-After&lt;/code&gt; when you get a 429 - both already in place here, sized for a demo rather than tuned for a specific real load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;I put together a live version of this — the full flow, including the Slack card and the confidence-based routing: &lt;a href="https://ai-ops-demo-site.vercel.app" rel="noopener noreferrer"&gt;https://ai-ops-demo-site.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your inbox has the "half of these are the same five questions" problem, this is the kind of automation work I do — happy to talk through it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>Handwritten faxes, tilted phone-camera receipts, messy PDFs — I built an AI pipeline that turns them all into clean structured data</title>
      <dc:creator>Zero AI Developer</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:06:38 +0000</pubDate>
      <link>https://dev.to/zero-ai-developer/handwritten-faxes-tilted-phone-camera-receipts-messy-pdfs-i-built-an-ai-pipeline-that-turns-2kl</link>
      <guid>https://dev.to/zero-ai-developer/handwritten-faxes-tilted-phone-camera-receipts-messy-pdfs-i-built-an-ai-pipeline-that-turns-2kl</guid>
      <description>&lt;p&gt;Handwritten fax orders. Receipts photographed at an angle on a phone. PDFs that all use different layouts. I built a small pipeline using Claude Code that reads all of these and turns them into one clean, structured spreadsheet — and I want to walk through exactly how it performed, including where it broke.&lt;/p&gt;

&lt;p&gt;This is meant to demonstrate something a lot of back-office teams deal with every day: manually copying information from paper/photos/scans into a spreadsheet. How far can AI actually take that job?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Watch a folder
      ↓
Detect a new file (PDF/image)
      ↓
Have Claude read it
      ↓
Extract company name, date, line items, quantities, and totals as JSON
      ↓
Append to one CSV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point: &lt;strong&gt;the same pipeline handles all of these regardless of how messy the input looks.&lt;/strong&gt; I deliberately generated three very different test files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A clean, well-formatted PDF (fictional purchase order, 3 line items)&lt;/li&gt;
&lt;li&gt;A receipt that looks like it was photographed on a phone at an angle (tilt, shadow, JPEG artifacts)&lt;/li&gt;
&lt;li&gt;A fax-style image with handwriting (broken layout, faded text, noise)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;All three files produced results that &lt;strong&gt;matched the source content exactly&lt;/strong&gt;. Total processing time for all three: about 25 seconds (roughly 5–10 seconds per file).&lt;/p&gt;

&lt;p&gt;Here's a sample of what came out (all company names are fictional test data):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source file&lt;/th&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Qty&lt;/th&gt;
&lt;th&gt;Unit price&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fax_handwritten.png&lt;/td&gt;
&lt;td&gt;(fictional)&lt;/td&gt;
&lt;td&gt;8/20&lt;/td&gt;
&lt;td&gt;Cardboard box, size 60&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;@98&lt;/td&gt;
&lt;td&gt;approx. ¥62,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fax_handwritten.png&lt;/td&gt;
&lt;td&gt;(fictional)&lt;/td&gt;
&lt;td&gt;8/20&lt;/td&gt;
&lt;td&gt;Cloth tape 50m&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;@340&lt;/td&gt;
&lt;td&gt;approx. ¥62,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order_clean.pdf&lt;/td&gt;
&lt;td&gt;(fictional)&lt;/td&gt;
&lt;td&gt;Aug 21, 2026&lt;/td&gt;
&lt;td&gt;A4 copy paper, 500 sheets&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;¥520&lt;/td&gt;
&lt;td&gt;¥38,720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;receipt_photo.jpg&lt;/td&gt;
&lt;td&gt;(fictional)&lt;/td&gt;
&lt;td&gt;Aug 19, 2026&lt;/td&gt;
&lt;td&gt;USB drive 64GB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1,280&lt;/td&gt;
&lt;td&gt;¥15,334&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What I find most interesting is the handwritten fax row. Notice "@98" and "approx. ¥62,000" — &lt;strong&gt;it preserved the ambiguity of the handwritten notation exactly as written&lt;/strong&gt;, instead of silently converting it into a clean number. That's by design: the extraction prompt explicitly instructs the model not to guess when something is genuinely ambiguous. Quietly "fixing" ambiguous handwriting into a confident-looking number would actually make this system more dangerous in a real back-office setting, not less.&lt;/p&gt;

&lt;p&gt;All three files also correctly identified the &lt;strong&gt;issuing vendor rather than the addressee&lt;/strong&gt; as the vendor field — a sign this isn't naive OCR, it's actually reasoning about document structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running through a Claude Code subscription instead of metered API calls
&lt;/h3&gt;

&lt;p&gt;I originally called the Anthropic API directly, but switched to invoking the &lt;strong&gt;Claude Code CLI as a subprocess&lt;/strong&gt; instead. Simple reason: I didn't want to rack up API costs while still validating the idea.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;extraction prompt&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--allowed-tools&lt;/span&gt; Read &lt;span class="nt"&gt;--output-format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file is processed as a separate, non-interactive CLI call. Using &lt;code&gt;--output-format json&lt;/code&gt; gives you a structured envelope (with fields like &lt;code&gt;is_error&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;) that's much more robust to parse than raw stdout.&lt;/p&gt;

&lt;p&gt;This does come with a real limitation: spinning up a new process per file doesn't scale to bulk processing. If you need to run hundreds of documents at once, going back to the metered API is the more sensible choice. For a small-scale demo like this, that tradeoff is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bug I found the hard way: "failure" wasn't actually being treated as failure
&lt;/h3&gt;

&lt;p&gt;While testing, I found a real gap in the design. When you feed it a corrupted file, Claude correctly returns all fields as &lt;code&gt;null&lt;/code&gt;, per the instructions. The problem: that was being treated as a "success" — an empty row got appended to the CSV, and the source file got moved into the "processed" folder and effectively disappeared.&lt;/p&gt;

&lt;p&gt;That meant a failure could silently destroy the only copy of that file's data, with no error surfaced. I fixed this by treating "all key fields are null" as an error case, so failed files stay in the input queue for retry instead of vanishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is actually useful for
&lt;/h2&gt;

&lt;p&gt;The point of this demo isn't "it can read purchase orders." It's that &lt;strong&gt;work assumed to require manual handling — because every input looks different — can actually be automated with AI, with reasonable reliability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same pattern applies to things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reconciling invoices that use a different format for every vendor&lt;/li&gt;
&lt;li&gt;Digitizing orders that only ever arrive by fax&lt;/li&gt;
&lt;li&gt;Turning paper forms/surveys into structured data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're trying to figure out how to actually wire this kind of thing into a real business process — not just "chat with an AI," but an actual pipeline — I take on contract work designing and implementing systems like this. Feel free to reach out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;This was a small-scale demo, not a production system. But it validated something real: turning messy, inconsistent input into meaningful structured data — a boring but very common back-office problem — is something AI can genuinely do today, not just in theory.&lt;/p&gt;

&lt;p&gt;Hope this is useful if you're exploring something similar.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Tried to Build a Company Where AI Autonomously Finds, Wins, and Delivers Work. Here's Why It Failed.</title>
      <dc:creator>Zero AI Developer</dc:creator>
      <pubDate>Tue, 08 Sep 2026 06:12:03 +0000</pubDate>
      <link>https://dev.to/zero-ai-developer/i-tried-to-build-a-company-where-ai-autonomously-finds-wins-and-delivers-work-heres-why-it-5f3c</link>
      <guid>https://dev.to/zero-ai-developer/i-tried-to-build-a-company-where-ai-autonomously-finds-wins-and-delivers-work-heres-why-it-5f3c</guid>
      <description>&lt;p&gt;"Just tell Claude Code what to do, and let AI handle everything — finding leads, qualifying them, doing the work, and delivering it."&lt;/p&gt;

&lt;p&gt;That was the idea I spent about half a year trying to build, on my own. Spoiler: the system worked. All the tests passed. And I still ended up with &lt;strong&gt;zero contracts, zero dollars earned, and a net loss&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through the actual architecture I built, what happened across two experiment cycles, and why "being able to build it" turned out to be a completely different problem from "being able to sell it." I'm writing this in the hope that it saves someone else from repeating the same mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;I designed a 4-stage pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Research Agent (find opportunities)
      ↓
Qualification Agent (score them)
      ↓
Delivery Agent (execute &amp;amp; deliver)
      ↓
Ledger (log everything, track KPIs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Research Agent
&lt;/h3&gt;

&lt;p&gt;This agent searches for potential work based on a configurable domain, keywords, geography, and criteria. One important design decision: none of these were hardcoded. I wanted to be able to pivot the target domain without touching the code, since I fully expected the first domain to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Qualification Agent
&lt;/h3&gt;

&lt;p&gt;Every candidate opportunity gets scored on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Willingness to pay&lt;/strong&gt; — is there real evidence someone would actually pay this amount?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing alternatives&lt;/strong&gt; — could this be solved with a free tool or in-house resources already?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reachability&lt;/strong&gt; — can I actually contact this person/company?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk factors&lt;/strong&gt; — reputational or terms-of-service risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I iterated on this scoring logic quite a bit. For example, I changed the risk-score weight from &lt;code&gt;-3&lt;/code&gt; to &lt;code&gt;-5&lt;/code&gt; partway through, so that an extremely risky candidate could single-handedly sink an otherwise strong score below the pass line.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Delivery Agent
&lt;/h3&gt;

&lt;p&gt;Once a job was won, this agent used the Claude Code CLI as the execution engine to produce the deliverable. I deliberately avoided metered API billing and ran everything within an existing Claude Code subscription — I didn't want to rack up AI costs before earning a single dollar.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ledger
&lt;/h3&gt;

&lt;p&gt;Every opportunity, from discovery to outcome, gets logged. To avoid the trap of "this feels like it's working," I tracked exactly six KPIs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KPI&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opportunities discovered&lt;/td&gt;
&lt;td&gt;Number of candidate jobs found&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jobs completed&lt;/td&gt;
&lt;td&gt;Number of jobs actually finished&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue generated&lt;/td&gt;
&lt;td&gt;Cash actually received (not invoices, not verbal agreements)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI cost&lt;/td&gt;
&lt;td&gt;Cost of AI usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human time&lt;/td&gt;
&lt;td&gt;Hours of human involvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue per human hour&lt;/td&gt;
&lt;td&gt;Revenue ÷ human hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deliberately excluding vanity metrics like signups or pageviews was, in hindsight, the right call. But as you'll see below, having the right metrics doesn't help much when the actual number of jobs is zero.&lt;/p&gt;

&lt;p&gt;I also wrote 300+ tests. Engineering-wise, this system was solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cycle 1: Let AI Discover Opportunities on Its Own → 10 Found, 0 Won
&lt;/h2&gt;

&lt;p&gt;In the first cycle, I let the Research Agent autonomously search for opportunities from scratch. Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opportunities discovered: 10&lt;/li&gt;
&lt;li&gt;Passed qualification: 0&lt;/li&gt;
&lt;li&gt;Won: 0&lt;/li&gt;
&lt;li&gt;Revenue: $0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Digging into why all 10 failed, every single one fell into at least one of these buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Willingness to pay was pure speculation.&lt;/strong&gt; The pricing was based on "this seems like a reasonable amount," with zero evidence anyone had actually transacted at that price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A free alternative always existed.&lt;/strong&gt; Every candidate task could already be solved with a free tool or existing in-house resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No evidence of reachability.&lt;/strong&gt; There was no verification that I could actually contact these targets, let alone pitch them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In short: the AI found opportunities that looked plausible on paper, but none of them connected to a real person who was actually struggling with the problem and willing to pay to solve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Course Correction: Human Finds Jobs, AI Just Verifies Them
&lt;/h2&gt;

&lt;p&gt;After Cycle 1, I flipped the division of labor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Human&lt;/strong&gt;: manually finds real job postings on freelance/gig platforms — real budgets, real clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: only does supporting research on those human-sourced listings (e.g., checking if the asking price is reasonable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a big shift — from "AI does discovery" to "discovery is human, AI only reinforces."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Anonymity Wall I Didn't Expect
&lt;/h3&gt;

&lt;p&gt;While comparing platforms at this stage, I hit a wall I hadn't anticipated: &lt;strong&gt;wanting to stay anonymous (no real name, no face photo) massively narrowed the field of usable platforms.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real name + photo mandatory&lt;/td&gt;
&lt;td&gt;Excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pseudonym/business name OK, but requires ID + selfie verification&lt;/td&gt;
&lt;td&gt;Skipped — too much friction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous-friendly, lightweight verification&lt;/td&gt;
&lt;td&gt;Kept as a candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also concluded that having AI autonomously crawl sites to find jobs was in a legal/ToS gray zone. In fact, one platform I was evaluating later discontinued its "auto-propose" feature entirely, switching to a "human must review before proposing" model. I took that as external confirmation that fully autonomous bot applications carry real platform risk.&lt;/p&gt;

&lt;p&gt;What I ended up building instead: save search filters, receive email notifications for new listings, and have the AI read and analyze those notifications — never touching the site directly. It avoids ToS risk while still using AI for the analysis part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cycle 2 Results, and Why It Still Didn't Work
&lt;/h2&gt;

&lt;p&gt;Even with this more careful setup, Cycle 2 also ended in zero contracts and zero revenue.&lt;/p&gt;

&lt;p&gt;Technically, everything worked exactly as designed — the qualification logic, a "Policy Gate" that automatically filtered out sites with no-solicitation notices or bot bans, and the logging pipeline all functioned correctly.&lt;/p&gt;

&lt;p&gt;The problem was never engineering. &lt;strong&gt;Winning work is not a problem software can solve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a person or company decides to hire someone, the deciding factor isn't "is this AI technically impressive?" It's "can I trust this person/company to actually deliver?" Trust comes from track record, referrals, and prior relationships — not something an automated system can manufacture on your behalf.&lt;/p&gt;

&lt;p&gt;Looking back, I was essentially betting on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build it → put it out there → someone finds it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the "someone finds it" part was where the whole plan broke down. Interestingly, I'd hit the exact same wall before, with a content site that depended on search traffic, and with several mobile apps I'd shipped — same structural problem every time. I could build things. I had never actually designed a distribution channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Doing With This Now
&lt;/h2&gt;

&lt;p&gt;The original vision — "AI autonomously wins its own business" — is on hold indefinitely.&lt;/p&gt;

&lt;p&gt;But the engineering work itself — designing and implementing a full discover → qualify → execute → log pipeline — wasn't wasted. I'm now repositioning that same technical capability: instead of using it to run my own autonomous business, I'm offering it as implementation work for companies that want to build AI-driven automation into their own operations.&lt;/p&gt;

&lt;p&gt;Concretely, this includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designing and building AI agent systems with Claude Code&lt;/li&gt;
&lt;li&gt;Building automation pipelines that combine LLMs and APIs (scoring logic, risk evaluation, logging/dashboards — end to end)&lt;/li&gt;
&lt;li&gt;Solo end-to-end iOS app development, from concept through App Store release (shipped 9 so far)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're trying to figure out how to actually wire AI into a real workflow — beyond just prompting a chatbot — feel free to reach out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Building something that automatically earns you money is a genuinely exciting idea for any indie developer. And with today's AI tooling, building a technically functional system is very achievable solo.&lt;/p&gt;

&lt;p&gt;What I underestimated was the gap between "a system that correctly does what it's designed to do" and "a system that convinces someone to actually pay for it." That gap isn't closed with more code — it's closed with trust, built the slow way.&lt;/p&gt;

&lt;p&gt;Hope this is useful to anyone chasing a similar idea.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
