{"id":5969,"date":"2026-08-11T14:30:00","date_gmt":"2026-08-11T12:30:00","guid":{"rendered":"https:\/\/engel-wolf.com\/?p=5969"},"modified":"2026-11-08T21:30:57","modified_gmt":"2026-11-08T19:30:57","slug":"ynab-amazon-categorizer","status":"publish","type":"post","link":"https:\/\/engel-wolf.com\/?p=5969","title":{"rendered":"An AI agent that explains my Amazon transactions in YNAB (and the runbook that keeps it alive)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I built an AI agent that finally makes my Amazon and online-shopping transactions understandable. Or, to be precise: I described the problem and Claude built it. Then it broke, quietly, for weeks, and Claude fixed it again. This post is about both halves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At home we run our household budget in <a href=\"https:\/\/www.ynab.com\/\">YNAB<\/a>. Every transaction gets a category, so we know where the money goes. That works beautifully until the bank statement says &#8222;AMAZON PAYMENTS&#8220; followed by an order number. What did we actually buy? A bike light? Dishcloths? A birthday present?<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"1276\" src=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions.jpg\" class=\"wp-image-5966 sew-ai\" alt=\"Comic of a stressed man with glasses behind piles of paper, Amazon and PayPal receipts pinned to the wall, a laptop showing YNAB\" srcset=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions.jpg 1280w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-300x300.jpg 300w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-1024x1021.jpg 1024w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-150x150.jpg 150w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-768x766.jpg 768w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-100x100.jpg 100w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-301x300.jpg 301w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-01-bookkeeping-mystery-transactions-120x120.jpg 120w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><figcaption>Month end, before: 10 to 30 transactions nobody can explain.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">The problem: no API anywhere<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every month I had between 10 and 30 of these mystery transactions. For each one I opened Amazon or PayPal, searched the order, looked at the items and went back to YNAB. Ten to twenty minutes in total, and it felt pointless for a small charge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The obvious fix would be an API. Amazon: no API for your own orders. PayPal: nothing useful for this. The small online shop: forget it. But every one of them sends an order confirmation <strong>e-mail<\/strong>. That was the best idea in the whole project: <strong>forget the APIs, use e-mail<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The setup<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Everything runs on a Raspberry Pi in Docker. There are two pieces:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>The bank importer.<\/strong> An older project of mine: a small Flask app per bank account that pulls transactions through my bank&#8217;s API (with a push-TAN I confirm on my phone), plus a Vue web dashboard with an &#8222;Import&#8220; button. Both are on GitHub: <a href=\"https:\/\/github.com\/zappingseb\/comdirect_api\">comdirect_api<\/a> for the bank API and <a href=\"https:\/\/github.com\/zappingseb\/YNAB-Pi-Docker\">YNAB-Pi-Docker<\/a> for the Docker setup on the Pi.<\/li><li><strong>The categorizer.<\/strong> A second Flask service with one endpoint, <code>POST \/categorize<\/code>. Order e-mails are forwarded to a dedicated mailbox, and the service does 3 steps: <\/li><li>extract the order number from the bank text<\/li><li>search the mailbox over IMAP for the confirmation mail<\/li><li>ask Claude which products these are and which category fits.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The importer decides what counts as Amazon with a simple rule: the payee matches <code>amazon|amzn<\/code>, or the memo contains something shaped like an Amazon order number (<code>\\d{3}-\\d{7}-\\d{7}<\/code>). For those it calls the categorizer and rewrites the memo. Instead of a cryptic reference I now see something like &#8222;Shimano rear light \u2192 Cycling&#8220; or &#8222;10-pack dishcloths \u2192 Household&#8220;, and the category is already proposed. I only approve or re-categorize.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The prompt that does the work<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the heart of the tool, from <code>claude_service.py<\/code>. The category tree from YNAB goes in, grouped and with IDs, so Claude can answer with an ID I can write straight back:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n# Format tree for Claude (Group &gt; Category with IDs)\ncategory_list = &quot;&quot;\nfor group, cats in sorted(category_tree.items()):\n    category_list += f&quot;\\n{group}:\\n&quot;\n    for cat in sorted(cats, key=lambda x: x&#x5B;&#039;name&#039;]):\n        category_list += f&quot;  - {cat&#x5B;&#039;name&#039;]} &#x5B;ID: {cat&#x5B;&#039;id&#039;]}]\\n&quot;\n\nuser_message = f&quot;&quot;&quot;Analyze this Amazon order email and suggest the best YNAB category.\n\nOrder Number: {order_number}\n\nEMAIL CONTENT:\n{email_body}\n\nYOUR YNAB CATEGORIES (organized by group):\n{category_list}\n\nPRODUCT EXTRACTION RULES:\n- Remove cryptic codes at the start (like SFGSUP, ASINs, SKUs)\n- Keep the meaningful product description\n- Truncate to max 10 characters per product name\n- Extract up to 3 main products\n\nEXAMPLES:\n&#x5B;\u2026]&quot;&quot;&quot;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">A few things in there I would recommend to anyone. Give the model<strong> your real category lis<\/strong>t instead of letting it invent categories. <strong>Put IDs next to the names,<\/strong> so the answer is machine-usable without fuzzy matching. And spell out the ugly parts of the input: Amazon product titles start with seller codes and ASIN noise, and <strong>&#8222;remove cryptic codes&#8220;<\/strong> plus a hard length limit is what turns a 200-character title into something that fits in a YNAB memo. The service returns plain JSON: order number, category ID, category name, up to three products. The memo becomes <code>Amazon Order &lt;nr&gt;: &lt;products&gt; - &lt;category&gt;<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"796\" src=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot.jpg\" class=\"wp-image-5967 sew-ai\" alt=\"Comic of a small robot running off with a stack of Amazon order papers, PayPal receipts flying around\" srcset=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot.jpg 800w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-300x300.jpg 300w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-150x150.jpg 150w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-768x764.jpg 768w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-100x100.jpg 100w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-302x300.jpg 302w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-02-bookkeeping-robot-120x120.jpg 120w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption>The agent: grabs the order mails so I don&#8217;t have to.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">What went wrong: the model ages, the code doesn&#8217;t<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A tool that calls an LLM has a dependency that expires. The categorizer broke in three ways, and each one failed <strong>silently<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Retired model.<\/strong> The running image still had a hard-coded Sonnet 4 ID. Anthropic retired it, every call returned 404, and the importer quietly fell back to &#8222;Amazon: \u2026&#8220;. No alert, just slowly worse bookkeeping.<\/li><li><strong>Follow-up timeout.<\/strong> The importer asked Claude <em>before<\/em> checking which transactions were already imported. Three minutes per import, nginx gave up after 90 seconds.<\/li><li><strong>Unbuildable Dockerfile.<\/strong> The base image no longer ships the Python version it asks for. Deploys now overlay the changed file on a tagged backup image.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Lesson: give the agent the list of suspects, and write down what you learned.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AGENTS.md: the most useful file in the project<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The biggest takeaway is not the fix, it is the runbook. Claude wrote an <code>AGENTS.md<\/code> on the Pi with &#8222;Rule #1: never rebuild these images&#8220; at the top, the container layout, the import flow, the deploy-by-overlay recipe, a debugging checklist and a dated history of what broke. Short pointer files in each sub-project link back to it, and a memory note on my laptop says &#8222;read the runbook first&#8220;. The next agent that touches this stack, or me in six months, starts with the knowledge instead of rediscovering it. One line from that checklist I now apply everywhere: check what is <em>in the image<\/em>, not only what is on disk.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because a silent failure was the real problem, the same session added a small monitoring card to the dashboard. The Pi has under a gigabyte of RAM, so Grafana was out. Instead: a tiny standard-library status service that scans container logs for errors, plus <strong>Dozzle<\/strong> as a log viewer, both as ready-made ARM images, because building Python containers on that Pi is pain. When I asked whether Dozzle was overkill, Claude argued for keeping it for context around an error, and when I claimed an environment variable would filter log lines, it checked Dozzle&#8217;s own help output instead of agreeing with me. It was right; the setting I actually wanted was the log level, now set to WARN.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"797\" src=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done.jpg\" class=\"wp-image-5968 sew-ai\" alt=\"Comic of a happy man at a laptop showing YNAB, fireworks in the background, a small robot sorting Amazon and PayPal folders\" srcset=\"https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done.jpg 800w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-300x300.jpg 300w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-150x150.jpg 150w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-768x765.jpg 768w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-100x100.jpg 100w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-301x300.jpg 301w, https:\/\/engel-wolf.com\/wp-content\/uploads\/tech-ynab-amazon-categorizer-03-bookkeeping-done-120x120.jpg 120w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><figcaption>After: two minutes a month, and the robot does the filing.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">If you want to build this<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Use e-mail as your integration layer.<\/strong> Forward order confirmations to one mailbox. IMAP search by order number is boring and works for every shop.<\/li><li><strong>Hand the model your real categories with IDs.<\/strong> The answer is then directly writable to YNAB.<\/li><li><strong>Put the model name in config, and alert on failure.<\/strong> Models get retired. A fallback that silently swallows errors will hide that for weeks.<\/li><li><strong>Only call the LLM for new transactions.<\/strong> It is slow and it costs money; check &#8222;already imported&#8220; first.<\/li><li><strong>Let the agent write a runbook after every fix.<\/strong> &#8222;Add what you found today into AGENTS.md&#8220; is the cheapest prompt with the longest payoff.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A monthly chore that used to eat 10 to 20 minutes, sometimes more, now takes about two. Not a spectacular demo, just a small agent that removes a repetitive annoyance from everyday life. And the second lesson, maybe the more important one: a small agent needs a small amount of care, and the best care is a runbook the next agent can read.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Order e-mails, IMAP, one Claude prompt and YNAB: how a ten-minute side project turned cryptic Amazon payments into readable, categorized transactions, why it silently broke when the model was retired, and why AGENTS.md is now the most important file on my Raspberry Pi. <a href=\"https:\/\/engel-wolf.com\/?p=5969\">Weiterlesen <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":5971,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"sew_ai_header":false,"footnotes":""},"categories":[1],"tags":[381],"class_list":["post-5969","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-allgemein","tag-technical"],"_links":{"self":[{"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/posts\/5969","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5969"}],"version-history":[{"count":1,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/posts\/5969\/revisions"}],"predecessor-version":[{"id":5970,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/posts\/5969\/revisions\/5970"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=\/wp\/v2\/media\/5971"}],"wp:attachment":[{"href":"https:\/\/engel-wolf.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5969"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5969"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/engel-wolf.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5969"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}