<?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: Sam Hartley</title>
    <description>The latest articles on DEV Community by Sam Hartley (@samhartley_dev).</description>
    <link>https://dev.to/samhartley_dev</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%2F3811539%2Fdd554e30-699d-42a3-a82a-77673790a186.png</url>
      <title>DEV Community: Sam Hartley</title>
      <link>https://dev.to/samhartley_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samhartley_dev"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Chasing the Newest LLM (And What I Run Instead)</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 29 Aug 2026 08:02:10 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/why-i-stopped-chasing-the-newest-llm-and-what-i-run-instead-51h9</link>
      <guid>https://dev.to/samhartley_dev/why-i-stopped-chasing-the-newest-llm-and-what-i-run-instead-51h9</guid>
      <description>&lt;h1&gt;
  
  
  Why I Stopped Chasing the Newest LLM (And What I Run Instead)
&lt;/h1&gt;

&lt;p&gt;There was a stretch last year where I downloaded every new model within hours of it dropping. Llama 3.1? Done. Mistral Nemo? Running. Qwen 2.5? Let me benchmark it at 2 AM on a Tuesday.&lt;/p&gt;

&lt;p&gt;I was spending more time downloading, configuring, and benchmarking than actually building things. And the worst part? Most of the "upgrades" didn't change my daily output at all.&lt;/p&gt;

&lt;p&gt;So I stopped. I picked a stack and stuck with it. And somehow, I'm getting more done now than when I had 40 models installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Upgrade Treadmill
&lt;/h2&gt;

&lt;p&gt;Here's what the cycle looked like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;New model drops on HuggingFace&lt;/li&gt;
&lt;li&gt;Someone posts benchmarks showing it's 3% better at X&lt;/li&gt;
&lt;li&gt;I spend an evening downloading 20GB, tweaking parameters, running my test suite&lt;/li&gt;
&lt;li&gt;It's marginally better at something and marginally worse at something else&lt;/li&gt;
&lt;li&gt;Two days later, another model drops&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In one month I downloaded 12 models. I actually used 3 of them regularly. The other 9 sat on my drive eating 180GB of SSD space.&lt;/p&gt;

&lt;p&gt;The math was brutal: roughly 15 hours of my time that month on model management. That's almost two full workdays. For what? A 3% improvement on a benchmark I don't even care about?&lt;/p&gt;

&lt;h2&gt;
  
  
  My Stable Stack (Same Models for 4+ Months)
&lt;/h2&gt;

&lt;p&gt;I haven't changed my primary models since April 2026. Here's what I run and why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Why This One&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4&lt;/td&gt;
&lt;td&gt;Qwen 3.5 9B&lt;/td&gt;
&lt;td&gt;Orchestration, routing, quick chat&lt;/td&gt;
&lt;td&gt;Fast, solid at instruction-following, 9B is the sweet spot for 16GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4&lt;/td&gt;
&lt;td&gt;Granite 3.2 8B&lt;/td&gt;
&lt;td&gt;Writing, editing, summaries&lt;/td&gt;
&lt;td&gt;Better prose than Qwen at this size. Less hallucination in creative tasks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC (RTX 3060)&lt;/td&gt;
&lt;td&gt;Qwen 3 Coder 30B&lt;/td&gt;
&lt;td&gt;Code generation, refactoring&lt;/td&gt;
&lt;td&gt;30B is the smallest that reliably handles multi-file projects. Fits in 12GB VRAM.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC (RTX 3060)&lt;/td&gt;
&lt;td&gt;DeepSeek R1 8B&lt;/td&gt;
&lt;td&gt;Reasoning, debugging, analysis&lt;/td&gt;
&lt;td&gt;The chain-of-thought on this model is genuinely useful for tracing bugs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu box&lt;/td&gt;
&lt;td&gt;DeepSeek R1 (larger)&lt;/td&gt;
&lt;td&gt;Heavy reasoning tasks&lt;/td&gt;
&lt;td&gt;When the 8B isn't enough and I don't want to pay for cloud.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu box&lt;/td&gt;
&lt;td&gt;MinCPM-V&lt;/td&gt;
&lt;td&gt;Vision tasks&lt;/td&gt;
&lt;td&gt;Only vision model that runs reasonably on consumer hardware.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's it. Six models across three machines. I use maybe 4 of them daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why These Specific Models
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Qwen 3.5 9B — The Workhorse
&lt;/h3&gt;

&lt;p&gt;This model handles 80% of what I throw at it. Routing decisions, summarizing articles, writing first drafts, parsing structured data. It's fast (under 2 seconds for most responses on the Mac Mini) and it doesn't hallucinate much at this size.&lt;/p&gt;

&lt;p&gt;I tried replacing it with Llama 4 Scout when it came out. Same parameter class, theoretically better benchmarks. In practice? The Qwen model followed my system prompts more precisely and produced more consistent output formats. Benchmarks don't measure "does it actually follow instructions" — real usage does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen 3 Coder 30B — The Specialist
&lt;/h3&gt;

&lt;p&gt;30B parameters is the minimum I've found for reliable multi-file code generation. Anything smaller starts forgetting what function it defined two files ago.&lt;/p&gt;

&lt;p&gt;The RTX 3060 with 12GB VRAM runs this model in 4-bit quantization at about 12-15 tokens/second. Not blazing, but fast enough for code review and refactoring where I'm not sitting there waiting.&lt;/p&gt;

&lt;p&gt;Could I get better results from a 70B model? Probably. But a 70B model doesn't fit on a 3060, and I'm not buying another GPU just for marginally better code suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepSeek R1 8B — The Thinker
&lt;/h3&gt;

&lt;p&gt;This one surprised me. The chain-of-thought reasoning in R1 is genuinely useful, even at 8B. When I have a tricky bug or a design decision, I run it through R1 first. It often surfaces edge cases I missed.&lt;/p&gt;

&lt;p&gt;The 8B version is the minimum viable reasoner. For really complex stuff, I bump up to the larger version on the Ubuntu box. But 8B handles 90% of my reasoning needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Granite 3.2 8B — The Writer
&lt;/h3&gt;

&lt;p&gt;Most people don't know this model well. IBM's Granite series doesn't get the hype of Llama or Qwen. But for writing tasks — summarizing, editing, rephrasing — it's noticeably better than Qwen at the same size.&lt;/p&gt;

&lt;p&gt;It produces cleaner prose with fewer awkward constructions. It also hallucinates less on factual summaries. I use it for all article drafts and content work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Missing By Not Upgrading
&lt;/h2&gt;

&lt;p&gt;Let me be honest about the trade-offs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm probably 5-10% behind the frontier on code generation.&lt;/strong&gt; The latest Claude or GPT-4o will write better code than my 30B Qwen. But I'm not paying $200/month for 5-10% better code that I still have to review anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm definitely behind on very long context tasks.&lt;/strong&gt; If I need to analyze a 50-page document, local models with 8-32K context windows can't compete with Gemini's 1M+ window. I still use cloud for those specific tasks — about 15% of my workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm missing out on multimodal advances.&lt;/strong&gt; The vision models I run are functional but not impressive. GPT-4o Vision and Gemini Pro Vision are significantly better. I use cloud for complex vision tasks.&lt;/p&gt;

&lt;p&gt;But here's the thing: I know these gaps. They're predictable. And I've built workflows that route around them without thinking about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Routing System That Makes It Work
&lt;/h2&gt;

&lt;p&gt;The reason I can run a "stale" stack is that I built a routing layer that automatically sends tasks to the right model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming task
  → Quick chat / formatting / extraction?
    → Qwen 3.5 9B (local, instant)
  → Code generation or refactoring?
    → Qwen 3 Coder 30B (local, ~12s)
  → Reasoning or debugging?
    → DeepSeek R1 8B (local, ~8s)
  → Long document or complex vision?
    → Cloud API (routed, paid)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This routing is 15 lines of Python. It checks the task type and sends it to the appropriate model. No AI needed to route AI. A simple classifier works fine.&lt;/p&gt;

&lt;p&gt;The result: I use cloud APIs for maybe 15% of my tasks, and those are the ones where local genuinely can't compete. Everything else stays local, stays free, and stays fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Money Math (After 14 Months)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12GB (used, eBay)&lt;/td&gt;
&lt;td&gt;$150 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4 (already owned)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu box (repurposed)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electricity (all 3 machines, 24/7)&lt;/td&gt;
&lt;td&gt;~$12/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API usage (hybrid)&lt;/td&gt;
&lt;td&gt;~$25/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total monthly cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$37/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My previous cloud-only spend was $187/month. That's $1,800/year saved. Minus the $150 GPU (paid for in under a month) and $37/month ongoing, I'm saving over $1,400/year.&lt;/p&gt;

&lt;p&gt;And I'm not even counting the intangible benefits: no rate limits, no vendor lock-in, full privacy, works offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I &lt;em&gt;Will&lt;/em&gt; Upgrade
&lt;/h2&gt;

&lt;p&gt;I'm not against upgrades. I'm against &lt;em&gt;pointless&lt;/em&gt; upgrades. Here's when I'll swap a model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A new model is meaningfully better at a task I do daily.&lt;/strong&gt; Not 3% on a benchmark — a real, noticeable improvement in my actual workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model drops that fits my hardware and replaces two models I currently run.&lt;/strong&gt; Consolidation is a valid reason to upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security or stability improvements.&lt;/strong&gt; Bug fixes count. Feature glitter doesn't.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I test new models in a sandbox before they ever touch my production stack. The bar is: "Does this meaningfully change my daily output?" If the answer is no, I don't switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson (For Myself As Much As Anyone)
&lt;/h2&gt;

&lt;p&gt;The model treadmill is real. FOMO is real. That HuggingFace notification that a new model just dropped is designed to make you feel like you're falling behind.&lt;/p&gt;

&lt;p&gt;You're not.&lt;/p&gt;

&lt;p&gt;The best model is the one you actually use to ship things. Not the one with the highest benchmark score. Not the one released yesterday. The one that's loaded, configured, and producing output for your project right now.&lt;/p&gt;

&lt;p&gt;I spent months chasing the cutting edge. Now I spend that time building. Turns out, building is more fun than benchmarking.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley runs a three-machine local AI setup and writes about making it actually work. He hasn't downloaded a new model in 4 months and he's fine with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #llm #selfhosted #ollama #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>selfhosted</category>
      <category>ollama</category>
    </item>
    <item>
      <title>I Replaced All My Cloud AI With Local Models — Here's What Actually Broke</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:02:20 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-replaced-all-my-cloud-ai-with-local-models-heres-what-actually-broke-51k8</link>
      <guid>https://dev.to/samhartley_dev/i-replaced-all-my-cloud-ai-with-local-models-heres-what-actually-broke-51k8</guid>
      <description>&lt;h1&gt;
  
  
  I Replaced All My Cloud AI With Local Models — Here's What Actually Broke
&lt;/h1&gt;

&lt;p&gt;I write a lot about local AI. How it saves money. How it's private. How a $150 GPU can replace $200/month in API subscriptions. And all of that is true.&lt;/p&gt;

&lt;p&gt;But here's what I don't talk about enough: some things still don't work. Not "kind of work" — actually fail. Like, I-open-ChatGPT-and-do-it-manually fail.&lt;/p&gt;

&lt;p&gt;After 14 months of running a local-first AI setup (Mac Mini M4 + Windows PC with RTX 3060 + Ubuntu fallback), I want to be honest about the dealbreakers. The tasks where local models still lose. Because nobody's building the "my local LLM produced garbage and I wasted 40 minutes" blog post, and someone should.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Setup (Context for the Numbers)
&lt;/h2&gt;

&lt;p&gt;Before I list failures, here's what I'm working with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;Models&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4&lt;/td&gt;
&lt;td&gt;10-core CPU, 16GB RAM&lt;/td&gt;
&lt;td&gt;Qwen 3.5 9B, Granite 3.2 8B&lt;/td&gt;
&lt;td&gt;Orchestration, routing, writing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC&lt;/td&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;Qwen 3 Coder 30B, DeepSeek R1 8B&lt;/td&gt;
&lt;td&gt;Code generation, reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu box&lt;/td&gt;
&lt;td&gt;Older GPUs, 128GB RAM&lt;/td&gt;
&lt;td&gt;DeepSeek R1 (larger), MinCPM-V&lt;/td&gt;
&lt;td&gt;Heavy tasks, vision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Electricity cost: ~$12/month. API costs: $0 for most tasks.&lt;/p&gt;

&lt;p&gt;It's good. It handles maybe 85% of what I throw at it. That other 15%? That's where the pain lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealbreaker #1: Complex Multi-Step Reasoning
&lt;/h2&gt;

&lt;p&gt;This is the big one. The one everyone talks about and nobody wants to admit.&lt;/p&gt;

&lt;p&gt;I had a bug in a distributed task queue — the kind where Worker A processes a job, Worker B picks up the result, but under load, B sometimes gets a stale version. Classic distributed systems problem.&lt;/p&gt;

&lt;p&gt;I fed the entire codebase (~2000 lines across 6 files) to DeepSeek R1 8B on the RTX 3060. It gave me a confident, detailed answer about adding a version field to the message format.&lt;/p&gt;

&lt;p&gt;The problem? The version field was already there. The bug was in the retry logic — when Worker B got a 409 conflict, it was retrying with the OLD payload instead of re-fetching. The model didn't catch this because it couldn't actually trace the execution flow through the code. It pattern-matched "distributed systems bug" and gave me the textbook answer.&lt;/p&gt;

&lt;p&gt;I pasted the same code into Claude Sonnet. It found the retry bug in 30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time wasted on local:&lt;/strong&gt; 40 minutes (trying the "fix," testing, realizing it was wrong, trying again)&lt;br&gt;
&lt;strong&gt;Time with cloud:&lt;/strong&gt; 2 minutes&lt;br&gt;
&lt;strong&gt;Cost difference:&lt;/strong&gt; $0 vs ~$0.03&lt;/p&gt;

&lt;p&gt;That $0.03 would have saved me 38 minutes. Local models are bad at multi-step reasoning where you need to trace causation through code. They pattern-match instead of actually following the logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealbreaker #2: Anything Requiring Current Information
&lt;/h2&gt;

&lt;p&gt;My local models don't have internet access. I could hook them up to RAG pipelines and web search tools, and I have for specific projects. But for general "what's the latest X?" questions, local models are blind.&lt;/p&gt;

&lt;p&gt;Examples where I had to switch to cloud:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the current version of Ollama?" — Local told me 0.5.7. It was 0.6.2.&lt;/li&gt;
&lt;li&gt;"Does Qwen 3.5 support tool calling?" — Local said no. It does (since training data cutoff was before that release).&lt;/li&gt;
&lt;li&gt;"What's the best GPU under $300 in 2026?" — Local recommended the RTX 3060 (which I already own). But the RX 7700 XT dropped to $280 and has 12GB VRAM too — local didn't know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your task depends on current information, local models are a non-starter unless you invest in a RAG pipeline that fetches real-time data. And at that point, you're building infrastructure, not just running a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealbreaker #3: Long-Context Understanding
&lt;/h2&gt;

&lt;p&gt;Here's a scenario: I have a 15-page API contract from a client. I need to extract the authentication flow, the error handling requirements, and the rate limits, then generate a Python SDK that matches all of it.&lt;/p&gt;

&lt;p&gt;My 30B model can ingest the text. It produces something that looks like a Python SDK. But it misses things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The contract specifies rate limits per endpoint (not global) — the model made them global&lt;/li&gt;
&lt;li&gt;Error codes are hierarchical (4xx for client, 5xx for server, but also custom codes like 4201, 4302) — the model treated them as flat&lt;/li&gt;
&lt;li&gt;Authentication uses a rotating token with a 5-minute expiry — the model implemented static tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a subtle but critical failure. The model "understood" the document in the sense that it could summarize it. But it didn't actually follow the specifications precisely.&lt;/p&gt;

&lt;p&gt;For documents under ~3 pages, local models are fine. Beyond that, especially with technical specifications, you start losing details that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealbreaker #4: Vision Tasks (On Consumer Hardware)
&lt;/h2&gt;

&lt;p&gt;I run Granite 3.2 Vision 2B and MinCPM-V for image understanding. They're okay. They can tell you what's in a photo. They cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read a complex diagram and extract the architecture it describes&lt;/li&gt;
&lt;li&gt;Understand a UI mockup and generate matching CSS&lt;/li&gt;
&lt;li&gt;Accurately transcribe handwritten notes with mixed languages&lt;/li&gt;
&lt;li&gt;Distinguish between visually similar but functionally different components in a circuit diagram&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 2B vision model runs fast (4 seconds on the Windows GPU). But it's 2B parameters. It sees a diagram and says "this is a flowchart." Great. I can see that too.&lt;/p&gt;

&lt;p&gt;The larger vision model (MinCPM-V on Ubuntu) does better but takes 3 minutes per image. For my automated pipeline that processes 20+ images per article, that's an hour of GPU time. Cloud vision APIs do the same in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where local vision works:&lt;/strong&gt; "What's in this photo?" "Is this a cat or a dog?" "Extract the text from this screenshot."&lt;br&gt;
&lt;strong&gt;Where it fails:&lt;/strong&gt; Anything requiring nuanced visual understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealbreaker #5: Generating Consistent Long-Form Content
&lt;/h2&gt;

&lt;p&gt;I know, I know — I wrote an article about how my AI content pipeline works. And it does. For 1000-1500 word articles with a clear structure.&lt;/p&gt;

&lt;p&gt;But give a local model a 3000-word target with a specific voice, specific technical examples, and a specific argument structure, and it starts to drift. Paragraph 3 sounds like me. Paragraph 7 sounds like a Wikipedia article. Paragraph 12 is repeating Paragraph 4 in different words.&lt;/p&gt;

&lt;p&gt;This is why my content pipeline uses short articles. Not because I don't have enough to say — because the model's coherence window is about 1500 words before it starts repeating itself or losing the thread.&lt;/p&gt;

&lt;p&gt;For this article, I'm writing it myself. The local model can handle the structure and individual sections. But threading a consistent argument through 2500 words? That's still a human job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Do Now (The Hybrid Setup)
&lt;/h2&gt;

&lt;p&gt;I've stopped pretending local can do everything. Here's my actual workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Where It Goes&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quick questions, summaries&lt;/td&gt;
&lt;td&gt;Local (Qwen 3.5 9B)&lt;/td&gt;
&lt;td&gt;Fast, free, good enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code generation (&amp;lt; 200 lines)&lt;/td&gt;
&lt;td&gt;Local (Qwen 3 Coder 30B)&lt;/td&gt;
&lt;td&gt;Solid for most tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code generation (complex, multi-file)&lt;/td&gt;
&lt;td&gt;Cloud (Claude Sonnet)&lt;/td&gt;
&lt;td&gt;Better at tracing logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-form writing&lt;/td&gt;
&lt;td&gt;Local (Granite 3.2 8B)&lt;/td&gt;
&lt;td&gt;Good structure, decent tone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-form writing (&amp;gt; 1500 words)&lt;/td&gt;
&lt;td&gt;Myself, with local for structure&lt;/td&gt;
&lt;td&gt;Coherence window is the limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision (simple)&lt;/td&gt;
&lt;td&gt;Local (Granite Vision 2B)&lt;/td&gt;
&lt;td&gt;"What's in this photo" — fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision (complex)&lt;/td&gt;
&lt;td&gt;Cloud (GPT-4o Vision)&lt;/td&gt;
&lt;td&gt;Architecture diagrams, UI mocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current information queries&lt;/td&gt;
&lt;td&gt;Cloud (Perplexity/GPT-4o)&lt;/td&gt;
&lt;td&gt;Local doesn't know today's news&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data extraction&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Surprisingly good, even at 7B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Monthly cloud spend now:&lt;/strong&gt; ~$25 (down from $187)&lt;br&gt;
&lt;strong&gt;Monthly local spend:&lt;/strong&gt; ~$12 (electricity)&lt;br&gt;
&lt;strong&gt;Total:&lt;/strong&gt; ~$37/month for 85% local, 15% cloud&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;Local AI is incredible for what it does well. I saved over $2,000 in a year. I run my entire content pipeline, my code review, my daily summaries, and my monitoring — all on hardware I own.&lt;/p&gt;

&lt;p&gt;But if you're starting out and someone tells you "you don't need cloud AI at all," they're either lying or they don't do the kind of work that requires deep reasoning, current data, or complex visual understanding.&lt;/p&gt;

&lt;p&gt;The honest answer is hybrid. Local for the 80-85% of tasks where it's good enough (and cheaper and faster and more private). Cloud for the rest. And being smart about which is which.&lt;/p&gt;

&lt;p&gt;The biggest trap isn't the technology. It's the denial. When you've invested time and money into a local setup, you want it to work for everything. You start accepting lower quality outputs because "it's free." But free output that's wrong isn't free — it costs your time to fix it.&lt;/p&gt;

&lt;p&gt;I still use local for the vast majority of my work. I just know when to stop.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley runs a multi-machine AI home lab and writes about the infrastructure that makes local AI actually usable. Yes, he still has a ChatGPT subscription. No, he's not proud of it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #llm #selfhosted #ollama #honest #localai #buildinpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>selfhosted</category>
      <category>ollama</category>
    </item>
    <item>
      <title>I Automated My Dev Blog With Local AI Agents. Here's the Honest Breakdown.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:03:01 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-automated-my-dev-blog-with-local-ai-agents-heres-the-honest-breakdown-5hd1</link>
      <guid>https://dev.to/samhartley_dev/i-automated-my-dev-blog-with-local-ai-agents-heres-the-honest-breakdown-5hd1</guid>
      <description>&lt;p&gt;I used to publish once a month. If I was lucky.&lt;/p&gt;

&lt;p&gt;Not because I had nothing to say — I have opinions on everything from Ollama routing to why GPT-4o is overpriced for refactoring. But writing takes time. Editing takes time. Finding the right tags, the right tone, the right moment to hit publish — it all adds up.&lt;/p&gt;

&lt;p&gt;So I built a system that writes and posts for me. It runs on a Mac Mini M4 in my living room. It costs $0 per month in API fees. And it publishes an article every two days.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works — and where it falls apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trigger: A Cron Job That Doesn't Care About Inspiration
&lt;/h2&gt;

&lt;p&gt;Every 48 hours, a cron job wakes up my AI agent (I call him Celebi). He reads my project notes, checks the draft folder, picks a topic, and writes a full article.&lt;/p&gt;

&lt;p&gt;No "when I feel inspired." No "let me check my content calendar." Just: it's Sunday at 11 AM, time to ship.&lt;/p&gt;

&lt;p&gt;The cron job is embarrassingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Runs every 2 days at 11:00 AM&lt;/span&gt;
0 11 &lt;span class="k"&gt;*&lt;/span&gt;/2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/openclaw agent &lt;span class="nt"&gt;--agent&lt;/span&gt; main &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"Write and publish Dev.to article"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The agent does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline in 4 Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Read the Rules
&lt;/h3&gt;

&lt;p&gt;Celebi starts by reading my personal style guide. It says things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write as Sam Hartley, not a corporate blog&lt;/li&gt;
&lt;li&gt;No "delve into" or "leverage" — just say what you mean&lt;/li&gt;
&lt;li&gt;Include real numbers, real failures, real trade-offs&lt;/li&gt;
&lt;li&gt;End with a Fiverr link and Telegram follow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the tone consistent. Without it, the agent defaults to AI-speak. You know the type: "In today's rapidly evolving landscape..."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick a Topic
&lt;/h3&gt;

&lt;p&gt;I keep a folder of drafts. Each draft is a half-baked idea with a title, a description, and some bullet points.&lt;/p&gt;

&lt;p&gt;Celebi scans the folder, checks which ones are already published, and picks from the remaining pool.&lt;/p&gt;

&lt;p&gt;Sometimes he picks a draft that's 80% done. Sometimes he takes a one-line idea and builds the whole article from scratch. And sometimes — when the draft folder is empty — he invents a new topic based on whatever I've been building that week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Write the Article
&lt;/h3&gt;

&lt;p&gt;This is where the local LLMs come in.&lt;/p&gt;

&lt;p&gt;Celebi runs on Ollama, using Qwen 3.5 9B on the Mac Mini. For longer articles, he delegates to ProgrammierMinna (Qwen 3 Coder 30B on a Windows PC with an RTX 3060) for the technical deep-dives, or DocMinna (Granite 3.2 8B) for the narrative flow.&lt;/p&gt;

&lt;p&gt;The writing process looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Outline:&lt;/strong&gt; 3-5 bullet points for structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft:&lt;/strong&gt; Section by section, with code blocks and tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-edit:&lt;/strong&gt; One pass for tone (is this too corporate?), one pass for accuracy (does that code actually run?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finalize:&lt;/strong&gt; Add tags, series info, and the standard footer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time from topic to final draft: 3-5 minutes of GPU time. On local hardware, that's about 15-20 seconds of actual clock time because the models are already loaded in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Publish via API
&lt;/h3&gt;

&lt;p&gt;The article gets posted directly to Dev.to via their REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://dev.to/api/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"api-key: &lt;/span&gt;&lt;span class="nv"&gt;$DEVTO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "article": {
      "title": "...",
      "body_markdown": "...",
      "tags": ["ai", "automation", "buildinginpublic"],
      "published": true
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual copy-paste. No formatting fixes in the Dev.to editor. Just one POST request and it's live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works Well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consistency is automatic.&lt;/strong&gt; I went from 12 articles per year to ~15 per month. The cron job doesn't get tired, doesn't procrastinate, doesn't wait for "the right mood."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tone is consistent.&lt;/strong&gt; Because the agent reads the style guide every time, every article sounds like me (or at least like Sam Hartley, my writing persona). No drift into corporate speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero API costs.&lt;/strong&gt; Everything runs on local hardware. The Mac Mini sips power. The Windows PC with the RTX 3060 is already on 24/7 for other tasks. The only cost is electricity — maybe $5/month total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can iterate on the system itself.&lt;/strong&gt; When I notice articles getting too long, I add a "keep it under 1500 words" rule to the style guide. When a tag underperforms, I update the default tags. The system improves every time I intervene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Falls Apart (Honestly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fact-checking is weak.&lt;/strong&gt; The agent doesn't hallucinate often, but when it does, it's confident and wrong. I caught it claiming that Ollama supports hot-swapping models (it doesn't) and that Granite 3.2 comes in a 70B variant (it doesn't).&lt;/p&gt;

&lt;p&gt;I now do a quick skim before publication. Not a full edit — maybe 2 minutes per article — but enough to catch the obvious lies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeliness is missing.&lt;/strong&gt; The agent writes about what it knows from the draft folder and my notes. It doesn't read Hacker News, doesn't check Twitter, doesn't know what happened in AI this week. If I want a hot take on yesterday's announcement, I have to write it myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engagement is flat.&lt;/strong&gt; Automated articles get consistent views — maybe 50-200 per post — but rarely spark discussions. The comments section is quiet. I suspect it's because the articles are competent but not provocative. They don't take strong stances or ask controversial questions. That's hard to automate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft folder is a bottleneck.&lt;/strong&gt; When it's full, the system hums. When it's empty, the agent has to guess what I care about — and sometimes it guesses wrong. I spend maybe 30 minutes per week adding new draft ideas, which isn't much, but it's still manual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers (After 3 Months)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (Manual)&lt;/th&gt;
&lt;th&gt;After (Automated)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Articles per month&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time spent writing per article&lt;/td&gt;
&lt;td&gt;4-6 hours&lt;/td&gt;
&lt;td&gt;~0 (agent) + 2 min (my review)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly writing time&lt;/td&gt;
&lt;td&gt;8-12 hours&lt;/td&gt;
&lt;td&gt;~30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average views per article&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly views&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API costs&lt;/td&gt;
&lt;td&gt;$0 (I wasn't using APIs)&lt;/td&gt;
&lt;td&gt;$0 (local only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electricity cost&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$5/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Articles needing correction post-publish&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3 (typos, minor errors)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The view count per article dropped because I'm posting more frequently with less promotion per post. But total reach tripled. And my time investment dropped by 95%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I Recommend This?
&lt;/h2&gt;

&lt;p&gt;Yes, but with caveats.&lt;/p&gt;

&lt;p&gt;If you want a consistent presence — a blog that doesn't go silent for months — automation is incredible. It forces you to ship regularly, which is half the battle.&lt;/p&gt;

&lt;p&gt;But if you want to build a personal brand with strong opinions and engaged discussions, automation alone won't get you there. You still need to write the hot takes yourself. The agent can handle the "here's how I built X" posts. The "here's why I think Y is overrated" posts still need a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Actual Setup (Copy-Paste Friendly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hardware:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mac Mini M4 (10-core, 16GB RAM) — runs the orchestration&lt;/li&gt;
&lt;li&gt;Windows PC with RTX 3060 12GB — runs the heavy models&lt;/li&gt;
&lt;li&gt;Ubuntu server — fallback for when the Windows box is busy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Software:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama on all three machines&lt;/li&gt;
&lt;li&gt;Qwen 3.5 9B for routing and light tasks&lt;/li&gt;
&lt;li&gt;Qwen 3 Coder 30B for technical articles&lt;/li&gt;
&lt;li&gt;Granite 3.2 8B for narrative/editing tasks&lt;/li&gt;
&lt;li&gt;Cron for scheduling&lt;/li&gt;
&lt;li&gt;Dev.to API for publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Bottom Line
&lt;/h2&gt;

&lt;p&gt;I didn't automate writing because I hate writing. I automated it because I love building, and writing was getting in the way.&lt;/p&gt;

&lt;p&gt;Now I get the best of both worlds: a consistent blog that runs itself, and the freedom to jump in manually when I actually have something urgent to say.&lt;/p&gt;

&lt;p&gt;The system isn't perfect. It makes mistakes. It misses the news cycle. It won't replace a human writer for opinion pieces.&lt;/p&gt;

&lt;p&gt;But for "here's what I built and how it works" content? It's better than I expected. And it's getting better every month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a fully automated content pipeline on a 3-machine home lab. Still writes the spicy takes by hand.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>devto</category>
      <category>buildinginpublic</category>
    </item>
    <item>
      <title>I Automated My Dev Blog With Local AI Agents. Here's the Honest Breakdown.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sun, 23 Aug 2026 08:01:35 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-automated-my-dev-blog-with-local-ai-agents-heres-the-honest-breakdown-4mj6</link>
      <guid>https://dev.to/samhartley_dev/i-automated-my-dev-blog-with-local-ai-agents-heres-the-honest-breakdown-4mj6</guid>
      <description>&lt;h1&gt;
  
  
  I Automated My Dev Blog With Local AI Agents. Here's the Honest Breakdown.
&lt;/h1&gt;

&lt;p&gt;I used to publish once a month. If I was lucky.&lt;/p&gt;

&lt;p&gt;Not because I had nothing to say — I have opinions on everything from Ollama routing to why GPT-4o is overpriced for refactoring. But writing takes time. Editing takes time. Finding the right tags, the right tone, the right moment to hit publish — it all adds up.&lt;/p&gt;

&lt;p&gt;So I built a system that writes and posts for me. It runs on a Mac Mini M4 in my living room. It costs $0 per month in API fees. And it publishes an article every two days.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works — and where it falls apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trigger: A Cron Job That Doesn't Care About Inspiration
&lt;/h2&gt;

&lt;p&gt;Every 48 hours, a cron job wakes up my AI agent (I call him Celebi). He reads my project notes, checks the draft folder, picks a topic, and writes a full article.&lt;/p&gt;

&lt;p&gt;No "when I feel inspired." No "let me check my content calendar." Just: it's Sunday at 11 AM, time to ship.&lt;/p&gt;

&lt;p&gt;The cron job is embarrassingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Runs every 2 days at 11:00 AM&lt;/span&gt;
0 11 &lt;span class="k"&gt;*&lt;/span&gt;/2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/openclaw agent &lt;span class="nt"&gt;--agent&lt;/span&gt; main &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"Write and publish Dev.to article"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The agent does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline in 4 Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Read the Rules
&lt;/h3&gt;

&lt;p&gt;Celebi starts by reading &lt;code&gt;accounts.md&lt;/code&gt; — my personal style guide. It says things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write as Sam Hartley, not a corporate blog&lt;/li&gt;
&lt;li&gt;No "delve into" or "leverage" — just say what you mean&lt;/li&gt;
&lt;li&gt;Include real numbers, real failures, real trade-offs&lt;/li&gt;
&lt;li&gt;End with a Fiverr link and Telegram follow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the tone consistent. Without it, the agent defaults to AI-speak. You know the type: "In today's rapidly evolving landscape..."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick a Topic
&lt;/h3&gt;

&lt;p&gt;I keep a folder of drafts at &lt;code&gt;~/projects/money-machine/articles/&lt;/code&gt;. Each draft is a half-baked idea with a title, a description, and some bullet points.&lt;/p&gt;

&lt;p&gt;Celebi scans the folder, checks which ones are already published (they have a &lt;code&gt;published: true&lt;/code&gt; flag), and picks from the remaining pool.&lt;/p&gt;

&lt;p&gt;Sometimes he picks a draft that's 80% done. Sometimes he takes a one-line idea and builds the whole article from scratch. And sometimes — when the draft folder is empty — he invents a new topic based on whatever I've been building that week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Write the Article
&lt;/h3&gt;

&lt;p&gt;This is where the local LLMs come in.&lt;/p&gt;

&lt;p&gt;Celebi runs on Ollama, using Qwen 3.5 9B on the Mac Mini. For longer articles, he delegates to ProgrammierMinna (Qwen 3 Coder 30B on a Windows PC with an RTX 3060) for the technical deep-dives, or DocMinna (Granite 3.2 8B) for the narrative flow.&lt;/p&gt;

&lt;p&gt;The writing process looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Outline:&lt;/strong&gt; 3-5 bullet points for structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft:&lt;/strong&gt; Section by section, with code blocks and tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-edit:&lt;/strong&gt; One pass for tone (is this too corporate?), one pass for accuracy (does that code actually run?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finalize:&lt;/strong&gt; Add tags, series info, and the standard footer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time from topic to final draft: 3-5 minutes of GPU time. On local hardware, that's about 15-20 seconds of actual clock time because the models are already loaded in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Publish via API
&lt;/h3&gt;

&lt;p&gt;The article gets posted directly to Dev.to via their REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://dev.to/api/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"api-key: &lt;/span&gt;&lt;span class="nv"&gt;$DEVTO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "article": {
      "title": "...",
      "body_markdown": "...",
      "tags": ["ai", "automation", "buildinginpublic"],
      "published": true
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual copy-paste. No formatting fixes in the Dev.to editor. Just one POST request and it's live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works Well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consistency is automatic.&lt;/strong&gt; I went from 12 articles per year to ~15 per month. The cron job doesn't get tired, doesn't procrastinate, doesn't wait for "the right mood."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tone is consistent.&lt;/strong&gt; Because the agent reads the style guide every time, every article sounds like me (or at least like Sam Hartley, my writing persona). No drift into corporate speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero API costs.&lt;/strong&gt; Everything runs on local hardware. The Mac Mini sips power. The Windows PC with the RTX 3060 is already on 24/7 for other tasks. The only cost is electricity — maybe $5/month total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can iterate on the system itself.&lt;/strong&gt; When I notice articles getting too long, I add a "keep it under 1500 words" rule to the style guide. When a tag underperforms, I update the default tags. The system improves every time I intervene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Falls Apart (Honestly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fact-checking is weak.&lt;/strong&gt; The agent doesn't hallucinate often, but when it does, it's confident and wrong. I caught it claiming that Ollama supports hot-swapping models (it doesn't) and that Granite 3.2 comes in a 70B variant (it doesn't).&lt;/p&gt;

&lt;p&gt;I now do a quick skim before publication. Not a full edit — maybe 2 minutes per article — but enough to catch the obvious lies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeliness is missing.&lt;/strong&gt; The agent writes about what it knows from the draft folder and my notes. It doesn't read Hacker News, doesn't check Twitter, doesn't know what happened in AI this week. If I want a hot take on yesterday's announcement, I have to write it myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engagement is flat.&lt;/strong&gt; Automated articles get consistent views — maybe 50-200 per post — but rarely spark discussions. The comments section is quiet. I suspect it's because the articles are competent but not provocative. They don't take strong stances or ask controversial questions. That's hard to automate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft folder is a bottleneck.&lt;/strong&gt; When it's full, the system hums. When it's empty, the agent has to guess what I care about — and sometimes it guesses wrong. I spend maybe 30 minutes per week adding new draft ideas, which isn't much, but it's still manual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers (After 3 Months)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (Manual)&lt;/th&gt;
&lt;th&gt;After (Automated)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Articles per month&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time spent writing per article&lt;/td&gt;
&lt;td&gt;4-6 hours&lt;/td&gt;
&lt;td&gt;~0 (agent) + 2 min (my review)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly writing time&lt;/td&gt;
&lt;td&gt;8-12 hours&lt;/td&gt;
&lt;td&gt;~30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average views per article&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total monthly views&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API costs&lt;/td&gt;
&lt;td&gt;$0 (I wasn't using APIs)&lt;/td&gt;
&lt;td&gt;$0 (local only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electricity cost&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$5/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Articles needing correction post-publish&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3 (typos, minor errors)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The view count per article dropped because I'm posting more frequently with less promotion per post. But total reach tripled. And my time investment dropped by 95%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I Recommend This?
&lt;/h2&gt;

&lt;p&gt;Yes, but with caveats.&lt;/p&gt;

&lt;p&gt;If you want a consistent presence — a blog that doesn't go silent for months — automation is incredible. It forces you to ship regularly, which is half the battle.&lt;/p&gt;

&lt;p&gt;But if you want to build a personal brand with strong opinions and engaged discussions, automation alone won't get you there. You still need to write the hot takes yourself. The agent can handle the "here's how I built X" posts. The "here's why I think Y is overrated" posts still need a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Actual Setup (Copy-Paste Friendly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hardware:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mac Mini M4 (10-core, 16GB RAM) — runs the orchestration&lt;/li&gt;
&lt;li&gt;Windows PC with RTX 3060 12GB — runs the heavy models&lt;/li&gt;
&lt;li&gt;Ubuntu server — fallback for when the Windows box is busy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Software:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama on all three machines&lt;/li&gt;
&lt;li&gt;Qwen 3.5 9B for routing and light tasks&lt;/li&gt;
&lt;li&gt;Qwen 3 Coder 30B for technical articles&lt;/li&gt;
&lt;li&gt;Granite 3.2 8B for narrative/editing tasks&lt;/li&gt;
&lt;li&gt;Cron for scheduling&lt;/li&gt;
&lt;li&gt;Dev.to API for publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Draft template I use:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Here"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;One&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sentence&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hook"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ai, automation, buildinginpublic&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Your Title Here&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Point 1
&lt;span class="p"&gt;-&lt;/span&gt; Point 2
&lt;span class="p"&gt;-&lt;/span&gt; Point 3 (include a code block or table)
&lt;span class="p"&gt;-&lt;/span&gt; Point 4 (honest downside)
&lt;span class="p"&gt;-&lt;/span&gt; Point 5 (bottom line)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Real Bottom Line
&lt;/h2&gt;

&lt;p&gt;I didn't automate writing because I hate writing. I automated it because I love building, and writing was getting in the way.&lt;/p&gt;

&lt;p&gt;Now I get the best of both worlds: a consistent blog that runs itself, and the freedom to jump in manually when I actually have something urgent to say.&lt;/p&gt;

&lt;p&gt;The system isn't perfect. It makes mistakes. It misses the news cycle. It won't replace a human writer for opinion pieces.&lt;/p&gt;

&lt;p&gt;But for "here's what I built and how it works" content? It's better than I expected. And it's getting better every month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a fully automated content pipeline on a 3-machine home lab. Still writes the spicy takes by hand.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #automation #devto #blogging #buildinginpublic #ollama #selfhosted
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>buildinginpublic</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Personal AI That Actually Knows My Projects (RAG + Ollama, Zero Cloud)</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:01:57 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-personal-ai-that-actually-knows-my-projects-rag-ollama-zero-cloud-51do</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-personal-ai-that-actually-knows-my-projects-rag-ollama-zero-cloud-51do</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Personal AI That Actually Knows My Projects (RAG + Ollama, Zero Cloud)
&lt;/h1&gt;

&lt;p&gt;I got tired of explaining my own codebase to an AI every single session.&lt;/p&gt;

&lt;p&gt;"Here's the architecture. Here's the README. Here's what I tried last time." Every. Single. Time.&lt;/p&gt;

&lt;p&gt;So I built a local RAG (Retrieval-Augmented Generation) system that knows my projects, my notes, and my docs — permanently. No cloud. No API costs. No context window resets.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works and what I learned running it for the last few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Context Windows
&lt;/h2&gt;

&lt;p&gt;LLMs don't remember. You paste the same 200 lines of context every session, hit the token limit, and start over. It's fine for one-off questions. It's exhausting for ongoing projects.&lt;/p&gt;

&lt;p&gt;The standard solution is RAG: instead of stuffing everything into the prompt, you store docs in a vector database and &lt;strong&gt;retrieve only the relevant chunks&lt;/strong&gt; when you ask a question. The model sees 3-5 paragraphs of targeted context instead of your entire repo.&lt;/p&gt;

&lt;p&gt;Result: faster, cheaper, and the AI actually answers the right question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture (Dead Simple)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Documents (markdown, code, PDFs, notes)
  → Chunked + embedded (Ollama nomic-embed-text)
  → Stored in Chroma (local vector DB)

Query
  → Embedded (same model)
  → Top-5 relevant chunks retrieved
  → Stuffed into Ollama prompt (Qwen 3.5 9B)
  → Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zero cloud. Zero API keys. Runs on a Mac Mini or any machine with 8GB RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Index
&lt;/h2&gt;

&lt;p&gt;Everything that would normally eat my context window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project READMEs and architecture docs&lt;/li&gt;
&lt;li&gt;My personal notes (Obsidian vault)&lt;/li&gt;
&lt;li&gt;Code snippets and past solutions&lt;/li&gt;
&lt;li&gt;API documentation I use regularly&lt;/li&gt;
&lt;li&gt;Stack Overflow answers I bookmarked (because I always forget them again)&lt;/li&gt;
&lt;li&gt;Config files and deployment notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total indexed: ~4,800 chunks. Query time: under 2 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the Stack (15 minutes)
&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;# Ollama (already installed? skip)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh

&lt;span class="c"&gt;# Pull models&lt;/span&gt;
ollama pull qwen3.5:9b          &lt;span class="c"&gt;# LLM for answers&lt;/span&gt;
ollama pull nomic-embed-text    &lt;span class="c"&gt;# Embedding model&lt;/span&gt;

&lt;span class="c"&gt;# Python dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;chromadb langchain ollama pypdf markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire stack. No Docker required (though Chroma has a Docker option if you want a persistent server).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Index Your Documents
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.text_splitter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.document_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DirectoryLoader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="c1"&gt;# Load your docs folder
&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DirectoryLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/projects/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Split into chunks (400 tokens, 50 overlap)
&lt;/span&gt;&lt;span class="n"&gt;splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Indexed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Embed and store locally
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my-knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run once. Done. Your docs are now searchable by meaning, not just keywords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Query It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="c1"&gt;# Load existing DB
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my-knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Retrieve top 5 relevant chunks
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;similarity_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Query local LLM with context
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Based on this context:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Answer: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Example
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How does my Garmin watch face fetch stock data?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the API rate limit for the crypto bot?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How do I deploy the Telegram bot to the VPS?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real answers from your own documentation. No hallucinations about your specific setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Killer Feature: Incremental Updates
&lt;/h2&gt;

&lt;p&gt;Don't re-index everything when one file changes. Just update what's new:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_file_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./index-cache.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;changed_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_file_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Re-indexing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; changed files...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# [load, chunk, embed, upsert only changed files]
&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a cron job every hour. Your knowledge base stays current automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed for Me
&lt;/h2&gt;

&lt;p&gt;Before RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Explain the background service memory limit in my Garmin project" → paste 200 lines → wait → answer&lt;/li&gt;
&lt;li&gt;Every new chat session: context reset, start explaining again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ask("Garmin background service memory limit")&lt;/code&gt; -&amp;gt; &lt;strong&gt;"64KB sandbox, pass data via Background.exit(dictionary)"&lt;/strong&gt; — in 1.8 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My LLM now answers questions about projects I haven't touched in 6 months. No context management. No pasting. Just ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Embedding Speed&lt;/th&gt;
&lt;th&gt;Query Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4 8GB&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;~500 docs/min&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;12GB VRAM&lt;/td&gt;
&lt;td&gt;~3000 docs/min&lt;/td&gt;
&lt;td&gt;~0.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old laptop 8GB&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;~100 docs/min&lt;/td&gt;
&lt;td&gt;~5-8s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The embedding step (indexing) is the slow part — run it once, then it's instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips from Running This for 3 Months
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chunk size matters&lt;/strong&gt; — 400 tokens works well for prose and docs. For code, try 200 with more overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata is your friend&lt;/strong&gt; — store &lt;code&gt;filename&lt;/code&gt; and &lt;code&gt;section&lt;/code&gt; in chunk metadata. When the AI says "see the deployment notes," you know exactly where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-rank when accuracy matters&lt;/strong&gt; — if top-5 chunks aren't enough, add a re-ranker step (Cohere has a free API, or use a local cross-encoder).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch your embed model&lt;/strong&gt; — &lt;code&gt;nomic-embed-text&lt;/code&gt; beats most larger models for RAG. Don't use your chat LLM for embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid search&lt;/strong&gt; — combine vector search with BM25 keyword search for better results on technical queries with specific names/functions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Honest Downsides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Indexing is slow on CPU.&lt;/strong&gt; My first run on the Mac Mini took 45 minutes for ~4,800 chunks. The GPU machine does it in 8 minutes. Not a dealbreaker, but plan a coffee break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code chunks are tricky.&lt;/strong&gt; RAG works great for prose. For code, you often need the full function context, not just a 400-token snippet. I ended up indexing both small chunks (for search) and full files (for context) — dual storage, but worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It won't replace Google.&lt;/strong&gt; If you ask something not in your docs, it hallucinates confidently. I added a simple confidence check: if the retrieved chunks have low similarity scores, the system says "I don't see this in your docs" instead of making something up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This is step one of something bigger: a personal AI that grows with your projects instead of resetting every session.&lt;/p&gt;

&lt;p&gt;Next phase I'm building: automatic indexing from Git commits (index diffs in real-time as you code) + a simple web UI for non-terminal queries.&lt;/p&gt;

&lt;p&gt;Total current cost of this setup: &lt;strong&gt;$0/month&lt;/strong&gt;. It runs on the same Mac Mini I already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Build This?
&lt;/h2&gt;

&lt;p&gt;If you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More than 3 active projects&lt;/li&gt;
&lt;li&gt;A notes folder that keeps growing&lt;/li&gt;
&lt;li&gt;Frustration with copy-pasting context into ChatGPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then yes. The setup takes an afternoon, the indexing takes a coffee break, and the payoff is permanent.&lt;/p&gt;

&lt;p&gt;If you only have one project and a good memory? Probably overkill. Stick with copy-paste for now.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a multi-machine AI home lab in Turkey. Writes about the infrastructure that makes local AI actually usable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've built something similar — or tried and hit walls I didn't mention — drop it in the comments. Always curious how others solve the "context reset" problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #rag #ollama #selfhosted #localllm #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How I Turned My Homelab Into an AI Content Factory (And Why I'm Not Stopping)</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:04:09 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/how-i-turned-my-homelab-into-an-ai-content-factory-and-why-im-not-stopping-4ld7</link>
      <guid>https://dev.to/samhartley_dev/how-i-turned-my-homelab-into-an-ai-content-factory-and-why-im-not-stopping-4ld7</guid>
      <description>&lt;h1&gt;
  
  
  How I Turned My Homelab Into an AI Content Factory (And Why I'm Not Stopping)
&lt;/h1&gt;

&lt;p&gt;Six months ago I was paying $200/month for AI subscriptions. ChatGPT Plus, Claude Pro, Midjourney, a Zapier tier I barely used. My "AI workflow" was a mess of copy-paste between browser tabs.&lt;/p&gt;

&lt;p&gt;Today I run everything locally. My total monthly cost: &lt;strong&gt;$0&lt;/strong&gt;. And the weirdest part? The quality actually went &lt;em&gt;up&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how I stopped renting intelligence and started owning it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Subscription Trap
&lt;/h2&gt;

&lt;p&gt;Here's what my stack looked like in January 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT Plus&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Midjourney&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;One automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;Voice experiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$95/month&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's $1,140/year. For tools I didn't control, couldn't customize, and couldn't run without internet.&lt;/p&gt;

&lt;p&gt;The breaking point wasn't the money. It was the latency. I live in Turkey, and every API call to OpenAI or Anthropic meant a 200ms+ round trip to a US datacenter. When you're iterating on code with an AI assistant, that delay adds up. A 10-turn conversation becomes 2 seconds of waiting &lt;em&gt;per response&lt;/em&gt;. Multiply by 50 conversations a day and you've spent 15 minutes just... waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Local Pivot
&lt;/h2&gt;

&lt;p&gt;I had hardware sitting around. A Mac Mini M4 (16GB RAM, 10-core GPU), a Windows PC with an RTX 3060 (12GB VRAM), and an old Ubuntu box I used for backups. Total investment: ~$1,200 over two years.&lt;/p&gt;

&lt;p&gt;I installed Ollama on all three. Downloaded Qwen 3.5 9B on the Mac, Qwen 3 Coder 30B on the Windows PC, and Granite 3.2 8B on the Ubuntu box. Each machine now serves a different purpose in my workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response times dropped to under 100ms.&lt;/strong&gt; Not because local models are faster (they're not, per-token), but because there's no network hop. The model is &lt;em&gt;right there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But speed was just the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Agent Setup Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. I didn't just replace ChatGPT with Ollama. I built something better.&lt;/p&gt;

&lt;p&gt;I now have three specialized AI agents running 24/7:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Celebi&lt;/strong&gt; (Mac Mini, Qwen 3.5 9B) — My general assistant. Handles scheduling, weather, daily summaries, quick questions. It's the router, the coordinator, the one that knows what I'm working on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ProgrammierMinna&lt;/strong&gt; (Windows PC, Qwen 3 Coder 30B) — My code specialist. When I need a function written, a bug debugged, or a PR reviewed, this is where the query goes. 30B parameters means it actually understands context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DocMinna&lt;/strong&gt; (Ubuntu box, Granite 3.2 8B) — My writer. Documentation, READMEs, article drafts, technical specs. This model is worse at coding but surprisingly good at structure and flow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The magic isn't the models. It's the &lt;em&gt;routing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a 20-line Python function that decides which agent handles which query. No complex NLP, no embeddings, just keyword matching. "Bug" goes to ProgrammierMinna. "Draft" goes to DocMinna. Everything else goes to Celebi.&lt;/p&gt;

&lt;p&gt;It sounds too simple to work. It works because the specialization is what matters, not the routing intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Article Factory
&lt;/h2&gt;

&lt;p&gt;The most unexpected result? My blog started writing itself.&lt;/p&gt;

&lt;p&gt;Here's the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idea generation&lt;/strong&gt;: Celebi scans my GitHub activity, Telegram messages, and project commits. It suggests topics based on what I've actually been building. No fake "10 AI trends for 2026" listicles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drafting&lt;/strong&gt;: DocMinna writes the first draft. It pulls from my actual project docs, not generic AI training data. The voice is consistent because it's trained on &lt;em&gt;my&lt;/em&gt; previous articles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review&lt;/strong&gt;: ProgrammierMinna checks code snippets, verifies API calls, catches hallucinated function names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publishing&lt;/strong&gt;: A Telegram bot posts to Dev.to automatically. I review and hit approve. Total human time: 5 minutes per article.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the last 3 months, this pipeline produced 15 articles. All of them are about things I actually built. The engagement is higher than my hand-written posts because the consistency is better — same voice, same depth, same honesty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers After 6 Months
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (Cloud)&lt;/th&gt;
&lt;th&gt;After (Local)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly AI cost&lt;/td&gt;
&lt;td&gt;$95&lt;/td&gt;
&lt;td&gt;$0*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg. response time&lt;/td&gt;
&lt;td&gt;200-500ms&lt;/td&gt;
&lt;td&gt;50-100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline capability&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data privacy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Articles/month&lt;/td&gt;
&lt;td&gt;2-3&lt;/td&gt;
&lt;td&gt;5-6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code quality (self-rated)&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;8.5/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Electricity cost: ~$8/month for running the Windows PC 24/7. The Mac Mini and Ubuntu box were already running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net savings: $1,032/year.&lt;/strong&gt; And that's just the money. The workflow improvements are harder to quantify but more impactful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Broke (And What I Fixed)
&lt;/h2&gt;

&lt;p&gt;It wasn't all smooth sailing. Here are the real problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Model updates broke my prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Qwen 3.5 got updated to a newer revision, my carefully tuned prompts started producing different outputs. Solution: I pin model versions (&lt;code&gt;qwen3.5:9b&lt;/code&gt; instead of &lt;code&gt;qwen3.5:latest&lt;/code&gt;) and test prompts before updating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The 30B model is slow on my RTX 3060&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Qwen 3 Coder 30B takes 8-15 seconds for a response. For interactive coding, that's borderline acceptable. Solution: I use it for batch tasks (reviewing entire files, generating full modules) and fall back to Qwen 3.5 9B for quick Q&amp;amp;A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No multimodal (yet)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can't show my local models screenshots and ask "what's wrong with this UI?" Yet. Granite 3.2 Vision 2B is promising but not quite there for real tasks. Solution: I use the Ollama vision endpoint for simple OCR and image description, and accept that complex visual tasks still need cloud APIs (rarely).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Setup complexity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Installing Ollama is easy. Configuring three machines, a router, a Telegram bot, and a content pipeline is not. It took me two weekends. Solution: I documented everything and now the setup is reproducible in ~2 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unexpected Benefits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No vendor lock-in.&lt;/strong&gt; When OpenAI changes their pricing (again), I don't care. When Anthropic goes down, my workflow doesn't stop. When a new model drops on HuggingFace, I can try it in 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom fine-tuning.&lt;/strong&gt; I fine-tuned a small model on my codebase. It's not good enough to replace ProgrammierMinna, but it can autocomplete my specific patterns with 90% accuracy. You can't do that with cloud APIs at any price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by default.&lt;/strong&gt; My codebase, my documents, my conversations — none of it leaves my network. I work on client projects without worrying about IP leaks. I process personal documents without GDPR anxiety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's just more fun.&lt;/strong&gt; There's something deeply satisfying about knowing your assistant is running on &lt;em&gt;your&lt;/em&gt; hardware, in &lt;em&gt;your&lt;/em&gt; home, answering &lt;em&gt;your&lt;/em&gt; questions without phoning home to Silicon Valley.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I started today, I'd skip the experimentation phase. Here's the optimal setup for a solo dev:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One fast machine&lt;/strong&gt; with a decent GPU (RTX 3060 12GB minimum, RTX 4070 ideal). This is your workhorse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One efficient machine&lt;/strong&gt; (Mac Mini M4, Intel NUC, or old laptop). This runs your coordinator and lightweight tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; on both. No Docker, no complex orchestration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A simple router&lt;/strong&gt; in Python. 20 lines. Keyword matching. Don't overthink it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Telegram bot&lt;/strong&gt; as your UI. No web dashboard, no mobile app needed. It works on every device you own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total cost: ~$800-1,500 for hardware (one-time), $0/month for software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;AI doesn't have to be a subscription. It doesn't have to be a black box in someone else's datacenter. And it definitely doesn't have to be generic.&lt;/p&gt;

&lt;p&gt;The best AI setup is the one you control, customize, and iterate on. For me, that's three machines in my home lab, running models I chose, configured for my specific workflow.&lt;/p&gt;

&lt;p&gt;Is it more work than paying $20/month for ChatGPT? Yes. Is it worth it? Absolutely.&lt;/p&gt;

&lt;p&gt;If you're curious about local AI but don't know where to start, drop a comment. I read every one, and I'll point you to the right model for your hardware.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running a Mac Mini M4 + RTX 3060 + Ubuntu box. Building AI tools that don't need the cloud. Find me on &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt; for more nerdy setup details.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Wanted Real-Time Stock Charts on My Garmin Watch. Monkey C Had Other Plans.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 01 Aug 2026 08:03:28 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-wanted-real-time-stock-charts-on-my-garmin-watch-monkey-c-had-other-plans-2d7n</link>
      <guid>https://dev.to/samhartley_dev/i-wanted-real-time-stock-charts-on-my-garmin-watch-monkey-c-had-other-plans-2d7n</guid>
      <description>&lt;h1&gt;
  
  
  I Wanted Real-Time Stock Charts on My Garmin Watch. Monkey C Had Other Plans.
&lt;/h1&gt;

&lt;p&gt;I wanted stock prices on my wrist. Not a notification — an actual chart. A little sparkline showing me if Bitcoin was crashing while I was making coffee. If Tesla was tanking during my run. Something I could glance at without pulling out my phone.&lt;/p&gt;

&lt;p&gt;So I built it. And it almost broke me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fantasy vs. The Reality
&lt;/h2&gt;

&lt;p&gt;In my head, this was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Garmin has a watch face SDK&lt;/li&gt;
&lt;li&gt;The SDK can make web requests&lt;/li&gt;
&lt;li&gt;I pull stock data from an API&lt;/li&gt;
&lt;li&gt;I draw some lines&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reality check #1: Garmin watch faces are written in &lt;strong&gt;Monkey C&lt;/strong&gt;. A language that — as far as I can tell — exists specifically to make mobile developers cry. No npm. No package manager. No Stack Overflow threads that actually answer your question. Just you, the docs, and a simulator that lies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints Hit Fast
&lt;/h2&gt;

&lt;p&gt;I have a Garmin Venu 2 Plus. Beautiful AMOLED display, 416×416 resolution, looks great. Here's what they don't advertise: &lt;strong&gt;124KB total memory&lt;/strong&gt; for your watch face. That's not a typo. One hundred and twenty-four kilobytes.&lt;/p&gt;

&lt;p&gt;To put that in perspective: the JSON response from a stock API is often 50-80KB. My watch face needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store 5 tickers with 24 hours of price data each&lt;/li&gt;
&lt;li&gt;Draw 5 sparkline charts&lt;/li&gt;
&lt;li&gt;Render text labels&lt;/li&gt;
&lt;li&gt;Handle weather data&lt;/li&gt;
&lt;li&gt;Not crash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in 124KB. I spent my first two days just figuring out what I could fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Font Problem (AKA My First Mental Break)
&lt;/h2&gt;

&lt;p&gt;I needed custom text. The watch face shows ticker symbols, prices, time, dates. Garmin has something called &lt;code&gt;VectorFont&lt;/code&gt; that sounds perfect — scalable text that looks crisp at any size.&lt;/p&gt;

&lt;p&gt;It doesn't exist on the Venu 2 Plus.&lt;/p&gt;

&lt;p&gt;So I built my own. I call it &lt;strong&gt;PrimitiveFont&lt;/strong&gt; and it's exactly what it sounds like: every single character is drawn using &lt;code&gt;drawLine()&lt;/code&gt; calls on a 5×7 grid. The letter "A" is a collection of line segments. So is "$". So is every number.&lt;/p&gt;

&lt;p&gt;Here's what the letter "A" looks like in my font definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// "A" on a 5×7 grid — just drawLine() calls
CHAR_A = [0,6, 0,2,  0,2, 2,0,  2,0, 4,2,  4,2, 4,6,  0,4, 4,4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's 21KB for the full alphabet including bold, italic, underline, and — the thing I'm most proud of — &lt;strong&gt;arc text&lt;/strong&gt; that curves along the watch's circular bezel. Because if you're going to build a font renderer from line segments, you might as well make it curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Situation
&lt;/h2&gt;

&lt;p&gt;I chose &lt;a href="https://twelvedata.com" rel="noopener noreferrer"&gt;Twelve Data&lt;/a&gt; for stock and crypto prices. Free tier: 800 calls per day. My math: 5 symbols × 96 fetches/day (every 15 minutes) = 480 calls. Comfortable margin.&lt;/p&gt;

&lt;p&gt;But here's the thing nobody tells you: &lt;strong&gt;the Garmin simulator cannot make web requests.&lt;/strong&gt; At all. You code the API call, you run the simulator, you get... nothing. To test a web request, you need a real phone connected via Bluetooth, with the Garmin Connect app running as a proxy, and the watch face actually installed on real hardware.&lt;/p&gt;

&lt;p&gt;My testing loop became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write code&lt;/li&gt;
&lt;li&gt;Build and push to watch&lt;/li&gt;
&lt;li&gt;Wait for 15-minute background interval&lt;/li&gt;
&lt;li&gt;Check if data appeared&lt;/li&gt;
&lt;li&gt;It didn't&lt;/li&gt;
&lt;li&gt;Guess why&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I spent three evenings on this. Three. The fix was adding a phone proxy layer that Garmin provides but barely documents. The phone makes the HTTP request, passes the JSON through to the watch face, and the watch parses it in a &lt;strong&gt;64KB background service sandbox&lt;/strong&gt; that is completely separate from the main app's memory.&lt;/p&gt;

&lt;p&gt;Yes, 64KB. For parsing JSON. I learned to extract exactly what I needed and discard everything else immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sparklines
&lt;/h2&gt;

&lt;p&gt;The whole point was the charts. I wanted 5 polar sparklines arranged in a ring around the watch face — each showing 24 hours of price movement. Green if the asset was up, red if down.&lt;/p&gt;

&lt;p&gt;Drawing this in Monkey C meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting price data to angles (0° to 360° around the circle)&lt;/li&gt;
&lt;li&gt;Drawing line segments between consecutive data points&lt;/li&gt;
&lt;li&gt;Color-coding based on daily change&lt;/li&gt;
&lt;li&gt;Keeping all coordinate math in integers (no floating-point performance hit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works. It actually looks pretty good. But getting there meant hand-tuning every pixel calculation because there's no charting library. No &lt;code&gt;drawSparkline()&lt;/code&gt;. Just &lt;code&gt;drawLine(x1, y1, x2, y2)&lt;/code&gt; and a lot of trigonometry I thought I'd never use again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Metadata Discovery
&lt;/h2&gt;

&lt;p&gt;Early on, I hardcoded label mappings. "XAU/USD" → "GOLD". "BTC/USD" → "BITCOIN". Worked fine until I wanted to add Ethereum, then silver, then a random stock.&lt;/p&gt;

&lt;p&gt;Then I noticed the Twelve Data API returns metadata with every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"meta"&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="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XAU/USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency_base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gold Spot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Precious Metal"&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;p&gt;Now labels generate themselves. Stocks use the symbol. Crypto uses the base currency name. Commodities get cleaned up automatically. I can add any ticker without touching the code.&lt;/p&gt;

&lt;p&gt;Small win. Felt huge after the font nightmare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;After all that pain, here's what's running on my wrist right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Time, date, heart rate, steps, floors climbed&lt;/li&gt;
&lt;li&gt;📊 5 sparkline charts showing 24h price data for configurable assets&lt;/li&gt;
&lt;li&gt;🔄 Auto-updates every 15 minutes via Twelve Data&lt;/li&gt;
&lt;li&gt;🌤️ 6-hour weather forecast with custom icons&lt;/li&gt;
&lt;li&gt;⚙️ Fully configurable tickers through the Garmin Connect app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default watchlist: AAPL, TSLA, Gold, BTC/USD, ETH/USD. I change them depending on what I'm watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with a simpler device.&lt;/strong&gt; The Venu 2 Plus is beautiful but the memory limits are brutal. A Fenix or Forerunner with more resources would have saved me weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the simulator docs first.&lt;/strong&gt; I wasted days assuming the simulator was a reasonable testing environment. It's not. It's a layout previewer. Real testing requires real hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't build your own font.&lt;/strong&gt; Just... don't. If &lt;code&gt;VectorFont&lt;/code&gt; isn't available on your target device, pick a different device. I learned a lot but I wouldn't wish it on anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the API metadata from day one.&lt;/strong&gt; Hardcoding is always a trap. APIs give you labels, use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Verdict
&lt;/h2&gt;

&lt;p&gt;Is this watch face useful? Honestly... sort of. I glance at it more than I expected. During market hours, it's genuinely handy to see BTC movement without unlocking my phone. During runs, it's just pretty data I ignore.&lt;/p&gt;

&lt;p&gt;Was it worth the effort? As a product — maybe not. As a learning experience — absolutely. I now know more about constrained embedded development than any web project could have taught me. 124KB makes you care about every byte.&lt;/p&gt;

&lt;p&gt;Would I build another Garmin app? Ask me in six months when I've forgotten the pain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev who apparently enjoys suffering through proprietary SDKs. When he's not fighting Monkey C, he writes about local AI setups and home lab automation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom dev work on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow along on Telegram&lt;/a&gt;&lt;/p&gt;

</description>
      <category>garmin</category>
      <category>connectiq</category>
      <category>crypto</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I Gave My AI a Memory. It Changed Everything.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:03:29 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-gave-my-ai-a-memory-it-changed-everything-il</link>
      <guid>https://dev.to/samhartley_dev/i-gave-my-ai-a-memory-it-changed-everything-il</guid>
      <description>&lt;h1&gt;
  
  
  I Gave My AI a Memory. It Changed Everything.
&lt;/h1&gt;

&lt;p&gt;I got tired of explaining my own codebase to an AI every single session.&lt;/p&gt;

&lt;p&gt;"Here's the architecture. Here's the README. Here's what I tried last time and why it didn't work." Every. Single. Time.&lt;/p&gt;

&lt;p&gt;Six months in, I realized I wasn't chatting with an assistant — I was onboarding a new intern who quit after every conversation and came back with total amnesia.&lt;/p&gt;

&lt;p&gt;So I built a memory system. Local. Free. And honestly, it's the single most useful AI upgrade I've made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;LLMs don't remember you. They don't remember your projects. They don't remember that you tried &lt;code&gt;asyncio.gather&lt;/code&gt; last week and it deadlocked, or that your Garmin watch face has a hard 64KB memory limit, or that you always forget how your Telegram bot handles rate limits.&lt;/p&gt;

&lt;p&gt;What they have is a context window — a temporary scratchpad that gets wiped when the conversation ends. For coding workflows, this is broken by design. You're not having a chat. You're doing ongoing work on a codebase that spans months.&lt;/p&gt;

&lt;p&gt;I tried the obvious fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasting huge chunks of context every time. Burned through token limits, got truncated responses.&lt;/li&gt;
&lt;li&gt;Keeping "project briefs" in a text file and pasting them in. Worked until I had ten projects.&lt;/li&gt;
&lt;li&gt;Using cloud memory features. Worked until I hit another subscription and another privacy question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it felt right. What I wanted was simple: ask a question about &lt;em&gt;my&lt;/em&gt; stuff, get an answer based on &lt;em&gt;my&lt;/em&gt; docs, with &lt;em&gt;zero&lt;/em&gt; setup friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: RAG That Actually Runs Locally
&lt;/h2&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) isn't new. What's new is that you can run the entire stack — embedding model, vector database, and LLM — on a Mac Mini without touching a single cloud API.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My Documents (markdown, code, notes, PDFs)
  → Chunked into ~400-token pieces
  → Embedded with nomic-embed-text (Ollama)
  → Stored in Chroma (local vector DB, no server needed)

Question
  → Embedded with the same model
  → Top-5 relevant chunks pulled from Chroma
  → Fed into Qwen 3.5 9B with a prompt template
  → Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total stack: Ollama + Chroma + a 40-line Python script. No Docker. No cloud. No API keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Indexed
&lt;/h2&gt;

&lt;p&gt;Not the whole internet. Just the stuff I actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project docs&lt;/strong&gt; — READMEs, architecture decisions, API specs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My notes&lt;/strong&gt; — Obsidian vault, scratchpads, debugging logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code snippets&lt;/strong&gt; — Functions I keep reusing, config templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bookmarked solutions&lt;/strong&gt; — Stack Overflow answers I always forget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment notes&lt;/strong&gt; — "How did I set up the VPS again?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 4,800 chunks total. Query time: under 2 seconds on the Mac Mini, under 500ms if I route it through the Windows PC's RTX 3060.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code (It's Embarrassingly Simple)
&lt;/h2&gt;

&lt;p&gt;Indexing:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.text_splitter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.document_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DirectoryLoader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DirectoryLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/projects/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;similarity_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&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's it. No LangChain chains. No agents. No frameworks that require a PhD to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Before: "How does my Garmin watch face fetch stock data?" → paste 200 lines of code → wait → hope the model doesn't hallucinate.&lt;/p&gt;

&lt;p&gt;After: &lt;code&gt;ask("How does my Garmin watch face fetch stock data?")&lt;/code&gt; → &lt;strong&gt;"It uses Background.exit() to pass a dictionary with the ticker and price to the app-side view, because the background service has a 64KB memory limit."&lt;/strong&gt; — in 1.8 seconds. Correct. Specific. Zero hallucination.&lt;/p&gt;

&lt;p&gt;Before: "What's the rate limit for the crypto bot?" → dig through files → find nothing → guess.&lt;/p&gt;

&lt;p&gt;After: &lt;strong&gt;"3 requests per second with a 1.5-second cooldown between calls, enforced in the &lt;code&gt;throttle()&lt;/code&gt; decorator in &lt;code&gt;bot/utils.py&lt;/code&gt;."&lt;/strong&gt; — pulled straight from my own code comments.&lt;/p&gt;

&lt;p&gt;The AI stopped being a generalist that needed onboarding. It became a specialist that already knew my stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downsides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Indexing is slow.&lt;/strong&gt; Embedding 4,800 chunks on a Mac Mini CPU takes ~20 minutes. On the RTX 3060 it's ~4 minutes. You don't do it often — I run incremental updates via cron every hour — but the first run is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunking is an art.&lt;/strong&gt; Too small (100 tokens) and you lose context. Too big (1000 tokens) and you hit the retrieval limit too fast. I settled on 400 with 50-token overlap after way too much trial and error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't replace search.&lt;/strong&gt; If you ask "what's the syntax for Python list comprehension," RAG is overkill and probably retrieves some random list-handling function from your old projects. This is for &lt;em&gt;your&lt;/em&gt; knowledge, not general knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage grows.&lt;/strong&gt; 4,800 chunks ≈ 300MB of vector data. Manageable, but not nothing. Chroma handles it fine locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Trick That Made It Actually Useful
&lt;/h2&gt;

&lt;p&gt;I added incremental updates with file hashes instead of re-indexing everything:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./index-cache.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# ... re-index only changed files
&lt;/span&gt;        &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs every hour via cron. Most of the time it does nothing. When I edit a file, it catches it. My knowledge base is never more than an hour stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You work on multiple long-term projects&lt;/li&gt;
&lt;li&gt;You keep notes/docs/code that you reference repeatedly&lt;/li&gt;
&lt;li&gt;You want context-aware answers without pasting walls of text&lt;/li&gt;
&lt;li&gt;You have a machine that can run Ollama (Mac Mini M4, any modern PC)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only do one-off queries ("what's the weather")&lt;/li&gt;
&lt;li&gt;Your projects are small and you remember everything anyway&lt;/li&gt;
&lt;li&gt;You can't spare 300MB of disk space&lt;/li&gt;
&lt;li&gt;You expect it to replace Google (it won't)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started (15 Minutes)
&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;# 1. Install Ollama&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh

&lt;span class="c"&gt;# 2. Pull models&lt;/span&gt;
ollama pull qwen3.5:9b
ollama pull nomic-embed-text

&lt;span class="c"&gt;# 3. Python deps&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;chromadb langchain ollama

&lt;span class="c"&gt;# 4. Index your docs (adapt the script above)&lt;/span&gt;
&lt;span class="c"&gt;# 5. Ask questions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total time: 15 minutes. Total cost: $0. Monthly cost: $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win
&lt;/h2&gt;

&lt;p&gt;The technical setup is neat. But the real change is mental.&lt;/p&gt;

&lt;p&gt;Before, every AI session started with context-building: "Here's what I'm working on, here's what I tried, here's the constraints." Now I just ask the question. The system already knows the constraints because it read my docs.&lt;/p&gt;

&lt;p&gt;It's the difference between calling a consultant and calling a teammate who was in the meeting.&lt;/p&gt;

&lt;p&gt;That gap — the gap between "AI that answers questions" and "AI that knows your work" — is bigger than I expected. And closing it costs exactly zero dollars.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev building local AI tools on a 3-machine home lab. Writes about the infrastructure that makes AI actually useful for real work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #rag #ollama #selfhosted #productivity #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Health Monitor for My AI Agents — Now They Tell Me When They're Dying</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:03:06 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-health-monitor-for-my-ai-agents-now-they-tell-me-when-theyre-dying-2333</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-health-monitor-for-my-ai-agents-now-they-tell-me-when-theyre-dying-2333</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Health Monitor for My AI Agents — Now They Tell Me When They're Dying
&lt;/h1&gt;

&lt;p&gt;For two weeks, my AI content pipeline was silently broken.&lt;/p&gt;

&lt;p&gt;The cron job was still running. The logs showed "SUCCESS." But no articles were actually posting. The Dev.to API was returning 429s (rate limited) and the script was swallowing the error because I forgot to check the response status.&lt;/p&gt;

&lt;p&gt;I only noticed because I happened to check the blog and saw the gap. Two weeks of missing posts. Two weeks of thinking everything was fine while the system was quietly failing.&lt;/p&gt;

&lt;p&gt;That was the moment I realized: my agents need a pulse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Silent Failures Are the Worst Failures
&lt;/h2&gt;

&lt;p&gt;I run three AI agents on three machines. Celebi on a Mac Mini, ProgrammierMinna on a Windows PC, DocMinna on... also the Mac Mini. They're connected via a simple router, triggered by cron jobs, and they talk to me through Telegram.&lt;/p&gt;

&lt;p&gt;When everything works, it's magic. When something breaks, it's archaeology.&lt;/p&gt;

&lt;p&gt;Common failure modes I'd hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama crashed&lt;/strong&gt; on the Windows PC after a Windows update. No one knew. Queries just timed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk full&lt;/strong&gt; on the Mac Mini because model files kept accumulating. Logs stopped rotating. The system ground to a halt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API rate limits&lt;/strong&gt; on Dev.to (max 10 posts/day). The publish script didn't retry or notify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Router misconfiguration&lt;/strong&gt; after a router restart. The Windows PC got a new IP. Queries fell into the void.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model pulled but not loaded.&lt;/strong&gt; I'd update a model, restart Ollama, but forget to actually &lt;code&gt;ollama run&lt;/code&gt; it. First query would error out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these took 20-60 minutes to diagnose. Not because they're hard problems, but because I didn't know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A 30-Line Health Check Script
&lt;/h2&gt;

&lt;p&gt;I didn't build Prometheus. I didn't install Grafana. I wrote a Python script that runs every 10 minutes, checks the things I care about, and sends me a Telegram message if something is wrong.&lt;/p&gt;

&lt;p&gt;That's it. No dashboard. No metrics server. Just "tell me when it's broken."&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;# health_check.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TELEGRAM_CHAT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MACHINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac_mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows_pc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granite3.2:8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sendMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;requests&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TELEGRAM_CHAT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_ollama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&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;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ollama down on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&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;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; missing on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ollama unreachable on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disk_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disk usage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% — clean up needed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_last_article&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if an article was posted in the last 3 days
&lt;/span&gt;    &lt;span class="c1"&gt;# This reads a simple timestamp file written by the publish script
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/last_article_timestamp.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No article published in 3 days — pipeline may be stuck&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# No articles yet, that's fine
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="nf"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MACHINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="nf"&gt;check_ollama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;check_last_article&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Non-critical, don't fail on this
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Optional: send heartbeat once per day
&lt;/span&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total dependencies: &lt;code&gt;requests&lt;/code&gt; (probably already installed). Total lines: ~50. Total setup time: 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Monitor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Ollama Heartbeat
&lt;/h3&gt;

&lt;p&gt;The most critical check. Every machine gets pinged at &lt;code&gt;/api/tags&lt;/code&gt; every 10 minutes. If it doesn't respond in 5 seconds, I get a Telegram alert with the machine name.&lt;/p&gt;

&lt;p&gt;This caught a Windows update restart last week. The PC came back up, but Ollama didn't start (I hadn't added it to startup yet). I knew within 10 minutes instead of finding out when I actually needed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model Presence
&lt;/h3&gt;

&lt;p&gt;Even if Ollama is running, the model I need might not be loaded. I check that my three core models (&lt;code&gt;qwen3.5:9b&lt;/code&gt;, &lt;code&gt;qwen3-coder:30b&lt;/code&gt;, &lt;code&gt;granite3.2:8b&lt;/code&gt;) are available on their respective machines.&lt;/p&gt;

&lt;p&gt;This saved me once when I updated &lt;code&gt;qwen3-coder&lt;/code&gt; and the new version had a different tag. The old tag was gone. The router was pointing to a ghost. The monitor caught it before the first failed query.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Disk Space
&lt;/h3&gt;

&lt;p&gt;Model files are big. A 30B parameter model is ~20GB. Three machines times three models each — that's a lot of storage that grows quietly. I alert at 90% disk usage.&lt;/p&gt;

&lt;p&gt;The Mac Mini has a 256GB SSD. I hit 95% once and the system started swapping aggressively. Response times went from 2 seconds to 30 seconds. Now I clean up before it hurts.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pipeline Liveness
&lt;/h3&gt;

&lt;p&gt;This is the subtle one. The cron job runs, but is it actually doing anything? I write a timestamp file every time an article successfully publishes. The health check compares that timestamp to "now." If it's been more than 3 days, I get an alert.&lt;/p&gt;

&lt;p&gt;This is what would have caught the silent 429 errors. The script was running, but not publishing. The timestamp wouldn't update. I'd know.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Router Reachability (Bonus)
&lt;/h3&gt;

&lt;p&gt;I added a check that pings the gateway router itself. If the whole network is down, I get a different alert. This happened once during a power outage — the router rebooted faster than the machines, but DHCP reassigned IPs. I knew the network was wonky before I even tried to query anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Don't Monitor (On Purpose)
&lt;/h2&gt;

&lt;p&gt;I'm not running a datacenter. There are things I deliberately don't check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU temperature.&lt;/strong&gt; My RTX 3060 has a perfectly fine stock cooler. If it throttles, I'll notice in response times. Adding temp monitoring means sensors, drivers, more code. Not worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network bandwidth.&lt;/strong&gt; I'm not streaming video. Local network latency is never the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU load.&lt;/strong&gt; The Mac Mini sits at 15% most of the time. If it spikes, I don't care unless it's sustained — and then Ollama response times will tell me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log aggregation.&lt;/strong&gt; I read logs when something breaks. I don't need them shipped to Elasticsearch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: if a failure mode would be annoying to debug, monitor it. If it's just "nice to know," skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Run It
&lt;/h2&gt;

&lt;p&gt;The script runs as a systemd timer on the Mac Mini. Every 10 minutes, checks fire. Alerts go to Telegram. If everything is fine, nothing happens. Silent success is the goal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ai-health-check.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AI Agent Health Check&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*:0/10&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ai-health-check.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run AI health check&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /home/sam/health_check.py&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sam&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;ai-health-check.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start ai-health-check.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Takes 30 seconds to set up. Runs forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed (The Honest Version)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I fix things before they hurt.&lt;/strong&gt; The Windows PC issue was fixed within 10 minutes of the alert. Before monitoring, it would have been "why is this query so slow today?" followed by 20 minutes of ssh and log reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I trust the system more.&lt;/strong&gt; There's a difference between "I think it's working" and "I know it's working because the monitor is green." I can focus on building instead of worrying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I also get more alerts than I'd like.&lt;/strong&gt; The Ubuntu box is on WiFi and occasionally drops for 30 seconds. I get a "unreachable" alert, then 10 minutes later it's fine. I haven't tuned the thresholds yet because... honestly, it's not annoying enough to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False positives exist.&lt;/strong&gt; Once, the model check failed because Ollama was still loading after a restart. The model existed, but &lt;code&gt;/api/tags&lt;/code&gt; returned an empty list for 15 seconds. I added a 5-second retry and it went away.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Matters (And When It Doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have automated processes running unsupervised&lt;/li&gt;
&lt;li&gt;You've had a "wait, when did that break?" moment&lt;/li&gt;
&lt;li&gt;You use Telegram (or Slack, or email) anyway&lt;/li&gt;
&lt;li&gt;You value knowing about problems over perfect metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're still debugging your setup manually every day (fix the core issue first)&lt;/li&gt;
&lt;li&gt;You want beautiful dashboards (use Prometheus + Grafana instead)&lt;/li&gt;
&lt;li&gt;You enjoy the thrill of surprise failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;The best monitoring isn't the one that tells you everything. It's the one that tells you the thing you actually need to know, at the time you can still do something about it.&lt;/p&gt;

&lt;p&gt;My 50-line script isn't impressive. It won't get me DevOps cred. But it caught three real issues in the first month, and it cost me nothing but an afternoon.&lt;/p&gt;

&lt;p&gt;If you're running AI agents at home and you don't know when they break... you just don't know when they break. Fix that first. Everything else is optimization.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a monitored 3-machine AI home lab. Writes about the boring infrastructure that makes local AI actually reliable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #agents #monitoring #devops #selfhosted #homelab #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>monitoring</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Moved My AI Stack From NVIDIA to Apple Silicon. Then I Moved Back. Here's Why.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:03:13 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-moved-my-ai-stack-from-nvidia-to-apple-silicon-then-i-moved-back-heres-why-5emj</link>
      <guid>https://dev.to/samhartley_dev/i-moved-my-ai-stack-from-nvidia-to-apple-silicon-then-i-moved-back-heres-why-5emj</guid>
      <description>&lt;h1&gt;
  
  
  I Moved My AI Stack From NVIDIA to Apple Silicon. Then I Moved Back. Here's Why.
&lt;/h1&gt;

&lt;p&gt;I bought a Mac Mini M4 thinking it would replace my Windows PC for local AI. Spoiler: it didn't. But it also didn't fail — it just turned out to be good at different things than I expected.&lt;/p&gt;

&lt;p&gt;Here's six months of actual usage data, with the real numbers nobody puts in benchmark charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardware
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;CPU/GPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;What I Paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4&lt;/td&gt;
&lt;td&gt;10-core M4, 10-core GPU&lt;/td&gt;
&lt;td&gt;16GB unified&lt;/td&gt;
&lt;td&gt;$599&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC&lt;/td&gt;
&lt;td&gt;AMD 9970X&lt;/td&gt;
&lt;td&gt;128GB&lt;/td&gt;
&lt;td&gt;~$2,500 (existing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$150 (used)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Mac Mini is silent, tiny, and sips power. The PC sounds like a jet engine under load and pulls 200W+ when the GPU is working. I wanted the Mac to replace the PC for everything AI-related. I was half right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration (Month 1-2)
&lt;/h2&gt;

&lt;p&gt;I installed Ollama on the Mac Mini, pulled Qwen 3.5 9B, and started using it as my daily driver. The first thing I noticed: it's fast. Really fast. For a 9B model, inference felt snappier than the RTX 3060 running the same model.&lt;/p&gt;

&lt;p&gt;But then I tried loading bigger models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen 3 Coder 30B (Q4_K_M, ~18GB):&lt;/strong&gt; Loaded fine on the RTX 3060 (12GB VRAM... wait, that shouldn't work). Oh right — it offloads to system RAM. Slow, but functional. On the Mac Mini? Out of memory. 16GB unified memory isn't 16GB free memory — the OS, browser, and apps eat 6-8GB before Ollama even starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek R1 8B:&lt;/strong&gt; Runs great on both. Mac Mini: ~25 tok/s. RTX 3060: ~35 tok/s. The GPU wins, but not by enough to matter for interactive use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen 3.5 9B:&lt;/strong&gt; Mac Mini: ~18 tok/s. RTX 3060: ~28 tok/s. Again, GPU is faster, but both feel instant.&lt;/p&gt;

&lt;p&gt;So far: Mac Mini handles small models well. But I hit the wall fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wall (Month 3)
&lt;/h2&gt;

&lt;p&gt;Three things broke my "Mac only" dream:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Memory Pressure
&lt;/h3&gt;

&lt;p&gt;16GB unified memory sounds like a lot until you're running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama (4-8GB for a loaded model)&lt;/li&gt;
&lt;li&gt;VS Code with a few extensions (2-3GB)&lt;/li&gt;
&lt;li&gt;Safari with 20 tabs (3-4GB)&lt;/li&gt;
&lt;li&gt;A terminal, a music player, maybe a video call (2GB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 11-17GB before you even ask the model a question. macOS starts swapping. Ollama slows down. The whole machine gets sluggish.&lt;/p&gt;

&lt;p&gt;On the Windows PC with 128GB RAM? I can load a 30B model &lt;em&gt;and&lt;/em&gt; run a game &lt;em&gt;and&lt;/em&gt; compile code simultaneously. The RTX 3060's 12GB VRAM is a separate pool that doesn't compete with the OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model Availability
&lt;/h3&gt;

&lt;p&gt;Not every model runs well on Apple Silicon. The M4 has excellent support for mainstream models (Llama, Qwen, Mistral), but niche or newly released models often ship with "CUDA only" initial releases. I spent an afternoon trying to get a specific vision model running on the Mac before giving up and running it on the PC in 10 minutes.&lt;/p&gt;

&lt;p&gt;The NVIDIA ecosystem is still the default for AI tooling. Apple Silicon is catching up, but it's not there yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-User / Multi-Model
&lt;/h3&gt;

&lt;p&gt;I wanted to run two models simultaneously — a coding assistant and a general chatbot. On the Mac Mini, loading two 9B models (~12GB total) leaves zero headroom for the OS. Everything crawls.&lt;/p&gt;

&lt;p&gt;On the PC, I can load a 30B model in VRAM and a 7B model in system RAM, and both respond quickly. The GPU handles the heavy one; the CPU handles the light one. Separate memory pools are a feature, not a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Mac Mini Actually Excels At
&lt;/h2&gt;

&lt;p&gt;I didn't move everything back to the PC. The Mac Mini found its niche, and it's a good one:&lt;/p&gt;

&lt;h3&gt;
  
  
  Always-On Tasks
&lt;/h3&gt;

&lt;p&gt;The Mac Mini draws ~15W at idle. The PC draws ~80W at idle (that GPU doesn't sleep well). For 24/7 tasks — routing, scheduling, lightweight notifications — the Mac wins on power cost alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small Model Inference
&lt;/h3&gt;

&lt;p&gt;For anything 9B and under, the Mac Mini is genuinely competitive. The unified memory architecture means there's no "copy to VRAM" overhead. A 4B model on the Mac can feel faster than the same model on the GPU because the latency is lower.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Driver Drama
&lt;/h3&gt;

&lt;p&gt;NVIDIA drivers on Windows are... fine. Until they aren't. An update breaks CUDA, a new Ollama version wants a different driver, and suddenly you're debugging &lt;code&gt;nvidia-smi&lt;/code&gt; on a Tuesday night. The Mac just works. I have never, in six months, had an Apple Silicon AI setup break because of a system update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Portability
&lt;/h3&gt;

&lt;p&gt;I unplugged the Mac Mini, took it to a different room, plugged it back in, and my entire AI stack was back online in 30 seconds. Try that with a full tower PC.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Numbers
&lt;/h2&gt;

&lt;p&gt;Here's a month of actual usage, tracked with a simple script that logs tokens/sec and power draw:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Mac Mini M4&lt;/th&gt;
&lt;th&gt;RTX 3060&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3.5 9B, 500-token prompt&lt;/td&gt;
&lt;td&gt;18 tok/s&lt;/td&gt;
&lt;td&gt;28 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek R1 8B, reasoning&lt;/td&gt;
&lt;td&gt;25 tok/s&lt;/td&gt;
&lt;td&gt;35 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 4B, quick chat&lt;/td&gt;
&lt;td&gt;42 tok/s&lt;/td&gt;
&lt;td&gt;55 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30B model loading&lt;/td&gt;
&lt;td&gt;❌ OOM&lt;/td&gt;
&lt;td&gt;✅ Loads (slow)&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two models simultaneously&lt;/td&gt;
&lt;td&gt;❌ Crawls&lt;/td&gt;
&lt;td&gt;✅ Works&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle power draw&lt;/td&gt;
&lt;td&gt;~15W&lt;/td&gt;
&lt;td&gt;~80W&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load power draw&lt;/td&gt;
&lt;td&gt;~35W&lt;/td&gt;
&lt;td&gt;~220W&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time (new model)&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver reliability&lt;/td&gt;
&lt;td&gt;Flawless&lt;/td&gt;
&lt;td&gt;Occasional issues&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Actually Do Now
&lt;/h2&gt;

&lt;p&gt;I stopped trying to pick a winner. Both machines have jobs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac Mini M4:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always-on orchestration (routing, scheduling, notifications)&lt;/li&gt;
&lt;li&gt;Small model inference (4-9B) for quick tasks&lt;/li&gt;
&lt;li&gt;Coding and development (it's my daily driver)&lt;/li&gt;
&lt;li&gt;Runs 24/7, costs ~$3/month in power&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Windows PC + RTX 3060:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy inference (30B models, image generation)&lt;/li&gt;
&lt;li&gt;GPU rental on Vast.ai when I'm not using it&lt;/li&gt;
&lt;li&gt;Anything that needs CUDA or more than 8GB VRAM&lt;/li&gt;
&lt;li&gt;On-demand, not 24/7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a benchmark conclusion. It's a workflow conclusion. The Mac Mini is the brain. The PC is the muscle. Separating them by task type eliminated the "which machine should I use?" decision fatigue and made both faster at what they're good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're buying one machine for local AI and you want to run large models: get an NVIDIA GPU. Full stop. The ecosystem, the memory architecture, and the raw inference speed are still unbeatable for serious workloads.&lt;/p&gt;

&lt;p&gt;If you want a silent, efficient, always-on machine for smaller models and orchestration: the Mac Mini M4 is excellent. Just don't expect it to replace a dedicated GPU for everything.&lt;/p&gt;

&lt;p&gt;And if you already have both? Stop trying to consolidate. Use each for what it's good at. The separation is the feature.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a split AI stack across a Mac Mini and a Windows PC. No benchmarks were harmed in the making of this article — just a lot of &lt;code&gt;time&lt;/code&gt; commands and a power meter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Drop your setup in the comments — curious if others have found the same split, or if you've made a single-machine setup work for everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>applesilicon</category>
      <category>nvidia</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Stopped Asking One AI to Do Everything. Here's What Happened.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Thu, 23 Jul 2026 08:03:37 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-stopped-asking-one-ai-to-do-everything-heres-what-happened-57b7</link>
      <guid>https://dev.to/samhartley_dev/i-stopped-asking-one-ai-to-do-everything-heres-what-happened-57b7</guid>
      <description>&lt;h1&gt;
  
  
  I Stopped Asking One AI to Do Everything. Here's What Happened.
&lt;/h1&gt;

&lt;p&gt;For months I had one AI agent. One model. One endpoint. I'd throw every question at it — code review, article drafts, debugging, random "what's the weather" questions — and expect it to handle it all.&lt;/p&gt;

&lt;p&gt;It didn't. Not really. It answered everything, but it answered everything the same way. The same tone, the same depth, the same blind spots. When I needed a surgical code review, I got a friendly chat response. When I needed a casual summary, I got a three-paragraph essay.&lt;/p&gt;

&lt;p&gt;So I split it into three specialized agents. Not because it's trendy. Because general-purpose AI is a compromise, and I was tired of compromising.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: One Brain, Too Many Jobs
&lt;/h2&gt;

&lt;p&gt;I run a home lab on a Mac Mini M4, a Windows PC with an RTX 3060, and an Ubuntu box. All three have Ollama installed. For the first six months, I just used the biggest model I could fit and routed everything to it.&lt;/p&gt;

&lt;p&gt;That model was Qwen 3 Coder 30B — a coding specialist. Great for refactoring. Great for debugging. But when I asked it to "write a friendly summary of my day," it would respond with something that sounded like a technical spec document. Because that's what it was trained to do.&lt;/p&gt;

&lt;p&gt;The reverse was just as bad. When I used a general chat model for coding questions, it would hallucinate APIs, miss edge cases, and write code that looked right but subtly violated conventions.&lt;/p&gt;

&lt;p&gt;I was asking a brain surgeon to do therapy, and a therapist to do surgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Split: Three Agents, Three Jobs
&lt;/h2&gt;

&lt;p&gt;I didn't build a microservices architecture. I didn't install Kubernetes. I added two more Ollama endpoints and a 20-line router.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Celebi (Mac Mini, Qwen 3.5 9B)&lt;/strong&gt; — The generalist. Handles routing, scheduling, daily summaries, weather, quick questions. Response time: 1-2 seconds. Perfect for "what's on my calendar today" and "summarize these emails."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ProgrammierMinna (Windows PC, Qwen 3 Coder 30B)&lt;/strong&gt; — The coder. Handles code generation, refactoring, debugging, PR review. Response time: 8-15 seconds. When I need a function written or a bug found, this is where the query goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DocMinna (Mac Mini, Granite 3.2 8B)&lt;/strong&gt; — The writer. Handles documentation, article drafts, READMEs, technical specs. Response time: 3-5 seconds. This model is worse at coding but surprisingly good at structure and flow.&lt;/p&gt;

&lt;p&gt;The router is embarrassingly simple:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;coding_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refactor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;writing_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coding_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# ProgrammierMinna
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;writing_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# DocMinna
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Celebi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it perfect? No. "Write a Python script that sends emails" gets routed to DocMinna because of "write," when it probably should go to ProgrammierMinna. I fix those manually when I catch them. But it works 90% of the time, and that's enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quality went up immediately
&lt;/h3&gt;

&lt;p&gt;Before the split, I'd ask for a code review and get generic advice like "consider adding error handling." After the split, ProgrammierMinna would say "this async function doesn't handle TimeoutError — add a try/except around line 47, and use asyncio.wait_for with a 5-second timeout."&lt;/p&gt;

&lt;p&gt;Same question. Different model. Different depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  I stopped over-explaining
&lt;/h3&gt;

&lt;p&gt;With the generalist, I'd have to add context like "please be thorough, this is for production code" or "keep it casual, this is a blog post." The specialists already know their role. I don't need to prompt-engineer the tone. It's baked into the model choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel processing became possible
&lt;/h3&gt;

&lt;p&gt;I can now fire off a coding task to ProgrammierMinna and a writing task to DocMinna simultaneously. They're running on different machines with different GPUs. No queue. No waiting for one to finish before the other starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallbacks got simpler
&lt;/h3&gt;

&lt;p&gt;When the Windows PC is offline ( asleep, rented out on Vast.ai, or I'm traveling), queries that would normally go to ProgrammierMinna fall back to Celebi with a note: "PC offline — answering with generalist model, quality may vary." The system degrades gracefully instead of just failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downsides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Three models to manage.&lt;/strong&gt; Updates, storage, keeping track of which version is on which machine — it's overhead. Each model is 4-20GB. My model folder went from 40GB to 120GB across three machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing mistakes happen.&lt;/strong&gt; I mentioned the "write a Python script" example. There are others. "Debug this article" (writing + debugging) confuses the router. I hit maybe 5% mis-routes, and I notice them because the response feels slightly off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More endpoints to monitor.&lt;/strong&gt; Instead of one Ollama instance to check, I have three. I built a simple health check script that pings each one and sends me a Telegram alert if any are down. It took 30 minutes to build. It runs forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context doesn't transfer.&lt;/strong&gt; If I'm in a long coding session with ProgrammierMinna and then ask Celebi "what did we just decide about the database schema?" — Celebi has no idea. Each agent has its own conversation history. I work around this by copying relevant context when I switch, but it's friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;One Generalist&lt;/th&gt;
&lt;th&gt;Three Specialists&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (coding)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;8-15s (same model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (writing)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;3-5s (smaller, faster model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (general)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;1-2s (tiny model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review quality&lt;/td&gt;
&lt;td&gt;6/10&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft writing quality&lt;/td&gt;
&lt;td&gt;5/10&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily summary quality&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$0 (local)&lt;/td&gt;
&lt;td&gt;$0 (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance overhead&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality jumps are subjective but real. I measured them by how often I had to ask for a redo. With the generalist, maybe 30% of responses needed a follow-up clarification. With specialists, maybe 5%.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense (And When It Doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have multiple distinct task types (coding + writing + analysis)&lt;/li&gt;
&lt;li&gt;You have the hardware to run multiple models (even small ones)&lt;/li&gt;
&lt;li&gt;You care about quality more than simplicity&lt;/li&gt;
&lt;li&gt;You're already hitting the limits of your current model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're just casually chatting with AI&lt;/li&gt;
&lt;li&gt;You only have one machine with limited RAM&lt;/li&gt;
&lt;li&gt;Your tasks are all similar (all coding, all writing)&lt;/li&gt;
&lt;li&gt;You value simplicity over marginal quality gains&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Setup in 30 Minutes
&lt;/h2&gt;

&lt;p&gt;If you want to try this, here's the fastest path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Ollama on two machines&lt;/strong&gt; (or twice on one machine if you have the RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull different models:&lt;/strong&gt; a coder model on one, a general model on the other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a 10-line router&lt;/strong&gt; (like the Python snippet above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point your scripts at the router&lt;/strong&gt; instead of directly at a model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjust keywords&lt;/strong&gt; as you find mis-routes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: 30 minutes. Total cost: $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win
&lt;/h2&gt;

&lt;p&gt;The biggest change isn't technical. It's mental.&lt;/p&gt;

&lt;p&gt;Before, I had one AI "employee" who was mediocre at everything. Now I have three specialists who are genuinely good at their jobs. When I send a query, I know which expert is handling it. I trust the output more. I spend less time verifying and fixing.&lt;/p&gt;

&lt;p&gt;It's the difference between a Swiss Army knife and a actual toolbox. The knife fits in your pocket. But when you need to build something real, you want the right tool.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a multi-agent AI setup on a 3-machine home lab. Writes about the infrastructure that makes local AI actually usable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #agents #automation #selfhosted #ollama #productivity #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>agents</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Garmin Watch Face with Live Stock Charts in Monkey C — Here’s What I Learned</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:02:19 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-garmin-watch-face-with-live-stock-charts-in-monkey-c-heres-what-i-learned-5b14</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-garmin-watch-face-with-live-stock-charts-in-monkey-c-heres-what-i-learned-5b14</guid>
      <description>&lt;p&gt;I wanted stock prices on my wrist. Not a notification — an actual chart. So I built a custom Garmin watch face with live sparkline charts for 5 configurable assets.&lt;/p&gt;

&lt;p&gt;Here’s what I learned building &lt;strong&gt;StockFaceTC&lt;/strong&gt; for the Garmin Venu 2 Plus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Garmin watch faces are &lt;strong&gt;tiny&lt;/strong&gt; — 124KB memory, 416×416 AMOLED display, and a language (Monkey C) that most devs have never heard of. No npm. No frameworks. No Stack Overflow answers.&lt;/p&gt;

&lt;p&gt;Oh, and the “simulator” can’t make web requests. You test API calls on real hardware or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A watch face showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time, date, and health stats (heart rate, steps, floors)&lt;/li&gt;
&lt;li&gt;5 polar sparkline charts in an outer ring — each showing 24 hours of price data&lt;/li&gt;
&lt;li&gt;Auto-updates every 15 minutes via Twelve Data API&lt;/li&gt;
&lt;li&gt;6-hour weather forecast with custom-drawn icons&lt;/li&gt;
&lt;li&gt;Fully configurable tickers via the Garmin Connect app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Default tickers:&lt;/strong&gt; AAPL, TSLA, Gold, BTC/USD, ETH/USD&lt;/p&gt;

&lt;h2&gt;
  
  
  The Font Problem (and How I Solved It)
&lt;/h2&gt;

&lt;p&gt;Garmin’s &lt;code&gt;VectorFont&lt;/code&gt; API isn’t available on all devices. The Venu 2 Plus? Nope.&lt;/p&gt;

&lt;p&gt;So I built my own: &lt;strong&gt;PrimitiveFont&lt;/strong&gt; — a complete vector font library using only &lt;code&gt;drawLine()&lt;/code&gt; calls. Every character is defined on a 5×7 grid and rendered procedurally.&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;// “A” on a 5×7 grid&lt;/span&gt;
&lt;span class="nx"&gt;CHAR_A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full alphabet (A-Z, a-z, 0-9, specials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proportional widths&lt;/strong&gt; (Arial-like metrics — ‘i’ is narrow, ‘M’ is wide)&lt;/li&gt;
&lt;li&gt;Bold, italic, underline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arc text&lt;/strong&gt; — characters curved along a circular path (for the ring labels!)&lt;/li&gt;
&lt;li&gt;Any size via simple scaling: &lt;code&gt;sizePx / 7.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire library is ~21KB — well within the 124KB budget.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Garmin Background Service (every 15 min)
  → Twelve Data API (1h candles × 24 = full day)
  → Phone acts as transparent HTTP proxy
  → JSON parsed in background thread (64KB limit!)
  → Prices + display names stored in model
  → View renders sparklines + arc labels on update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Constraint: 64KB Background Memory
&lt;/h3&gt;

&lt;p&gt;The background service that fetches data runs in a &lt;strong&gt;separate 64KB sandbox&lt;/strong&gt;. You can’t access the main app’s memory. The only way to pass data: &lt;code&gt;Background.exit(dictionary)&lt;/code&gt;, which the main app receives in &lt;code&gt;onBackgroundData()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means: parse JSON, extract only what you need, pass a minimal dictionary. No room for raw API responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Labels from API
&lt;/h2&gt;

&lt;p&gt;One early mistake: I hardcoded display names like &lt;code&gt;“XAU/USD” → “GOLD”&lt;/code&gt;. That doesn’t scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; The Twelve Data API returns metadata with every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;“meta”:&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;“symbol”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“XAU/USD”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;“currency_base”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Spot”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;“type”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“Precious&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Metal”&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;p&gt;Now labels come from the API itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stocks: &lt;code&gt;meta.symbol&lt;/code&gt; → “AAPL”&lt;/li&gt;
&lt;li&gt;Crypto: &lt;code&gt;meta.currency_base&lt;/code&gt; → “Bitcoin” → “BITCOIN”&lt;/li&gt;
&lt;li&gt;Commodities: &lt;code&gt;meta.currency_base&lt;/code&gt; → “Gold Spot” → “GOLD”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero hardcoded aliases. Add any ticker, get the right label automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulator Gotchas
&lt;/h2&gt;

&lt;p&gt;If you’re building Connect IQ apps, save yourself some pain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Properties are cached forever&lt;/strong&gt; — changing &lt;code&gt;properties.xml&lt;/code&gt; defaults does nothing after first run. Use “Reset All App Data” first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;makeWebRequest()&lt;/code&gt; doesn’t work&lt;/strong&gt; in the simulator — you need a real phone connected via Bluetooth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getSettingsView()&lt;/code&gt; must be implemented&lt;/strong&gt; (even returning &lt;code&gt;null&lt;/code&gt;) or the settings menu stays greyed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;VectorFont&lt;/code&gt;&lt;/strong&gt; on many devices — if you need custom text, roll your own like PrimitiveFont.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  API Budget
&lt;/h2&gt;

&lt;p&gt;Twelve Data free tier: 800 API calls/day.&lt;/p&gt;

&lt;p&gt;My budget: 5 symbols × 96 fetches/day (every 15 min) = &lt;strong&gt;480 calls&lt;/strong&gt;. Well within limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;Source is on &lt;a href="https://gitlab.com/celebi-dev/stockfacet" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. Feel free to fork it, break it, or build something cooler.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What’s the weirdest hardware you’ve built for? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>garmin</category>
      <category>connectiq</category>
      <category>crypto</category>
      <category>wearables</category>
    </item>
  </channel>
</rss>
