<?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: ujja</title>
    <description>The latest articles on DEV Community by ujja (@ujja).</description>
    <link>https://dev.to/ujja</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png</url>
      <title>DEV Community: ujja</title>
      <link>https://dev.to/ujja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ujja"/>
    <language>en</language>
    <item>
      <title>Building an Offline-First Bushfire Response Platform With Hermes Agent</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Thu, 28 May 2026 01:51:59 +0000</pubDate>
      <link>https://dev.to/ujja/building-an-offline-first-bushfire-response-platform-with-hermes-agent-4m0a</link>
      <guid>https://dev.to/ujja/building-an-offline-first-bushfire-response-platform-with-hermes-agent-4m0a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hermes-agent-2026-05-15"&gt;Hermes Agent Challenge&lt;/a&gt;: Build With Hermes Agent&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Project Haven&lt;/strong&gt; is an AI-powered emergency response platform for bushfire preparedness — evacuation routing, tiered real-time alerts, government recovery grant discovery, and offline-first PWA support for when mobile networks go down mid-crisis.&lt;/p&gt;

&lt;p&gt;The platform was originally a 46-hour hackathon build (GovHack 2024 — we won). This year I &lt;a href="https://dev.to/ujja/from-govhack-win-to-something-that-actually-matters-2mmi"&gt;brought it back from the dead and rebuilt &lt;/a&gt; it properly: event-driven microservices, contract-first OpenAPI specs, a prediction engine based on XGBoost weights from our historical bushfire notebooks, and a fully offline-capable React PWA.&lt;/p&gt;

&lt;p&gt;The one piece that was always a hollow mock was the &lt;strong&gt;AI Assistant&lt;/strong&gt; — the in-app emergency guidance chat. It had a &lt;code&gt;setTimeout&lt;/code&gt; pretending to think and a big &lt;code&gt;switch&lt;/code&gt; statement of canned responses. Every time I looked at it I felt embarrassed.&lt;/p&gt;

&lt;p&gt;Hermes fixed that.&lt;/p&gt;

&lt;p&gt;Hermes Agent is now the live brain behind three things in Project Haven:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In-app emergency guidance&lt;/strong&gt; — the AI Assistant page calls Hermes via its OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt; API, grounded with a system prompt that constrains it to verified Australian emergency protocols and instructs it to escalate emergency situations to 000.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduled fire-risk briefings&lt;/strong&gt; — Hermes runs a natural-language cronjob that fires every morning at 6am during fire season, calls the Bureau of Meteorology and NSW RFS feeds, synthesises a risk summary, and publishes it as an event into the alert pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recovery grant research&lt;/strong&gt; — when a user marks themselves as "in recovery", Hermes autonomously searches for current government grant programs (NDRA, state schemes, Services Australia), compares them against the user's declared situation, and adds matched recommendations to the recommendation service DB.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ujjavala/project-haven#" rel="noopener noreferrer"&gt;project-haven&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Running it locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One command spins up 6 microservices, an API gateway, PostgreSQL instances, RabbitMQ, and the React PWA at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To trigger the full prediction → alert pipeline:&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 http://localhost:8080/weather &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"lat":-33.87,"lng":151.21,"temperature":42,"windSpeed":80,"humidity":10,"season":"summer","vegetationDensity":0.9}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That simulates an extreme weather event near Sydney, runs it through the prediction engine, and fires a CRITICAL alert through the system within seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2y4ec9cqn3z8xrob0zxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2y4ec9cqn3z8xrob0zxe.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqv28hzwatx4q1cz9suow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqv28hzwatx4q1cz9suow.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7spgr4rlmklzvn5ghemz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7spgr4rlmklzvn5ghemz.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk89u2rrhjqsjr8rct90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk89u2rrhjqsjr8rct90.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqfku3yjspc98c33zjhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqfku3yjspc98c33zjhx.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbidh0y7ko8vs2soqpsel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbidh0y7ko8vs2soqpsel.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9pst2zgwfy0n6ahm6ka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9pst2zgwfy0n6ahm6ka.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Assistant — Hermes-powered emergency guidance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI Assistant page now sends messages to Hermes via the api-gateway (&lt;code&gt;/assistant/v1/chat/completions&lt;/code&gt;). Hermes has persistent memory across sessions via &lt;code&gt;X-Hermes-Session-Key&lt;/code&gt;, so if a user opened the app two days ago and said "I'm in the Blue Mountains", Hermes still knows that when they say "the fire is getting closer" today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled briefings appearing in the alert feed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every morning during fire season, Hermes fetches live fire danger ratings from the NSW RFS API, synthesises a 3-sentence risk summary grounded in real data, and injects it as a &lt;code&gt;feed.created&lt;/code&gt; event. The event propagates through RabbitMQ to the alert service and appears in-app within seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery grant matching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user marks the "Recovery" scenario. Hermes is asked to research grants available for their postcode and situation. It uses its web search tool to check current Services Australia pages — bypassing the staleness problem of any static dataset — and returns structured results that get persisted back to the recommendations table.&lt;/p&gt;




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

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ujja/project-haven" rel="noopener noreferrer"&gt;project-haven&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Running it locally
&lt;/h3&gt;

&lt;p&gt;The full stack — Haven microservices + Hermes Agent + a local LLM — runs entirely on your machine. No external inference API needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Docker Desktop 27+, 8 GB RAM minimum (16 GB recommended), macOS / Linux / WSL2 on Windows.&lt;/p&gt;

&lt;p&gt;Ollama, the model, all backend services, and the frontend are all managed by Docker Compose. No host-level installation needed beyond Docker itself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1 — Clone and start everything
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ujja/project-haven.git
&lt;span class="nb"&gt;cd &lt;/span&gt;project-haven
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first start, the &lt;code&gt;ollama-init&lt;/code&gt; container pulls &lt;code&gt;nous-hermes2&lt;/code&gt; (~4.5 GB) automatically. The api-gateway waits for the pull to complete before starting. Subsequent starts are fast — the model is cached in a Docker volume.&lt;/p&gt;

&lt;p&gt;Progress is streamed to the compose log. Once you see &lt;code&gt;api-gateway | api-gateway listening on port 8080&lt;/code&gt;, everything is ready.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2 — Test the full pipeline
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Simulate an extreme weather event near Sydney → triggers prediction → alert&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/weather &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"lat":-33.87,"lng":151.21,"temperature":42,"windSpeed":80,"humidity":10,"season":"summer","vegetationDensity":0.9}'&lt;/span&gt;

&lt;span class="c"&gt;# Ask the AI assistant directly&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/assistant/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"model":"nous-hermes2","messages":[{"role":"user","content":"There is a bushfire near me. What do I do right now?"}]}'&lt;/span&gt;

&lt;span class="c"&gt;# Search live recovery grants&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/recommendations/research &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"postcode":"2750","situation":"home destroyed by bushfire"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Architecture of the local stack
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React PWA (port 3000)
        ↓
API Gateway (port 8080)
   ├── /assistant/*  →  Ollama /v1/chat/completions (nous-hermes2)
   ├── /recommendations/research  →  Ollama (structured JSON)
   └── node-cron @ 6am  →  Ollama → feed-service
        ↓
Ollama container (port 11434, haven-net)
        ↓
nous-hermes2 (cached in ollama-data volume)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs inside Docker Compose. &lt;code&gt;ollama-init&lt;/code&gt; pulls the model once on first start; the &lt;code&gt;ollama-data&lt;/code&gt; volume persists it across restarts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Starter model recommendations
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nous-hermes2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~4.5 GB&lt;/td&gt;
&lt;td&gt;Default — strong instruction following, good JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:9b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~5.4 GB&lt;/td&gt;
&lt;td&gt;Superior JSON adherence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.6 GB&lt;/td&gt;
&lt;td&gt;Low-RAM machines, faster responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~4.7 GB&lt;/td&gt;
&lt;td&gt;General-purpose alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set &lt;code&gt;HERMES_MODEL&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; to swap. Also update the &lt;code&gt;ollama-init&lt;/code&gt; entrypoint in &lt;code&gt;docker-compose.yml&lt;/code&gt; to pull the new model.&lt;/p&gt;

&lt;p&gt;Avoid 70B-class models unless you have GPU hardware with 40+ GB VRAM.&lt;/p&gt;

&lt;h4&gt;
  
  
  Common issues
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;First start is slow:&lt;/strong&gt; The &lt;code&gt;ollama-init&lt;/code&gt; container has to download the &lt;code&gt;nous-hermes2&lt;/code&gt; model (~4.5 GB). Subsequent starts skip this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model too slow on CPU:&lt;/strong&gt; Edit &lt;code&gt;.env&lt;/code&gt; and set &lt;code&gt;HERMES_MODEL=gemma2:2b&lt;/code&gt; (1.6 GB, much faster). Also update the &lt;code&gt;ollama-init&lt;/code&gt; entrypoint in &lt;code&gt;docker-compose.yml&lt;/code&gt; to pull the new model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WSL2 networking problems:&lt;/strong&gt; Volume mounts and bridge networking have edge cases — increasing Docker's memory allocation in Docker Desktop settings usually resolves them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux GPU acceleration:&lt;/strong&gt; Uncomment the &lt;code&gt;deploy&lt;/code&gt; block in the &lt;code&gt;ollama&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt; and ensure &lt;code&gt;nvidia-container-toolkit&lt;/code&gt; is installed.&lt;/p&gt;




&lt;h3&gt;
  
  
  My Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI backbone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nous Hermes 2 via Ollama (OpenAI-compatible, locally hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React 18, TypeScript, Vite, Workbox PWA, Leaflet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js 20, Express, TypeScript — 6 microservices + API gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Messaging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RabbitMQ (event-driven: weather → prediction → alert pipeline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Databases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL (per-service)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;XGBoost (Python notebooks → TypeScript heuristic engine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Digital Atlas / Geoscience Australia ArcGIS REST APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker Compose, multi-stage builds, shared &lt;code&gt;@haven/shared&lt;/code&gt; npm package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How I Used Hermes Agent
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. OpenAI-Compatible Inference — Zero New API Surface
&lt;/h3&gt;

&lt;p&gt;Ollama exposes &lt;code&gt;POST /v1/chat/completions&lt;/code&gt; exactly like the OpenAI SDK expects. It runs as a Docker Compose service (&lt;code&gt;ollama&lt;/code&gt;) on the internal &lt;code&gt;haven-net&lt;/code&gt; network. The api-gateway proxies &lt;code&gt;/assistant/*&lt;/code&gt; directly to &lt;code&gt;http://ollama:11434&lt;/code&gt;. No separate agent container, no extra port, no API key management between services.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;AIAssistant.tsx&lt;/code&gt;, the &lt;code&gt;getAIResponseHermes()&lt;/code&gt; function — previously a &lt;code&gt;setTimeout&lt;/code&gt; + &lt;code&gt;switch&lt;/code&gt; statement — became a real API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAIResponseHermes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/assistant/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nous-hermes2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HAVEN_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// emergency protocols, escalate to 000, AU context&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Ollama responded with &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The api-gateway proxy strips any client-side auth before forwarding to Ollama. A stable session key is stored in &lt;code&gt;localStorage&lt;/code&gt; per browser for memory scoping, ready for if a stateful layer is added upstream.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Scheduled Fire Risk Briefings — Natural Language Cron
&lt;/h3&gt;

&lt;p&gt;Hermes's cron scheduling is genuinely one of its most underrated features. Instead of writing a Node worker with &lt;code&gt;node-cron&lt;/code&gt;, a BOM API client, a response parser, and an event publisher, I wrote this in the Hermes config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fire-risk-briefing&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Oct-Mar)"&lt;/span&gt;   &lt;span class="c1"&gt;# 6am daily, fire season only&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Check the current fire danger ratings for NSW, VIC, SA, and WA from the&lt;/span&gt;
      &lt;span class="s"&gt;Bureau of Meteorology and RFS feeds. Synthesise a 3-sentence morning&lt;/span&gt;
      &lt;span class="s"&gt;briefing — severity level, highest-risk regions, and one action&lt;/span&gt;
      &lt;span class="s"&gt;recommendation. Keep it under 80 words. Respond with JSON:&lt;/span&gt;
      &lt;span class="s"&gt;{ "severity": "LOW|MEDIUM|HIGH|EXTREME|CATASTROPHIC", "summary": "..." }&lt;/span&gt;
    &lt;span class="na"&gt;deliver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://api-gateway:8080/feeds&lt;/span&gt;
    &lt;span class="na"&gt;skills&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;web_search&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermes handles the scheduling, the web fetch, the summarisation, and the HTTP delivery. The &lt;code&gt;/feeds&lt;/code&gt; endpoint in the feed service receives the JSON payload and publishes it as a &lt;code&gt;feed.created&lt;/code&gt; event. The entire pipeline — external data → summary → in-app alert — runs without any new code.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Recovery Grant Research — Delegating Agentic Workloads
&lt;/h3&gt;

&lt;p&gt;The recommendation service seeds static government programs at startup. But government grants change — new schemes open after disasters, eligibility criteria shift, application portals go down and come back up.&lt;/p&gt;

&lt;p&gt;For the recovery scenario, I added a route in the api-gateway that delegates a research task to Hermes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api-gateway: POST /recommendations/research&lt;/span&gt;
&lt;span class="c1"&gt;// HERMES_BASE = http://ollama:11434 (set via env in docker-compose)&lt;/span&gt;
&lt;span class="c1"&gt;// HERMES_MODEL = nous-hermes2 (default, overridable via HERMES_MODEL env var)&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/recommendations/research&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;situation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hermesRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HERMES_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HERMES_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are an Australian emergency recovery specialist. Return ONLY a valid JSON array — no markdown fences, no commentary, just the array.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Research current Australian government disaster recovery grants
            available for postcode &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Situation: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;situation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.
            Return JSON array of { title, provider, description, applicationUrl, eligibilitySummary }.
            Only include currently open programs with verified URLs.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hermesRes&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Strip accidental markdown fences if the model wraps the JSON&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;(?:&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;/, ''&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;.replace&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```$/, '').trim();
  const grants = JSON.parse(cleaned);
  res.json({ grants });
});
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hermes uses its web search tool to check current Services Australia, state government, and Red Cross pages. It returns structured JSON. The gateway upserts the results into the recommendation service — live, verified, current — not frozen in a seed file.&lt;/p&gt;

&lt;p&gt;The key agentic capability here is &lt;strong&gt;web search grounded in the real prompt&lt;/strong&gt;: Hermes significantly reduced stale or hallucinated grant recommendations by grounding responses in live web results rather than parametric knowledge. It fetches the actual pages, checks them, and only returns what it finds. For emergency advice, that correctness bar is non-negotiable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Hermes vs. the Alternatives — An Honest Comparison
&lt;/h3&gt;

&lt;p&gt;Before landing on Hermes, I experimented with a few other locally-run models. Here's what that comparison actually looked like for an emergency-context application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Llama 3.1 / 3.2 (via Ollama)
&lt;/h4&gt;

&lt;p&gt;Llama is the obvious first stop for local inference. I ran Llama 3.1 8B and 3.2 3B through Ollama and pointed the same system prompt at them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt; Ollama's OpenAI-compatible &lt;code&gt;/api/chat&lt;/code&gt; endpoint made drop-in testing easy. Llama 3.2 3B is genuinely fast on Apple Silicon — response latency was better than Hermes on equivalent hardware. For simple question-answering against a fixed system prompt, the outputs were reasonable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't:&lt;/strong&gt; Neither model had built-in tool execution, persistent session memory, or a scheduler — all three capabilities I needed. To replicate Hermes's cron briefings with Llama, I would have written a Node cron worker, a separate BOM API client, a response parser, an event publisher, and a memory store. That's four services Hermes replaced with a YAML block. Llama also had a notable tendency toward elaboration — answers were often 3-4× too long for a crisis UX where brevity is safety-critical. Prompt engineering helped, but it was a constant battle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Great for straight inference tasks where you're bringing your own orchestration layer. Not the right fit when you need the agent capabilities without the plumbing.&lt;/p&gt;




&lt;h4&gt;
  
  
  Gemma 2 (9B, via Ollama)
&lt;/h4&gt;

&lt;p&gt;Google's Gemma 2 9B was the most pleasant surprise in terms of raw instruction-following. It respected format constraints (JSON output, word limits) more reliably than anything else I tested at this size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt; JSON output adherence was excellent. When I told it to return &lt;code&gt;{ "severity": "...", "summary": "..." }&lt;/code&gt;, it almost always did — no markdown wrapping, no prose preamble. That's unusually good at 9B parameters. It also handled the Australian emergency context well without needing extensive ground-truth examples in the system prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't:&lt;/strong&gt; Same infrastructure gap as Llama — no native tool use, no session memory, no scheduling. Gemma 2's knowledge cutoff also made it confidently wrong about post-2024 government programs (it cited DRFA schemes that had been superseded). For a platform that needs to tell people where to apply for grants right now, that's a hard failure mode. Web search grounding isn't optional here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best raw model for structured output tasks at the 7-9B tier. If I ever strip Hermes out and build the orchestration layer myself, Gemma 2 is what I'd put under it.&lt;/p&gt;




&lt;h4&gt;
  
  
  Mistral 7B / Mistral-Nemo
&lt;/h4&gt;

&lt;p&gt;I tried Mistral 7B Instruct and Mistral-Nemo (12B) briefly. Both are fast and capable general-purpose models. But in the emergency context, Mistral had one consistent problem: it over-hedged.&lt;/p&gt;

&lt;p&gt;Every answer about what to do in a bushfire came back wrapped in "I am not a qualified emergency services professional and this should not be taken as official advice…" disclaimers that would fill half the screen on a mobile device. I understand why models are trained to do this. But during an actual emergency, a response that leads with three sentences of disclaimer before telling someone to leave is arguably worse than no AI at all. Getting Mistral to drop the hedging without also dropping the actual safety guardrails required more system prompt engineering than the ROI justified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Capable model, wrong default behaviour for a high-stakes UX. Taming it is possible but expensive in prompt tokens and iteration time.&lt;/p&gt;




&lt;h4&gt;
  
  
  Hermes — What It Does Better
&lt;/h4&gt;

&lt;p&gt;Against this field, here's where Hermes genuinely pulls ahead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native tool execution with no orchestration layer.&lt;/strong&gt; Web search, HTTP calls, file reads — built in. No LangChain, no custom function-calling wrapper, no managing tool schemas manually. For the grant research workflow (fetch → parse → structure → return), this is a week of orchestration code that just isn't written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent session memory across requests.&lt;/strong&gt; Every other model I tested was stateless per-request. Hermes's session-scoped memory model is simple and works reliably in practice. For a crisis scenario that plays out over days, that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron scheduling with natural language task definitions.&lt;/strong&gt; Nothing else offers this at the infrastructure level. The fire-season briefing scheduler is 12 lines of YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stays on the wire it's told to stay on.&lt;/strong&gt; Hermes with the Haven system prompt stays grounded in Australian emergency protocols, is instructed to escalate emergency situations to 000, and significantly reduced stale or hallucinated grant recommendations by grounding responses in live web results. The safety system prompt feels sticky in a way that required more reinforcement with Llama and Mistral.&lt;/p&gt;




&lt;h4&gt;
  
  
  Why I Chose Hermes Over More General Agent Platforms
&lt;/h4&gt;

&lt;p&gt;I also evaluated broader autonomous-agent platforms — particularly OpenClaw-style systems built around persistent personal agents, plugins, and wide capability surfaces.&lt;/p&gt;

&lt;p&gt;Those systems are impressive. But for Project Haven, they solved a different problem than the one I actually had.&lt;/p&gt;

&lt;p&gt;Project Haven is not trying to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a personal AI operating system,&lt;/li&gt;
&lt;li&gt;a desktop automation agent,&lt;/li&gt;
&lt;li&gt;or an infinitely extensible multi-agent ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It needs something much narrower and more reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic emergency workflows,&lt;/li&gt;
&lt;li&gt;bounded tool execution,&lt;/li&gt;
&lt;li&gt;persistent memory,&lt;/li&gt;
&lt;li&gt;scheduled autonomy,&lt;/li&gt;
&lt;li&gt;and predictable behaviour under pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction ended up mattering a lot.&lt;/p&gt;

&lt;p&gt;Platforms like OpenClaw optimise for maximum flexibility — plugins, integrations, autonomous behaviours, evolving capability graphs. Hermes feels more opinionated. In practice, that was a benefit.&lt;/p&gt;

&lt;p&gt;For an emergency-response application, I cared more about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reliability over extensibility,&lt;/li&gt;
&lt;li&gt;constrained behaviour over open-ended autonomy,&lt;/li&gt;
&lt;li&gt;and operational predictability over ecosystem breadth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tighter execution model made Hermes easier to reason about architecturally. The memory model was simpler. The scheduling primitives were built in. And the default operational surface felt significantly safer for a high-stakes context.&lt;/p&gt;

&lt;p&gt;That tradeoff means Hermes currently has a smaller ecosystem, fewer integrations, less community tooling, and less flexibility than broader agent platforms. But for Project Haven, that narrower scope was exactly the point.&lt;/p&gt;

&lt;p&gt;I didn't need an AI operating system. I needed a dependable emergency-response runtime that could live entirely inside my infrastructure stack and keep working when the situation around it stopped being normal.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where Hermes Falls Short
&lt;/h4&gt;

&lt;p&gt;Being honest about the gaps matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start time.&lt;/strong&gt; Hermes in Docker takes noticeably longer to reach a ready state than a model served via Ollama. On my MacBook Pro M2, Ollama with Llama 3.2 3B is ready in under 5 seconds. Hermes takes 15-25 seconds to initialise its memory backend and tool registry. In a production deployment this is a non-issue (it starts once). In development, it slows iteration loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw inference speed at the same model size.&lt;/strong&gt; Hermes's overhead — memory management, tool routing, session handling — costs tokens per request. For a simple in-context QA task with no tools, Llama through Ollama will answer faster. For the agentic tasks (web search, multi-step research), that comparison flips because Hermes automates and coordinates multi-step tool execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation gaps.&lt;/strong&gt; The job scheduler YAML schema isn't well-documented — I spent more time than I'd like reading source to understand what &lt;code&gt;deliver:&lt;/code&gt; accepted and how session keys scope memory. Llama/Gemma via Ollama have significantly more community documentation. Stack Overflow has nothing for Hermes-specific issues yet; you're on the GitHub issues list and Discord.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model selection is less flexible.&lt;/strong&gt; Hermes currently offers less model flexibility than a pure Ollama workflow. If you want Gemma 2 9B's superior JSON adherence under Hermes's tool/memory layer, that combination isn't always straightforward depending on your configuration. With Ollama, you can swap models in one command. This matters if you're trying to tune cost/quality tradeoffs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows support is rough.&lt;/strong&gt; Docker on Windows with WSL2 works but the volume mounting and networking for Hermes's memory backend had edge cases I had to work around. On macOS and Linux it was smooth.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Hermes Was the Right Fit (Despite All That)
&lt;/h3&gt;

&lt;p&gt;A few things made Hermes specifically well-suited here over rolling a custom LLM integration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It lives on your server.&lt;/strong&gt; Project Haven is explicitly designed for scenarios where internet connectivity is degraded. Hermes runs in the same Docker Compose stack as everything else — no dependency on an external inference API during the crisis window. The model runs locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent memory that scales with the crisis.&lt;/strong&gt; Emergency situations evolve over hours and days. A user's context from this morning matters when they're asking questions tonight. Hermes's session memory handles this automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled autonomy fits the "prevention not reaction" model.&lt;/strong&gt; The best emergency outcome is a user who prepared before the fire arrived. Hermes's cron scheduler lets Project Haven push proactive briefings during fire season without any always-on polling infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI-compatible API means zero new client code.&lt;/strong&gt; The React frontend, the Node gateway, anything that already speaks the OpenAI format works against Hermes unmodified. No new SDK. No vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local inference eliminates API dependency risk.&lt;/strong&gt; One thing that became obvious very quickly while testing hosted AI APIs was how fast autonomous workflows amplify usage. A single user interaction can become multiple model calls — retrieval, summarisation, reasoning, formatting, follow-up clarification. For a crisis-response platform, depending entirely on external inference APIs introduced operational and cost dependencies I wasn't comfortable with. Running Hermes locally shifted the tradeoff toward compute and infrastructure complexity instead of per-token billing and rate limits — which was the right trade for this project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Project Haven started as a 46-hour hackathon build, sat frozen for two years, and became the platform it was always supposed to be during this rebuild.&lt;/p&gt;

&lt;p&gt;The AI Assistant was always the most important feature and the least real. Hermes made it real — not just more capable, but genuinely appropriate for the stakes of an emergency context: server-local, memory-persistent, verifiably grounded, and invisible enough to get out of the way when someone needs an answer fast.&lt;/p&gt;

&lt;p&gt;If you're building anything where the AI output actually matters — where a wrong answer has real consequences — the pattern of running Hermes locally with a tight system prompt and opaque tool execution is one I'd recommend seriously.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with TypeScript, React, Node.js, PostgreSQL, RabbitMQ, Docker, Hermes Agent, and a lot of respect for the people who actually work bushfire emergencies.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Originally born at GovHack 2024. Finally given room to grow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hermesagentchallenge</category>
      <category>devchallenge</category>
      <category>agents</category>
    </item>
    <item>
      <title>From Govhack Win to Something That Actually Matters</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sun, 24 May 2026 12:46:29 +0000</pubDate>
      <link>https://dev.to/ujja/from-govhack-win-to-something-that-actually-matters-2mmi</link>
      <guid>https://dev.to/ujja/from-govhack-win-to-something-that-actually-matters-2mmi</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;Project Haven is an AI-powered emergency response and disaster recovery platform built specifically for bushfire preparedness — helping people find safe evacuation points, receive real-time alerts, access government recovery support, and stay informed even when the internet cuts out.&lt;/p&gt;

&lt;p&gt;The core idea is simple but important: &lt;strong&gt;during a bushfire, information is survival&lt;/strong&gt;. Where are the nearest evacuation centres? How bad is the risk right now? What government grants can I apply for after my home is damaged? Project Haven tries to answer all of that — fast, reliably, and even offline.&lt;/p&gt;

&lt;p&gt;Under the hood, the platform combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictive analytics&lt;/strong&gt; — a bushfire risk engine combining the McArthur FFDI Mark 5 formula (official BOM standard) with an XGBoost model trained on GA historical fire data, running as a Python sidecar blended at 80/20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart evacuation routing&lt;/strong&gt; — nearest safe space recommendations with distance and ETA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-powered recovery support&lt;/strong&gt; — scenario-based government grants and services backed by live AI research via Nous Hermes 2 (real ones, like the Disaster Recovery Payment and Primary Producer Recovery Grant)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live emergency guidance&lt;/strong&gt; — an AI Assistant powered by Nous Hermes 2 running locally via Ollama; real conversational responses grounded in Australian emergency protocols, with a system prompt that escalates to 000 when needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live community feed&lt;/strong&gt; — on-the-ground updates from people in affected areas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time alerts&lt;/strong&gt; — tiered notifications from low-priority feed updates to full-screen critical banners&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first PWA&lt;/strong&gt; — because during emergencies, mobile networks are the first thing to go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is fully event-driven microservices: a prediction service subscribes to weather updates, runs the FFDI engine blended with an XGBoost model-service sidecar (Python/FastAPI, trained from GA bushfire boundaries and baked into Docker at build time), publishes bushfire predictions, and the alert service automatically generates tiered notifications from those predictions. All async. All decoupled. RabbitMQ handling the message bus, PostgreSQL for persistence, React PWA for the frontend, everything containerised and wired together with Docker Compose — including a local Ollama instance serving Nous Hermes 2 for the AI Assistant, grant research, and scheduled fire-season briefings.&lt;/p&gt;

&lt;p&gt;The part I'm still most proud of? The offline-first thinking. The app caches evacuation points, alerts, and recommendations locally. If connectivity drops mid-crisis — which it will — the app keeps working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ujjavala/project-haven#" rel="noopener noreferrer"&gt;project-haven&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Running it locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One command spins up 6 microservices, an API gateway, PostgreSQL instances, RabbitMQ, and the React PWA at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To trigger the full prediction → alert pipeline:&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 http://localhost:8080/weather &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"lat":-33.87,"lng":151.21,"temperature":42,"windSpeed":80,"humidity":10,"season":"summer","vegetationDensity":0.9}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That simulates an extreme weather event near Sydney, runs it through the prediction engine, and fires a CRITICAL alert through the system within seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2y4ec9cqn3z8xrob0zxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2y4ec9cqn3z8xrob0zxe.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqv28hzwatx4q1cz9suow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqv28hzwatx4q1cz9suow.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7spgr4rlmklzvn5ghemz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7spgr4rlmklzvn5ghemz.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk89u2rrhjqsjr8rct90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk89u2rrhjqsjr8rct90.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqfku3yjspc98c33zjhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqfku3yjspc98c33zjhx.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbidh0y7ko8vs2soqpsel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbidh0y7ko8vs2soqpsel.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9pst2zgwfy0n6ahm6ka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn9pst2zgwfy0n6ahm6ka.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;Okay, here's the honest version.&lt;/p&gt;

&lt;p&gt;Project Haven was built for &lt;a href="https://govhack.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;GovHack 2024&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've never done GovHack — it's a 46-hour hackathon using Australian government open data. You show up, pick a problem, build something, demo it, and hope the judges like it.&lt;/p&gt;

&lt;p&gt;I showed up with the idea of building something that could actually help people during bushfire emergencies. Real datasets. Real data analysis. A real prediction model. I used the Digital Atlas historical bushfire boundaries CSV and ABS community datasets, built XGBoost models in Jupyter notebooks, and sketched out an event-driven microservice architecture.&lt;/p&gt;

&lt;p&gt;No UI. No running backend. Just the notebooks, the numbers, and a vision for what the system &lt;em&gt;could&lt;/em&gt; be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And I won.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which was incredible. Genuinely one of those moments you don't forget.&lt;/p&gt;

&lt;p&gt;And then... the project just sat there.&lt;/p&gt;

&lt;p&gt;Because that's what happens after hackathons. The adrenaline drops. Everyone goes home. You sleep for two days. You go back to work on Monday. Life continues.&lt;/p&gt;

&lt;p&gt;The codebase stayed frozen. The notebooks stayed frozen. The architecture stayed frozen — as a snapshot of an idea that had way more potential than a 46-hour demo ever got to show.&lt;/p&gt;

&lt;p&gt;Not because the problem wasn't worth solving. Bushfire preparedness is genuinely important in Australia. Climate change is making it worse every year. The gap between what exists and what communities actually need during emergencies is still very real.&lt;/p&gt;

&lt;p&gt;But once the goal is achieved, you move on. That's just human nature.&lt;/p&gt;




&lt;p&gt;So when the &lt;strong&gt;GitHub Finish-Up-A-Thon&lt;/strong&gt; came up, I immediately thought of Project Haven.&lt;/p&gt;

&lt;p&gt;Because going back to old projects is a weird and strangely satisfying experience.&lt;/p&gt;

&lt;p&gt;You look at the code and you see everything at once — the shortcuts you took at 2am, the TODOs you never came back to, the half-implemented features, the places where "good enough for demo day" was the bar. But you also see the original intent underneath all of it. And sometimes that intent is still worth pursuing.&lt;/p&gt;

&lt;p&gt;This time, instead of treating it like a hackathon submission, I started treating it like an actual platform.&lt;/p&gt;

&lt;p&gt;That meant a completely different mindset:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (hackathon mode):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv4u7qjsl6z0vj2oyj03t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv4u7qjsl6z0vj2oyj03t.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A system design architecture diagram&lt;/li&gt;
&lt;li&gt;Jupyter notebooks with some number crunching&lt;/li&gt;
&lt;li&gt;No UI. No backend. Just the idea and the data analysis.&lt;/li&gt;
&lt;li&gt;"I'll build the rest... later"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After (product mode):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpd95u6d0afk5amj0lf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqpd95u6d0afk5amj0lf5.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contract-first OpenAPI specs before any implementation&lt;/li&gt;
&lt;li&gt;Proper microservice boundaries with clear domain ownership&lt;/li&gt;
&lt;li&gt;Real TypeScript throughout — strict mode, no &lt;code&gt;any&lt;/code&gt;, proper DTOs&lt;/li&gt;
&lt;li&gt;Shared event contracts so every service speaks the same language&lt;/li&gt;
&lt;li&gt;End-to-end Docker setup so anyone can run it with one command&lt;/li&gt;
&lt;li&gt;Offline-first PWA with Workbox service workers, not just a checkbox&lt;/li&gt;
&lt;li&gt;A prediction engine that actually reflects the model weights from my notebooks&lt;/li&gt;
&lt;li&gt;Real Australian government services and grants seeded into the recommendation service&lt;/li&gt;
&lt;li&gt;Full lucide-react icon system, proper design tokens, accessible UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the biggest architectural calls was how to handle the shared module. Every service needed the same event shapes, DTOs, and logger. Instead of duplicating types everywhere (the hackathon approach), I built &lt;code&gt;@haven/shared&lt;/code&gt; as a proper local npm package that each service depends on. The Dockerfiles build it first, then link it. Small thing, but it's the kind of thing that makes a codebase actually maintainable.&lt;/p&gt;

&lt;p&gt;Another big one was the prediction engine. My XGBoost notebooks had a trained model for fire risk prediction. And here I'll be fully honest: I'm not a data scientist. During the hackathon I didn't really know what I was doing with the notebooks. I just sort of... cooked up weights that felt plausible. Temperature matters a lot? Sure, give it 0.35. Wind is bad? 0.25. Humidity inverse, obviously. Slap some season bonuses on. Done. Ship it.&lt;/p&gt;

&lt;p&gt;Copilot called this out completely. Not in a confrontational way — but when I started asking about the prediction engine during the rebuild, it surfaced the actual problems: the &lt;code&gt;test_size=0.8&lt;/code&gt; split that meant the model was &lt;em&gt;training on 20%&lt;/em&gt; of the data and validating on 80% (backwards), the &lt;code&gt;area_ha &amp;lt; 3&lt;/code&gt; outlier filter that was literally removing all the dangerous large fires from the training set, and — most damningly — the fact that the XGBoost model was predicting &lt;em&gt;fire size&lt;/em&gt; from historical boundary features while the TypeScript engine was computing a &lt;em&gt;risk score&lt;/em&gt; from weather inputs. They were solving completely different problems. The weights I'd put in had no relationship to anything the notebook had analysed. They were just numbers I made up at 2am that sounded reasonable.&lt;/p&gt;

&lt;p&gt;So instead of just transcribing invented numbers, I rebuilt it properly. The engine now implements the McArthur Forest Fire Danger Index (FFDI) Mark 5 — the actual formula used by the Bureau of Meteorology and every Australian state fire service. The notebook was fixed (correct 80/20 train/test split, log-IQR outlier handling to keep the large fires), and the XGBoost model now serves a real purpose: predicting expected fire size (in hectares) for a given state and month, used as a historical context signal blended at 20% weight into the FFDI weather score. The XGBoost model now runs as a proper Python &lt;code&gt;model-service&lt;/code&gt; FastAPI sidecar, trained at Docker build time from the GA Historical Bushfire Boundaries dataset and baked into the image so startup is instant. The prediction service calls it asynchronously with a 2-second timeout; if it's unreachable, FFDI-only scoring kicks in — no hard dependency on the critical path.&lt;/p&gt;

&lt;p&gt;The "I just made numbers up" moment is a good illustration of why having something that can actually interrogate your assumptions matters, especially when you're working outside your domain.&lt;/p&gt;

&lt;p&gt;The whole thing went from "hackathon skeleton with mock data" to a working end-to-end system you can actually spin up and test. Weather event goes in, prediction comes out, alert fires, PWA displays it — all observable, all local, all real.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;I used GitHub Copilot as the primary engineering partner throughout this comeback — not as an autocomplete tool, but as a &lt;strong&gt;system-aware collaborator that held the full architecture in context while I worked&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting the stage: copilot-instructions.md
&lt;/h3&gt;

&lt;p&gt;The first thing I did before writing a single line of implementation was write &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; — a detailed spec covering the service boundaries, event contract shapes, CQRS split, security requirements, and UI/UX principles. Every rule I wanted enforced (contract-first OpenAPI, no &lt;code&gt;any&lt;/code&gt;, DTOs at boundaries, observability hooks, WCAG AA) went in there.&lt;/p&gt;

&lt;p&gt;That file became the foundation everything else was built on. Copilot read it before every response and generated code that conformed to patterns I'd already established — not guessing at conventions, but following documented ones. The difference in output quality was significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaffolding 6 microservices from a shared pattern
&lt;/h3&gt;

&lt;p&gt;The biggest single time-save was scaffolding all six backend services. Each needed the same structure: Express app, &lt;code&gt;helmet&lt;/code&gt; + &lt;code&gt;cors&lt;/code&gt; + &lt;code&gt;morgan&lt;/code&gt;, a rate limiter, a structured logger from &lt;code&gt;@haven/shared&lt;/code&gt;, a postgres schema init with seed data, RabbitMQ pub/sub wiring, health endpoints, and a multi-stage Dockerfile that builds &lt;code&gt;@haven/shared&lt;/code&gt; first and links it.&lt;/p&gt;

&lt;p&gt;That's probably 150 lines of consistent boilerplate per service. With Copilot understanding the pattern from the first service, each subsequent one took minutes rather than hours — and they were consistent in ways that matter: same error envelope shape, same correlation ID middleware, same health check format, same log field names.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shared module problem
&lt;/h3&gt;

&lt;p&gt;Every service needed the same event shapes, DTOs, and logger. The hackathon approach was to duplicate them. The right approach was &lt;code&gt;@haven/shared&lt;/code&gt; — a local npm package with its own &lt;code&gt;tsconfig&lt;/code&gt;, built first in Docker, then installed into each service via &lt;code&gt;file:&lt;/code&gt; resolution.&lt;/p&gt;

&lt;p&gt;Working out the correct multi-stage Dockerfile pattern for this (build shared → copy dist → npm install in service context) is fiddly. Copilot got it right on the first pass and applied it consistently across all six service Dockerfiles and the Docker Compose dependency graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transcribing XGBoost weights into TypeScript
&lt;/h3&gt;

&lt;p&gt;My GovHack notebooks had a trained XGBoost model for fire risk prediction. Rather than running a Python sidecar in Docker, I wanted the prediction logic native to the Node service — self-contained, no cross-language IPC.&lt;/p&gt;

&lt;p&gt;I showed Copilot the notebook's feature importances and coefficient output. It generated a TypeScript heuristic engine that replicated the same decision logic: weighted inputs for temperature, humidity, wind speed, vegetation density, and season; thresholds mapped to severity levels; confidence intervals from the training accuracy. Same model, different runtime. The prediction output matched what the notebook produced for the same inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rebuilding the entire UI from scratch
&lt;/h3&gt;

&lt;p&gt;Emergency UX has specific demands — high contrast, large touch targets, one-handed mobile use, offline indicators, priority-tiered alert behaviour. None of the original components was built with any of that in mind.&lt;/p&gt;

&lt;p&gt;Copilot rebuilt the entire frontend systematically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design system&lt;/strong&gt; — a new &lt;code&gt;index.css&lt;/code&gt; with semantic CSS variables (&lt;code&gt;--c-critical&lt;/code&gt;, &lt;code&gt;--c-high&lt;/code&gt;, &lt;code&gt;--c-medium&lt;/code&gt;, &lt;code&gt;--c-safe&lt;/code&gt;, spacing tokens, radius tokens, &lt;code&gt;--nav-w&lt;/code&gt; for sidebar width, &lt;code&gt;--header-h&lt;/code&gt; for page header clearance) and glassmorphism card styles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Left sidebar nav&lt;/strong&gt; — rebuilt navigation from a mobile bottom bar to a 220px fixed left sidebar with icon + label rows, an active state indicator bar, online/offline status dot, and AI assistant + Settings links pinned at the bottom; collapses to a 60px icon-only rail at &lt;code&gt;&amp;lt; 768px&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page headers with back navigation&lt;/strong&gt; — every screen except the dashboard has a sticky &lt;code&gt;52px&lt;/code&gt; page header with a &lt;code&gt;ChevronLeft&lt;/code&gt; back button that calls &lt;code&gt;navigate(-1)&lt;/code&gt;, giving the app proper browser-history navigation without a router-level back stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web app layout&lt;/strong&gt; — removed the &lt;code&gt;430px&lt;/code&gt; phone-shell constraint and switched the root layout to &lt;code&gt;display: flex; flex-direction: row&lt;/code&gt; so the sidebar and content fill the viewport side by side; no &lt;code&gt;max-width&lt;/code&gt; on the main content area&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency dashboard&lt;/strong&gt; — risk severity banner with animated pulse on CRITICAL, two-column grid layout (map + stat tiles on the left, live alerts + nearest safe spaces on the right at &lt;code&gt;380px&lt;/code&gt;), sticky CTA buttons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert overlay&lt;/strong&gt; — full-screen takeover for CRITICAL priority alerts with haptic-style dismiss, priority-tiered behaviour (CRITICAL → full screen, HIGH → persistent, MEDIUM → toast, LOW → feed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three new screens&lt;/strong&gt; — Onboarding (4-step permissions flow), Settings (toggles, emergency contacts, accessibility), AI Assistant (chat interface — now backed by Nous Hermes 2 via Ollama)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All components used the design token system consistently — no hardcoded hex values, no magic numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atlas data enrichment and real open data
&lt;/h3&gt;

&lt;p&gt;The static seed data was a starting point, not a destination. I wanted the platform to pull from the best available Australian open data sources at startup — live fire detections, real evacuation assembly points, current government grants.&lt;/p&gt;

&lt;p&gt;Copilot initially wrote three &lt;code&gt;atlasEnrich.ts&lt;/code&gt; files using Geoscience Australia's Digital Atlas ArcGIS REST endpoints — a good foundation. But GA polygon data only goes so far, especially for fire detection where you want sub-3-hour satellite observations. So I went further.&lt;/p&gt;

&lt;p&gt;The final enrichment pipeline by service:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;safe-space-service&lt;/strong&gt; — GA Atlas (topographic facilities) → &lt;strong&gt;OpenStreetMap Overpass API&lt;/strong&gt; (no key required). The Overpass query targets &lt;code&gt;emergency=assembly_point&lt;/code&gt;, &lt;code&gt;emergency=evacuation_point&lt;/code&gt;, and &lt;code&gt;amenity=evacuation_centre&lt;/code&gt; nodes within the Australia bounding box. OSM has surprisingly comprehensive coverage of designated emergency assembly points that don't appear in government datasets. Accessibility tags (&lt;code&gt;wheelchair&lt;/code&gt;, &lt;code&gt;toilets_wheelchair&lt;/code&gt;, &lt;code&gt;parking_disabled&lt;/code&gt;, &lt;code&gt;pets&lt;/code&gt;) are read directly from OSM node tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;prediction-service&lt;/strong&gt; — A four-source priority cascade: &lt;strong&gt;NASA FIRMS VIIRS NRT satellite detections&lt;/strong&gt; (3-hour near-real-time, requires a free MAP_KEY) → &lt;strong&gt;VIC Emergency public JSON feed&lt;/strong&gt; (no key) → &lt;strong&gt;NSW RFS major incidents JSON feed&lt;/strong&gt; (no key) → GA vegetation polygon centroids → static fallback. The FIRMS response includes fire radiative power (&lt;code&gt;frp&lt;/code&gt;) in MW and string confidence values (&lt;code&gt;'l'&lt;/code&gt;, &lt;code&gt;'n'&lt;/code&gt;, &lt;code&gt;'h'&lt;/code&gt;) which map to numeric probabilities for the risk engine. If FIRMS data exists, the live state feeds supplement it. If nothing is reachable, the static seed ensures the service always starts with plausible data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;recommendation-service&lt;/strong&gt; — Curated seed of known recovery programs → GA Atlas community facilities → &lt;strong&gt;GrantConnect API&lt;/strong&gt; (&lt;code&gt;/v2/grants?keyword=bushfire+disaster&amp;amp;status=open&lt;/code&gt;, requires free API key). GrantConnect is the Australian Government's official grants database. The integration maps grant categories to the service's scenario taxonomy (&lt;code&gt;GRANT&lt;/code&gt;, &lt;code&gt;RECOVERY&lt;/code&gt;, &lt;code&gt;HOUSING&lt;/code&gt;, &lt;code&gt;HEALTH&lt;/code&gt;, &lt;code&gt;EMERGENCY&lt;/code&gt;) and deduplicates on &lt;code&gt;(scenario, title)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All three are fire-and-forget calls in each service's &lt;code&gt;start()&lt;/code&gt; — never blocking, never throwing, always falling back gracefully. Both API keys (&lt;code&gt;NASA_FIRMS_MAP_KEY&lt;/code&gt;, &lt;code&gt;GRANTCONNECT_API_KEY&lt;/code&gt;) are optional environment variables documented in &lt;code&gt;.env.example&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging the ON CONFLICT DO NOTHING silent failure
&lt;/h3&gt;

&lt;p&gt;After wiring up the enrichment layer, the recommendation and safe-space services kept inserting duplicate rows on every restart. The &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; clause was there — visually it looked correct. But it silently did nothing.&lt;/p&gt;

&lt;p&gt;The root cause: every INSERT was using &lt;code&gt;gen_random_uuid()&lt;/code&gt; as the primary key. UUID PKs are always unique by definition, so &lt;code&gt;ON CONFLICT&lt;/code&gt; on the PK could never trigger. There was no other unique constraint, so duplicates streamed in on every startup.&lt;/p&gt;

&lt;p&gt;The fix was two-part: add &lt;code&gt;UNIQUE&lt;/code&gt; constraints on natural keys (&lt;code&gt;name&lt;/code&gt; for safe spaces, &lt;code&gt;(scenario, title)&lt;/code&gt; for recommendations) idempotently in the service's &lt;code&gt;init()&lt;/code&gt; via a &lt;code&gt;DO $$ BEGIN ... EXCEPTION WHEN duplicate_table THEN NULL; END $$&lt;/code&gt; block, then update every INSERT — both seed and enrichment — to specify the column: &lt;code&gt;ON CONFLICT (name) DO NOTHING&lt;/code&gt;. Copilot caught that the same pattern existed in all three &lt;code&gt;atlasEnrich.ts&lt;/code&gt; files and fixed them in one pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging the amqplib type breaking change
&lt;/h3&gt;

&lt;p&gt;Mid-rebuild, four services stopped compiling. The &lt;code&gt;amqplib&lt;/code&gt; types had changed between versions — &lt;code&gt;Connection&lt;/code&gt; became &lt;code&gt;ChannelModel&lt;/code&gt;, and import paths shifted. The errors were spread across &lt;code&gt;bus.ts&lt;/code&gt; in every service that used the message broker.&lt;/p&gt;

&lt;p&gt;Rather than hunting them down one by one, Copilot identified the pattern across all four files at once and applied the fix consistently: updated import paths, replaced &lt;code&gt;Connection&lt;/code&gt; with &lt;code&gt;ChannelModel&lt;/code&gt;, added the &lt;code&gt;.channel&lt;/code&gt; access where the API had changed. What would have been 20 minutes of grep-and-fix was one operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The quality multiplier
&lt;/h3&gt;

&lt;p&gt;The pattern I noticed across all of this: &lt;strong&gt;Copilot's output quality scaled directly with the quality of my specifications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Vague prompt → vague result. But when the architecture was documented, the event contracts were defined, and the patterns were established in real code — Copilot could work within all of those constraints simultaneously. It wasn't generating generic Express boilerplate. It was generating Express code that matched the correlation ID middleware pattern, used the shared logger with the right field names, published events with the correct envelope shape, and returned errors in the &lt;code&gt;{ code, message }&lt;/code&gt; format defined in the OpenAPI spec.&lt;/p&gt;

&lt;p&gt;That's the real leverage. Not autocomplete. &lt;strong&gt;Specification → conforming implementation at speed.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;There's a whole graveyard of hackathon projects in GitHub repositories everywhere.&lt;/p&gt;

&lt;p&gt;Most of them deserved more time than they got.&lt;/p&gt;

&lt;p&gt;Project Haven started as a 46-hour sprint, won a competition, and then sat untouched for almost two years. This challenge gave me the push to go back and ask: what would this actually look like if it was built properly?&lt;/p&gt;

&lt;p&gt;The answer turned out to be: a lot better.&lt;/p&gt;

&lt;p&gt;The prediction pipeline works end-to-end. The offline-first architecture actually functions. The microservices actually talk to each other through real events. The UI is a full-width web application — not a phone simulator in a browser — with proper sidebar navigation, back-button routing, and a two-column dashboard that uses screen real estate the way a desktop tool should. Live fire detections from NASA satellites, real Australian evacuation assembly points from OpenStreetMap, and current government grants from GrantConnect flow in at startup. The AI Assistant — previously a &lt;code&gt;setTimeout&lt;/code&gt; and a &lt;code&gt;switch&lt;/code&gt; statement of canned responses — is now backed by Nous Hermes 2 running locally via Ollama, with a system prompt grounded in verified Australian emergency protocols. The prediction engine now implements the McArthur FFDI Mark 5 formula (the actual BOM standard) blended with an XGBoost model trained from real GA historical fire data — running as a Python FastAPI sidecar built into Docker — replacing the made-up weights from the hackathon. The whole thing runs with a single &lt;code&gt;docker compose up --build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of that would have happened at this pace working solo without Copilot. Not because any individual piece was impossible — but because the sheer volume of consistent, conforming implementation across six services, a shared module, a full UI rebuild, a data enrichment layer across four distinct external APIs, and edge cases like the &lt;code&gt;ON CONFLICT&lt;/code&gt; silent failure would have taken weeks of context-switching. With Copilot holding the system context and working within the established patterns, it compressed into days.&lt;/p&gt;

&lt;p&gt;I'm not saying it's done. There's still proper auth flows, load testing, mobile push notifications, and a dozen other things on the list.&lt;/p&gt;

&lt;p&gt;But it's no longer frozen in time as a hackathon demo.&lt;/p&gt;

&lt;p&gt;It feels like something that could actually help someone.&lt;/p&gt;

&lt;p&gt;And that's probably the best thing this challenge could have done.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with TypeScript, React, Node.js, Python (FastAPI + XGBoost), PostgreSQL, RabbitMQ, Docker, Ollama (Nous Hermes 2), and GitHub Copilot.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Originally born at GovHack 2024. Finally given room to grow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>machinelearning</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a Multi-Agent AI Tribunal with Gemma 4</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Fri, 15 May 2026 15:41:31 +0000</pubDate>
      <link>https://dev.to/ujja/i-built-a-multi-agent-ai-tribunal-with-gemma-4-3ood</link>
      <guid>https://dev.to/ujja/i-built-a-multi-agent-ai-tribunal-with-gemma-4-3ood</guid>
      <description>&lt;p&gt;What If AI Agents Put Each Other on Trial?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;HumanLayer is a multi-agent AI governance platform. It features specialised Gemma 4 agents that collaborate to review, challenge, and hold one another accountable. This approach differs from a single model that quietly makes all the decisions.&lt;/p&gt;

&lt;p&gt;Most governance tooling is written for people who already understand governance. I aimed to build a system that makes governance decisions understandable to the people they impact. I consider plain-English explainability a hard requirement, not simply a nice-to-have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overall Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────────┐
│                          Browser / Next.js 15 UI                            │
│          Upload Doc  ·  Start Tribunal  ·  Appeal  ·  Audit Trail           │
└────────────────────────────────┬────────────────────────────────────────────┘
                                 │  HTTPS + CSRF
┌────────────────────────────────▼────────────────────────────────────────────┐
│                        FastAPI Backend  (port 8000)                         │
│  REST API v1  ·  JWT Auth  ·  Rate Limiting  ·  Content Scan Middleware     │
│                                                                              │
│  ┌─────────────────────────┐     ┌──────────────────────────────────────┐  │
│  │   Governance Council    │     │       Constitutional Tribunal        │  │
│  │                         │     │                                      │  │
│  │  Security  ·  Ethics    │     │  Prosecutor  ·  Defender             │  │
│  │  Privacy   ·  Access.   │     │  Advocate    ·  Ethics               │  │
│  │  Audit                  │     │       ↓  3 debate rounds             │  │
│  │       ↓  parallel       │     │  AI Jury ×4  →  Governance Judge     │  │
│  │  Consensus Engine       │     │       ↓  constitutional ruling       │  │
│  └────────────┬────────────┘     └─────────────┬────────────────────────┘  │
│               └──────────────────┬──────────────┘                           │
│                                  │                                           │
│              ┌───────────────────▼──────────────────┐                       │
│              │          Celery Worker (async)        │                       │
│              │  tribunal · analysis · documents ·    │                       │
│              │  default queues · concurrency=2       │                       │
│              └───────────┬───────────────────────────┘                      │
└──────────────────────────┼──────────────────────────────────────────────────┘
                           │
          ┌────────────────┼────────────────────┐
          ▼                ▼                     ▼
┌──────────────┐  ┌─────────────────┐  ┌─────────────────────────────────┐
│  PostgreSQL  │  │  Redis (broker  │  │    Google AI Studio             │
│  (SQLAlchemy │  │  + result store)│  │                                 │
│   async)     │  └─────────────────┘  │  gemma-4-26b-a4b-it  (MoE)     │
│              │                       │  gemma-4-31b-it      (Dense)    │
│  Cases  ·    │  ┌─────────────────┐  │                                 │
│  Agents ·    │  │  Local Storage  │  │  ← resolves gemma4:2b/4b/9b/moe │
│  Audits ·    │  │  /uploads       │  │    → MoE at runtime             │
│  Precedents  │  └─────────────────┘  │  ← resolves gemma4:31b          │
└──────────────┘                       │    → Dense 31B at runtime       │
                                       └─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│              Observability  (OpenTelemetry → Prometheus → Grafana)          │
│              Dev Monitor on port 8001  ·  SSE log stream  ·  phase bars    │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The platform has two modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance Council&lt;/strong&gt; — Five agents check uploaded documents. They review policy docs, OAuth configs, onboarding flows, and architecture reports. They ensure compliance with GDPR, AI Act, WCAG 2.2, and ISO 27001 standards. They then use a consensus engine to create one governance verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constitutional Tribunal&lt;/strong&gt; — A full adversarial process: four agents present their cases in three debate rounds. An AI jury with four members checks the reasoning quality, and a Governance Judge delivers a constitutional ruling. The human can appeal and override at any point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────────────────────┐
│                        Constitutional Tribunal                                   │
│                                                                                  │
│  ┌─────────────────┐  ┌──────────────────────┐  ┌──────────────┐  ┌──────────┐ │
│  │ Security        │  │ Accessibility         │  │ Privacy      │  │ Ethics   │ │
│  │ Prosecutor      │  │ Defender              │  │ Advocate     │  │ Council  │ │
│  │ gemma4:31b      │  │ gemma4:2b             │  │ gemma4:9b    │  │ gemma4:  │ │
│  │                 │  │ · WCAG 2.2 AA         │  │ · GDPR Art.7 │  │ 31b      │ │
│  │ · OWASP Top 10  │  │ · Flesch-Kincaid      │  │ · Data min.  │  │ · Bias   │ │
│  │ · STRIDE model  │  │ · Plain-English       │  │ · Consent    │  │   ×8     │ │
│  │ · RBAC/OAuth    │  │   rewrite             │  │   validity   │  │   classes│ │
│  └────────┬────────┘  └──────────┬────────────┘  └──────┬───────┘  └────┬─────┘ │
│           └───────────────────────┴──────────────────────┴───────────────┘       │
│                              Phase 1: Opening Arguments (concurrent)             │
│                              Phase 2: Cross-Examination (each challenges all)    │
│                              Phase 3: Closing Arguments (full history injected)  │
│                                              │                                   │
│                              ┌───────────────▼────────────────────────┐         │
│                              │  AI Jury Panel  (×4 independent)       │         │
│                              │  gemma4:moe                            │         │
│                              │  · Evidence validity check             │         │
│                              │  · Logical consistency score           │         │
│                              │  · Hallucination risk flag             │         │
│                              │  · Constitutional alignment            │         │
│                              └───────────────┬────────────────────────┘         │
│                              Phase 4: Jury Deliberation + consensus score        │
│                              Phase 5: Trust scores + governance DSL rules        │
│                                              │                                   │
│                              ┌───────────────▼────────────────────────┐         │
│                              │  Governance Judge                      │         │
│                              │  gemma4:moe                            │         │
│                              │  approve · reject · escalate ·         │         │
│                              │  conditional (with remediation steps)  │         │
│                              └───────────────┬────────────────────────┘         │
│                              Phase 6: Constitutional Ruling                      │
│                                              │                                   │
│               ┌──────────────────────────────┼──────────────────────────┐       │
│               ▼                              ▼                          ▼       │
│      ┌────────────────┐          ┌───────────────────┐       ┌─────────────────┐│
│      │  Audit Agent   │          │  Human Appeal &amp;amp;   │       │  Precedent      ││
│      │  gemma4:4b     │          │  Override         │       │  Library        ││
│      │  Immutable     │          │  always available │       │  high-confidence││
│      │  audit trail   │          │  at any phase     │       │  cases stored   ││
│      └────────────────┘          └───────────────────┘       └─────────────────┘│
│      Phase 7: Audit + Precedent                                                  │
└──────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tribunal case runs 7 ordered phases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Opening Arguments&lt;/td&gt;
&lt;td&gt;All 4 adversarial agents argue concurrently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Cross-Examination&lt;/td&gt;
&lt;td&gt;Each agent challenges the others' positions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Closing Arguments&lt;/td&gt;
&lt;td&gt;Final positions with full debate history injected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Jury Deliberation&lt;/td&gt;
&lt;td&gt;4 independent validators score reasoning quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Trust + DSL Evaluation&lt;/td&gt;
&lt;td&gt;Trust scores updated; governance rules evaluated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Constitutional Ruling&lt;/td&gt;
&lt;td&gt;Judge issues approve/reject/escalate / conditional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Audit + Precedent&lt;/td&gt;
&lt;td&gt;Immutable audit written; high-confidence cases become precedents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Minority opinions are preserved at every phase. The human can appeal and override at any point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No agent can approve its own actions. Every verdict is traceable to a specific rationale chain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;: FastAPI + Celery + Redis · Next.js 15 · Google AI Studio (Gemma 4) · OpenTelemetry + Prometheus&lt;/p&gt;

&lt;h3&gt;
  
  
  Case Simulations
&lt;/h3&gt;

&lt;p&gt;The platform ships with 8 pre-built governance scenarios. Here are the most illustrative ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1 — Inaccessible MFA Crisis&lt;/strong&gt; &lt;em&gt;(accessibility vs security)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A bank mandates CAPTCHA + MFA after brute-force attacks. Security Prosecutor (&lt;code&gt;gemma4:31b&lt;/code&gt;) builds a rigorous OWASP case. Accessibility Defender (&lt;code&gt;gemma4:2b&lt;/code&gt;) flags that the CAPTCHA does not meet WCAG 2.2 SC 1.1.1. Also, the MFA UX creates cognitive barriers for users with anxiety. Ethics Council (&lt;code&gt;gemma4:31b&lt;/code&gt;) adds intersectional impact — disabled users are disproportionately locked out. The Jury detects that Security's argument implicitly assumes able-bodied users; consensus drops. Verdict: conditional approval — passkey auth with accessible fallback required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2 — Rogue Agent Self-Approval&lt;/strong&gt; &lt;em&gt;(constitutional violation)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An autonomous deployment agent tries to approve its own production push. This is a clear violation of the separation of duties. Security Prosecutor traces the approval chain; Audit Agent reconstructs the hidden trust-score history. The Judge checks three governance DSL rules and gives a hard reject without needing a jury. The constitutional violation is clear. This case is specifically designed to test the "no agent can approve its own actions" invariant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 3 — Manipulative Consent Flow&lt;/strong&gt; &lt;em&gt;(dark pattern detection)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A SaaS onboarding screen checks tracking consent. It hides the opt-out option three layers deep. Urgent language is used. Accessibility Defender (&lt;code&gt;gemma4:2b&lt;/code&gt;) has a Grade 14 Flesch-Kincaid score. It warns about cognitive overload. Privacy Advocate (&lt;code&gt;gemma4:9b&lt;/code&gt;) focuses on GDPR Article 7. It argues that pre-checked boxes do not give real consent. Ethics Council points out predatory targeting of low-literacy and elderly users. Verdict: reject. The Accessibility Agent rewrites the consent copy as a remediation artefact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 4 — Discriminatory Hiring Pipeline&lt;/strong&gt; &lt;em&gt;(AI bias review)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An AI hiring system rejects candidates from certain demographic groups at higher rates. The Ethics Council (&lt;code&gt;gemma4:31b&lt;/code&gt;) conducts intersectional analysis across 8 protected classes. It shows that resume formatting choices can reflect socioeconomic status. This can lead to proxy discrimination. Verdict: escalated — cannot approve or reject without a third-party fairness audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 5 — Translation Drift Crisis&lt;/strong&gt; &lt;em&gt;(multilingual governance failure)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A governance policy is translated into six languages. This change quietly affects its legal meaning in two of them. The term "data minimisation" becomes "data reduction" in one area. This leads to different legal implications. The Jury (&lt;code&gt;gemma4:moe&lt;/code&gt;) uses Gemma 4's long-context window. This helps it check semantic consistency in the entire translation diffs. Ethics Council (&lt;code&gt;gemma4:31b&lt;/code&gt;) identifies that the error would have changed user rights without notice. Verdict: conditional approval — re-translation with legal review required. This case shows why context window size is key. A text-snippet method would miss the cross-locale semantic drift. It wouldn't capture the differences at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;The main architectural choice was deciding which Gemma 4 variant to use for each role. This is important because each reasoning task needs a different model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model-per-role design
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Canonical model&lt;/th&gt;
&lt;th&gt;Why this size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility Agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:2b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast pattern recognition: reading level, WCAG checks, plain-English rewrites. Speed and empathy over depth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit Agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Neutral summarisation and multi-agent narrative coherence. Slightly more capable than 2b for timeline reconstruction.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Advocate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:9b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nuanced consent analysis across three adversarial debate rounds. Needs legal depth without the latency of 31b.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:31b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Context-dense threat modelling — holds full RBAC configs, OAuth flows, and STRIDE analysis simultaneously.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethics &amp;amp; Inclusion&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:31b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Intersectional bias reasoning across 8 protected classes. Detecting proxy discrimination requires the full model capacity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Agent + Jury (×4) + Judge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemma4:moe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Orchestration, meta-reasoning, cross-domain judgment. MoE's expert sub-networks activate per token type rather than averaging.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The canonical IDs are used throughout the codebase. Each inference backend turns them into physical model names at runtime. The agent code stays the same when you switch backends.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's actually running today
&lt;/h3&gt;

&lt;p&gt;In practice, only two Gemma 4 models are currently available via any working hosted API (Google AI Studio):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;&lt;/strong&gt; — Sparse MoE, 26B total / ~4B active params. Maps to the &lt;code&gt;2b&lt;/code&gt;, &lt;code&gt;4b&lt;/code&gt;, &lt;code&gt;9b&lt;/code&gt;, and &lt;code&gt;moe&lt;/code&gt; roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gemma-4-31b-it&lt;/code&gt;&lt;/strong&gt; — Dense 31B. Maps to security and ethics roles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model-resolution maps in the backend (&lt;code&gt;_GOOGLE_AI_MODEL_MAP&lt;/code&gt;) bridge the gap. &lt;code&gt;gemma4:2b&lt;/code&gt; resolves to &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; today and will resolve to the real 2b model the moment it's available — no code changes needed.&lt;/p&gt;

&lt;p&gt;Getting Gemma 4 running was a significant part of the project. Here's what I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt;: Gemma 4 isn't in the registry yet (404 error).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HuggingFace Serverless&lt;/strong&gt;: Tried 7 providers; all returned "Model not supported".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kaggle hosted inference&lt;/strong&gt;: No REST endpoint; token only allows downloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kaggle local download&lt;/strong&gt;: Works for &lt;code&gt;2b&lt;/code&gt;/&lt;code&gt;4b&lt;/code&gt; but CPU inference takes 30–120s per call — impractical for 10–15 agent calls per tribunal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AI Studio&lt;/strong&gt; — free API key, OpenAI-compatible endpoint, 1,500 req/day — was the one that worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why the model-sizing decisions matter
&lt;/h3&gt;

&lt;p&gt;Using the sparse MoE model for accessibility and the dense 31B for security isn't just a cost optimisation. It reflects a genuine difference like those reasoning tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt; (MoE)&lt;/strong&gt; — The Accessibility Agent quickly spots patterns with empathy. It estimates the Flesch-Kincaid reading level, checks for WCAG 2.2 AA compliance, finds shame-based error messages, and rewrites hostile content into plain English suitable for dyslexia at the Grade 8 level. These tasks don't require deep cross-referenced reasoning. They require speed and consistency. Sparse activation (~4B params per token) is exactly right.&lt;/p&gt;

&lt;p&gt;The same model handles the Audit Agent (neutral timeline reconstruction), Privacy Advocate (consent analysis across debate rounds), and the full Jury Panel + Governance Judge (meta-reasoning, cross-domain orchestration). MoE's expert sub-network routing means different token types activate different specialists rather than averaging across all domains simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gemma-4-31b-it&lt;/code&gt; (Dense 31B)&lt;/strong&gt; — The Security Agent must manage the full RBAC setup, OAuth token flow, redirect URIs, and STRIDE threat model all at once. The Ethics Agent must consider 8 protected classes. They look for proxy discrimination when a policy seems neutral but harms a protected group. Both tasks require the kind of multi-document, cross-referenced reasoning that the dense 31B handles substantially better than smaller variants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Gemma 4 capabilities that made this possible
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Long-context window.&lt;/strong&gt; A single tribunal case feeds the full debate history (all prior round outputs) into each jury agent. Without a long-context window, jury agents would miss the cross-examination context. This context is key for evaluating reasoning quality effectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal input.&lt;/strong&gt; A significant portion of governance artefacts aren't text — they're screenshots of onboarding flows, consent screens, and admin dashboards. Agents can check visual accessibility patterns and CAPTCHA flows. They can also spot UI governance risks. A text-only model would miss these details. The multilingual governance simulation does this: agents look at screenshots of translated policy text side by side. They check for layout differences. These differences can affect readability in different places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning mode.&lt;/strong&gt; The &lt;code&gt;&amp;lt;thought&amp;gt;&lt;/code&gt; tags in Gemma 4's output are removed before parsing the JSON response. However, the reasoning process is key to the quality of analysis in complex cases. This is especially true for detecting jury hallucinations and checking constitutional alignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;p&gt;Disagreement is a feature, not a bug. Most AI systems optimise for confident, singular answers. HumanLayer deliberately surfaces disagreement — between agents, across debate rounds, in the audit trail. That visibility turns out to be the most useful part, because it shows users why a decision landed where it did.&lt;/p&gt;

&lt;p&gt;When smaller Gemma 4 variants become available via the API, the per-role assignment will get even more precise. The architecture is already waiting for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Note: The app currently runs locally.&lt;br&gt;
Demo video URL: &lt;a href="https://www.youtube.com/watch?v=pfUncccezQA" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=pfUncccezQA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fni99ex8lmkkzth4v8qmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fni99ex8lmkkzth4v8qmc.png" alt="Constitutional Proceeding"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tv4bfuqikx30dourrum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tv4bfuqikx30dourrum.png" alt="Opening Arguments"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20xfmsodq4l767z2xlnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20xfmsodq4l767z2xlnr.png" alt="Cross - Exam"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4anoi0t8s28o9byzu81n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4anoi0t8s28o9byzu81n.png" alt="Agent Closing Argument"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Repository: &lt;a href="https://github.com/ujjavala/HumanLayer" rel="noopener noreferrer"&gt;https://github.com/ujjavala/HumanLayer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architecture Docs: &lt;a href="https://github.com/ujjavala/HumanLayer/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/ujjavala/HumanLayer/tree/main/docs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;One question stayed with me throughout this project:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If AI systems become influential enough to shape governance decisions, who governs the governors?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HumanLayer is one answer: make the AI systems govern each other, transparently, with human override always available. Expose disagreement instead of hiding it. Treat accessibility as a governance requirement. Build audit trails that explain decisions to the people they affect, not just to the compliance team.&lt;/p&gt;

&lt;p&gt;Trustworthy AI will probably look less like all-knowing superintelligence and more like collaborative systems designed to keep each other accountable.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Rethought My Sustainability App After Google Cloud Next ’26 — and my architecture broke (in a good way)</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Mon, 27 Apr 2026 08:04:12 +0000</pubDate>
      <link>https://dev.to/ujja/i-rethought-planetledger-after-google-cloud-next-2026-and-my-architecture-broke-in-a-good-way-glp</link>
      <guid>https://dev.to/ujja/i-rethought-planetledger-after-google-cloud-next-2026-and-my-architecture-broke-in-a-good-way-glp</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days ago, I shipped &lt;strong&gt;&lt;a href="https://dev.to/ujja/planetledger-turning-spending-into-environmental-awareness-4b8f"&gt;PlanetLedger&lt;/a&gt;&lt;/strong&gt; — a weekend hackathon project that turns bank transactions into environmental impact insights.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an event-driven pipeline (OpenClaw)
&lt;/li&gt;
&lt;li&gt;an agent layer with memory
&lt;/li&gt;
&lt;li&gt;RAG-grounded insights
&lt;/li&gt;
&lt;li&gt;deterministic scoring + AI fallback
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, it looks like a modern AI system.&lt;/p&gt;

&lt;p&gt;Then I watched Google Cloud NEXT ‘26.&lt;/p&gt;

&lt;p&gt;And something didn’t sit right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable realisation
&lt;/h2&gt;

&lt;p&gt;While going through the announcements —&lt;br&gt;&lt;br&gt;
the &lt;strong&gt;Gemini Enterprise Agent Platform&lt;/strong&gt;, &lt;strong&gt;Agentic Data Cloud&lt;/strong&gt;, and long-running autonomous agents — I had this thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I didn’t build an AI system.&lt;br&gt;&lt;br&gt;
I built a pipeline that treats AI as the final step — not the decision-maker.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;It’s not.&lt;/p&gt;

&lt;h2&gt;
  
  
  My architecture (before NEXT)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bfkrzpc4t4xszw0ghw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bfkrzpc4t4xszw0ghw0.png" alt="old" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PlanetLedger today works like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;upload → parse → categorise → score → build context → generate insights → notify  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Internally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;event → &lt;a href="https://dev.to/ujja/i-built-my-own-event-bus-for-a-sustainability-app-heres-what-i-learned-about-agent-automation-2cfl"&gt;OpenClaw → workflows&lt;/a&gt; → AI → UI  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s clean, predictable, and works well.&lt;/p&gt;

&lt;p&gt;But it’s also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fully deterministic until the very last step  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI is where the pipeline ends — not where decisions begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 What NEXT ‘26 changes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5p5k2h5kbocc21pvh5o7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5p5k2h5kbocc21pvh5o7.png" alt="new" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The biggest shift across announcements wasn’t better models.&lt;/p&gt;

&lt;p&gt;It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We’ve entered the agentic era.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI is no longer something you call.&lt;/p&gt;

&lt;p&gt;It’s something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;acts
&lt;/li&gt;
&lt;li&gt;reasons over data
&lt;/li&gt;
&lt;li&gt;runs workflows autonomously
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three announcements made that click for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Gemini Enterprise Agent Platform&lt;/strong&gt; → build and scale real agents
&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Agentic Data Cloud&lt;/strong&gt; → agents reason directly over structured data
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-running agents in serverless environments&lt;/strong&gt; → agents don’t just respond, they operate
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this actually looks like on Google Cloud
&lt;/h2&gt;

&lt;p&gt;Mapping my system to Google Cloud made the gap obvious:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I built&lt;/th&gt;
&lt;th&gt;Google Cloud direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/ujja/i-built-my-own-event-bus-for-a-sustainability-app-heres-what-i-learned-about-agent-automation-2cfl"&gt;OpenClaw event triggers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pub/Sub / Eventarc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded workflows&lt;/td&gt;
&lt;td&gt;Workflows / agent execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG context builder&lt;/td&gt;
&lt;td&gt;Agentic Data Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM calls for insights&lt;/td&gt;
&lt;td&gt;Gemini agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cron-based automation&lt;/td&gt;
&lt;td&gt;Long-running autonomous agents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;What I built locally is essentially a &lt;strong&gt;proto-version of a cloud-native agent system — but missing the intelligence layer at the core&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reimagining PlanetLedger
&lt;/h2&gt;

&lt;p&gt;So I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if PlanetLedger wasn’t a pipeline… but an agent?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. Events → from triggers to signals
&lt;/h3&gt;

&lt;p&gt;Today:&lt;br&gt;
transactions_uploaded → trigger workflows&lt;/p&gt;

&lt;p&gt;In an agent-first system:&lt;br&gt;
transactions_uploaded → agent decides what to do&lt;/p&gt;

&lt;p&gt;Same event.&lt;br&gt;&lt;br&gt;
Completely different meaning.&lt;/p&gt;

&lt;p&gt;Events stop being instructions.&lt;/p&gt;

&lt;p&gt;They become &lt;strong&gt;inputs for reasoning&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pipeline → replaced by a Financial Agent
&lt;/h3&gt;

&lt;p&gt;Today, I explicitly define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parse
&lt;/li&gt;
&lt;li&gt;categorise
&lt;/li&gt;
&lt;li&gt;score
&lt;/li&gt;
&lt;li&gt;generate insights
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an agent-based system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Financial Agent:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understands transactions
&lt;/li&gt;
&lt;li&gt;detects patterns
&lt;/li&gt;
&lt;li&gt;decides what matters
&lt;/li&gt;
&lt;li&gt;chooses actions
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“run this sequence”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“figure out what needs to happen”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. RAG → becomes native data reasoning
&lt;/h3&gt;

&lt;p&gt;Right now, I manually construct context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;last 7 days
&lt;/li&gt;
&lt;li&gt;top categories
&lt;/li&gt;
&lt;li&gt;detected patterns
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then inject it into prompts.&lt;/p&gt;

&lt;p&gt;With something like the &lt;strong&gt;Agentic Data Cloud&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the agent queries data directly
&lt;/li&gt;
&lt;li&gt;builds its own context
&lt;/li&gt;
&lt;li&gt;adapts dynamically
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Less glue code. More intelligence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Workflows → become optional
&lt;/h3&gt;

&lt;p&gt;OpenClaw is intentionally simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sequential
&lt;/li&gt;
&lt;li&gt;deterministic
&lt;/li&gt;
&lt;li&gt;easy to debug
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it’s still:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;explicit orchestration  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The direction from NEXT suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long-running agents
&lt;/li&gt;
&lt;li&gt;dynamic tool usage
&lt;/li&gt;
&lt;li&gt;adaptive execution paths
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;step A → step B → step C  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;goal → agent decides steps → executes tools  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More powerful.&lt;br&gt;&lt;br&gt;
Also harder to control.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The new problem: trust
&lt;/h3&gt;

&lt;p&gt;In my current system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI generates insights
&lt;/li&gt;
&lt;li&gt;but doesn’t act
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an agent system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI can trigger workflows
&lt;/li&gt;
&lt;li&gt;influence decisions
&lt;/li&gt;
&lt;li&gt;shape outcomes
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which introduces something new:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You now need to trust your architecture — not just your code.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;validation layers
&lt;/li&gt;
&lt;li&gt;auditability
&lt;/li&gt;
&lt;li&gt;explainability
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially for something tied to financial behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real shift
&lt;/h2&gt;

&lt;p&gt;If I compress everything I learned into one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I went from designing workflows → to designing decision boundaries&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Old vs New mental model
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7tazbvqt08ptq5xi8i8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7tazbvqt08ptq5xi8i8.png" alt="old-vs-new" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PlanetLedger Today&lt;/th&gt;
&lt;th&gt;PlanetLedger (NEXT-style)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event triggers workflows&lt;/td&gt;
&lt;td&gt;Event triggers reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline-first&lt;/td&gt;
&lt;td&gt;Agent-first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG context builder&lt;/td&gt;
&lt;td&gt;Native data reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic flow&lt;/td&gt;
&lt;td&gt;Adaptive execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insights&lt;/td&gt;
&lt;td&gt;Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I’d do next
&lt;/h2&gt;

&lt;p&gt;If I were to rebuild PlanetLedger today using these ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduce an agent layer using Gemini-style reasoning
&lt;/li&gt;
&lt;li&gt;Let the agent decide when to generate insights vs alerts
&lt;/li&gt;
&lt;li&gt;Replace static RAG with dynamic data querying
&lt;/li&gt;
&lt;li&gt;Add explainability for every AI-driven decision
&lt;/li&gt;
&lt;li&gt;Keep events — but demote them to signals, not drivers
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not because the current system is wrong.&lt;/p&gt;

&lt;p&gt;But because the direction is clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The future isn’t event-driven systems &lt;em&gt;with AI&lt;/em&gt;&lt;br&gt;&lt;br&gt;
It’s AI systems that &lt;em&gt;use events&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Google Cloud NEXT ‘26 didn’t just introduce new tools.&lt;/p&gt;

&lt;p&gt;It exposed a shift:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We’re moving from systems that process data&lt;br&gt;&lt;br&gt;
to systems that interpret and act on it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;PlanetLedger didn’t break after NEXT.&lt;/p&gt;

&lt;p&gt;But the way I think about building it did.&lt;/p&gt;

&lt;p&gt;And that’s a much bigger change.&lt;/p&gt;

&lt;p&gt;If you’ve built something similar — pipelines, workflows, event buses — try this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remove the pipeline.&lt;br&gt;&lt;br&gt;
Replace it with an agent.&lt;br&gt;&lt;br&gt;
See what breaks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s probably where the next version lives.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
      <category>agents</category>
    </item>
    <item>
      <title>I built my own event bus for a sustainability app — here's what I learned about agent automation using OpenClaw</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:52:17 +0000</pubDate>
      <link>https://dev.to/ujja/i-built-my-own-event-bus-for-a-sustainability-app-heres-what-i-learned-about-agent-automation-2cfl</link>
      <guid>https://dev.to/ujja/i-built-my-own-event-bus-for-a-sustainability-app-heres-what-i-learned-about-agent-automation-2cfl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/openclaw-2026-04-16"&gt;OpenClaw Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/ujja/planetledger-turning-spending-into-environmental-awareness-4b8f"&gt;PlanetLedger&lt;/a&gt;&lt;/strong&gt; is a sustainability finance dashboard that turns your bank statements into environmental intelligence. You upload a CSV or PDF statement, and the app automatically categorises every transaction, calculates a planet impact score, and fires off a chain of automated workflows — all powered by an event bus I called &lt;strong&gt;OpenClaw&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple: most people have no idea whether their everyday spending is environmentally terrible or not. PlanetLedger makes that visible in 10 seconds, without a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used OpenClaw
&lt;/h2&gt;

&lt;p&gt;OpenClaw is the event-driven automation layer I built into PlanetLedger. It's intentionally lightweight — no external queue, no infrastructure, just a typed event bus that decouples "something happened" from "here's what to do about it."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnduqx82lo6sp84p62nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnduqx82lo6sp84p62nt.png" alt="Openclaw Diagram" width="798" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Design
&lt;/h3&gt;

&lt;p&gt;The whole thing is about 60 lines across four files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/openclaw/
  types.ts      ← event + trigger types
  registry.ts   ← register triggers, fire events
  trigger.ts    ← called from API routes
  workflows.ts  ← the actual automation logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Types&lt;/strong&gt; — everything is typed so workflows know exactly what data they get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score_calculated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;insights_generated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score_improved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly_report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high_impact_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;behavioral_pattern_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEventType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OpenClawTrigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&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;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Registry&lt;/strong&gt; — a simple map of event types to arrays of trigger functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fireOpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Registration&lt;/strong&gt; happens at module load time — the registry file imports and wires up all four workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoInsightOnUpload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highImpactAlert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly_report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weeklyReport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score_improved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scoreImprovedCelebration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firing events&lt;/strong&gt; from an API route is now one call that chains the whole pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/upload/route.ts — after parsing + storing transactions:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openClawChainedTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previousScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;openClawChainedTrigger&lt;/code&gt; fires four events in sequence: &lt;code&gt;transactions_uploaded → score_calculated → insights_generated → score_improved&lt;/code&gt; (only if the score actually increased). The upload route returns the parsed data to the client immediately — the whole chain runs after the response is already on its way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Workflows
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Workflow 1 — &lt;code&gt;autoInsightOnUpload&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fires on every &lt;code&gt;transactions_uploaded&lt;/code&gt; event. It fetches the just-stored transactions, runs &lt;code&gt;buildRagContext()&lt;/code&gt; to pull together the most relevant spend signals (top categories by spend, top merchants, recent patterns), then calls &lt;code&gt;buildAgentInsights()&lt;/code&gt; to generate personalised recommendations. The result gets cached so the insights panel loads instantly when the user navigates to the dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;autoInsightOnUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ragContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildRagContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildAgentInsights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ragContext&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setCachedInsights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;insights&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RAG context builder is what makes this interesting — instead of sending all transactions to the insight engine, it distills them into the most useful signals first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last 7 days of transactions (or all if none in the last week)&lt;/li&gt;
&lt;li&gt;Top 2 categories by total spend&lt;/li&gt;
&lt;li&gt;Detected behavioural patterns (e.g. "3+ food delivery orders this week")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Merchant names are intentionally excluded from the RAG context — they're PII-adjacent and the insight engine doesn't need them to produce useful recommendations.&lt;/p&gt;

&lt;p&gt;This grounding is what makes the insights feel specific rather than generic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow 2 — &lt;code&gt;highImpactAlert&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also fires on &lt;code&gt;transactions_uploaded&lt;/code&gt;. It checks if the impact score is low and, if so, pushes a structured notification directly to the user's in-app notification bell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;highImpactAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pseudonymize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impactScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;pushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high_impact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;High-Impact Alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highImpactCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; high-impact transactions detected this week (score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impactScore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100).`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The notification lands in the dashboard bell immediately — no email, no external service, no infrastructure. &lt;code&gt;pseudonymize()&lt;/code&gt; wraps the userId in an FNV-1a hash (&lt;code&gt;usr_XXXXXXXX&lt;/code&gt;) before it touches any log or store, so PII never leaks into structured outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow 3 — &lt;code&gt;weeklyReport&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fires on &lt;code&gt;weekly_report&lt;/code&gt; events, triggered by Vercel Cron every Monday at 09:00 UTC via &lt;code&gt;POST /api/cron/weekly-report&lt;/code&gt;. It generates a full digest and pushes it to the notification bell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;weeklyReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;insights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCachedInsights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;buildAgentInsights&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="nf"&gt;pushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly_report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Weekly Report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impactScore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100 · Spend: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalSpend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt; · Trend: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weeklyTrend&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vercel.json&lt;/code&gt; cron config:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"crons"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/cron/weekly-report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 9 * * 1"&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="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;The endpoint validates a &lt;code&gt;Bearer CRON_SECRET&lt;/code&gt; header, iterates over &lt;code&gt;CRON_USER_IDS&lt;/code&gt; from env, and fires a &lt;code&gt;weekly_report&lt;/code&gt; event per user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow 4 — &lt;code&gt;scoreImprovedCelebration&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fires on the &lt;code&gt;score_improved&lt;/code&gt; event, which &lt;code&gt;openClawChainedTrigger&lt;/code&gt; emits when the new score is higher than the score before the upload. It pushes a celebration notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreImprovedCelebration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenClawEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;pushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score_improved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Score Improved 🌱&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Your eco score improved to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;newScore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100 — great progress!`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the workflow that closes the feedback loop. Upload → pipeline runs → score improves → bell lights up. All in-process, no round trips.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple Workflows on the Same Event + Chained Pipeline
&lt;/h3&gt;

&lt;p&gt;One of the things I like about the registry approach: you can register multiple triggers for the same event type and they all fire sequentially. Both &lt;code&gt;autoInsightOnUpload&lt;/code&gt; and &lt;code&gt;highImpactAlert&lt;/code&gt; fire on &lt;code&gt;transactions_uploaded&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoInsightOnUpload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerOpenClawTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_uploaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highImpactAlert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;scoreImprovedCelebration&lt;/code&gt; on &lt;code&gt;score_improved&lt;/code&gt; was one more &lt;code&gt;registerOpenClawTrigger&lt;/code&gt; call. Zero changes to upload logic, zero changes to existing workflows.&lt;/p&gt;

&lt;p&gt;The chained pipeline takes this further — instead of firing one event and hoping downstream workflows pick it up, &lt;code&gt;openClawChainedTrigger&lt;/code&gt; fires a deliberate sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transactions_uploaded
  → score_calculated   (stores the new score)
  → insights_generated (triggers autoInsightOnUpload)
  → score_improved     (only if score increased — triggers celebration)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step in the chain passes context forward via &lt;code&gt;payload&lt;/code&gt;, so later workflows know exactly what the previous step produced. It's a lightweight saga pattern without any infrastructure.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Decoupling is worth the extra file.&lt;/strong&gt; The upload API route doesn't know anything about insights, alerts, or reports. It parses, stores, fires one chained trigger, and returns. That separation made it much easier to iterate — I could change how insights are generated without touching the upload logic at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your events well.&lt;/strong&gt; &lt;code&gt;transactions_uploaded&lt;/code&gt; is clear. &lt;code&gt;custom&lt;/code&gt; is a trap — you end up using it for everything and lose the ability to filter. Adding &lt;code&gt;score_calculated&lt;/code&gt;, &lt;code&gt;insights_generated&lt;/code&gt;, and &lt;code&gt;score_improved&lt;/code&gt; as first-class event types made the chained pipeline readable and debuggable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured payloads &amp;gt; strings.&lt;/strong&gt; Early versions of the alert workflow just logged a string message. Switching to a structured object with &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;impactScore&lt;/code&gt;, &lt;code&gt;highImpactCount&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; — and routing it to &lt;code&gt;pushNotification()&lt;/code&gt; — made the output immediately usable in the UI with no reformatting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pseudonymize before logging.&lt;/strong&gt; Passing raw user IDs into structured logs is a habit that causes problems at scale. Wrapping every &lt;code&gt;userId&lt;/code&gt; in &lt;code&gt;pseudonymize()&lt;/code&gt; (FNV-1a → &lt;code&gt;usr_XXXXXXXX&lt;/code&gt;) before it touches a log or store is a one-line fix that's easier to do from the start than to retrofit later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The in-process event bus is underrated for early-stage apps.&lt;/strong&gt; It's not RabbitMQ, it doesn't survive server restarts, and you can't replay events. But it gave me the same workflow separation patterns you'd get from a proper queue — at zero infrastructure cost. When it's time to scale, the migration path is clear: replace &lt;code&gt;fireOpenClawEvent&lt;/code&gt; with an enqueue call and move the workflow functions to workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenClaw as an extension point.&lt;/strong&gt; The event types I haven't fully wired to UI yet (&lt;code&gt;behavioral_pattern_detected&lt;/code&gt;) are sitting there ready. Future workflows — goal progress updates, peer benchmarks, proactive nudges — all land in &lt;code&gt;workflows.ts&lt;/code&gt; with zero changes to application routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClawCon Michigan
&lt;/h2&gt;

&lt;p&gt;I didn't attend ClawCon Michigan this time, but I'd love to next year — especially to talk through event-driven patterns in AI agent systems. A lot of the OpenClaw design decisions (typed events, sequential trigger execution, structured alert payloads) came from thinking about how agent workflows differ from traditional job queues, and that feels like exactly the kind of conversation that would thrive at an in-person event.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>openclawchallenge</category>
      <category>ai</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>I built PlanetLedger — an app that reveals the environmental impact hidden in your transactions.</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 18 Apr 2026 16:44:53 +0000</pubDate>
      <link>https://dev.to/ujja/planetledger-turning-spending-into-environmental-awareness-4b8f</link>
      <guid>https://dev.to/ujja/planetledger-turning-spending-into-environmental-awareness-4b8f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;PlanetLedger&lt;/strong&gt; — an AI-powered sustainability finance dashboard that turns your bank statements into environmental intelligence.&lt;/p&gt;

&lt;p&gt;You upload a CSV or PDF bank statement, and an agent pipeline automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses every transaction and categorises it against 20+ real Australian vendors (Woolworths, Chemist Warehouse, Didi, City Chic, Skechers, Aldi, IGA and more)&lt;/li&gt;
&lt;li&gt;Assigns a &lt;strong&gt;planet impact score&lt;/strong&gt; (GREEN / YELLOW / RED) per transaction based on category and spend size&lt;/li&gt;
&lt;li&gt;Aggregates spend into a &lt;strong&gt;category breakdown&lt;/strong&gt; — Fast Fashion, Grocery, Transport, Electronics, Hygiene, Food Delivery&lt;/li&gt;
&lt;li&gt;Runs a &lt;strong&gt;RAG-grounded insight pipeline&lt;/strong&gt; that generates personalised recommendations using your actual spending context&lt;/li&gt;
&lt;li&gt;Shows a &lt;strong&gt;gamified Planet Impact Score&lt;/strong&gt; dial (0–100) with unlockable badges (Conscious Spender → Low Impact Week → Planet Saver)&lt;/li&gt;
&lt;li&gt;Lets you chat with your personal agent ("Why is my impact high? What should I cut?")&lt;/li&gt;
&lt;li&gt;Runs a &lt;strong&gt;What-If Simulator&lt;/strong&gt; — drag a slider and instantly see how cutting food delivery by 30% changes your score&lt;/li&gt;
&lt;li&gt;Tracks a &lt;strong&gt;memory timeline&lt;/strong&gt; so the agent learns your patterns week over week&lt;/li&gt;
&lt;li&gt;Has a public &lt;strong&gt;/demo page&lt;/strong&gt; — no login required, try it with sample AU data before signing up&lt;/li&gt;
&lt;li&gt;The sample data is Australian, but &lt;strong&gt;any CSV using the &lt;code&gt;Date / Transaction / Debit / Credit / Balance&lt;/code&gt; schema works&lt;/strong&gt; — regardless of which bank or country it comes from&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal: make the invisible environmental cost of everyday spending visible and actionable — in seconds, not spreadsheets.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fumpozxfloqo9uzo0gl5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fumpozxfloqo9uzo0gl5z.png" alt="Architecture Diagram" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 15.5 App Router + TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Auth0 v4 (@auth0/nextjs-auth0) — middleware-based, zero route handlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI / LLM&lt;/td&gt;
&lt;td&gt;LangChain + OpenAI gpt-4o-mini (primary) / Google Gemini gemini-1.5-flash (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules engine&lt;/td&gt;
&lt;td&gt;Custom categorisation + scoring — deterministic, explainable, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG layer&lt;/td&gt;
&lt;td&gt;Context builder that grounds prompts in real spend data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF parsing&lt;/td&gt;
&lt;td&gt;pdf-parse v2 + AU bank statement regex extractor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSV parsing&lt;/td&gt;
&lt;td&gt;papaparse + AU format (Date / Transaction / Debit / Credit / Balance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event bus&lt;/td&gt;
&lt;td&gt;OpenClaw — homegrown event-driven workflow runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS v3, Space Grotesk font&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Agent Pipeline
&lt;/h3&gt;

&lt;p&gt;Five layers run in sequence every time you upload:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parse&lt;/strong&gt;&lt;br&gt;
The CSV/PDF parser extracts raw rows into a typed &lt;code&gt;Transaction[]&lt;/code&gt;. For PDFs, a regex pass converts raw AU bank text (dates like &lt;code&gt;03 Dec 2022&lt;/code&gt;, amounts, CR/DR markers) into the same CSV format before parsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Categorise&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;resolveCategory()&lt;/code&gt; maps merchant names to one of seven categories using regex + keyword matching against known AU vendors. Anything it can't match falls through to an LLM reclassification call (OpenAI or Gemini, temperature=0, 10 tokens max — cheap and fast).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Score&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;calculateImpactScore()&lt;/code&gt; runs the scoring engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GREEN transaction → +10 pts, YELLOW → +5 pts, RED → -2 pts&lt;/li&gt;
&lt;li&gt;Normalised to 0–100 across transaction count&lt;/li&gt;
&lt;li&gt;Preference bonuses: &lt;code&gt;+8&lt;/code&gt; if user owns no car, &lt;code&gt;+3&lt;/code&gt; for low-income mode on RED transactions&lt;/li&gt;
&lt;li&gt;Weekly trend: ≥75 = "Improving", 45–74 = "Stable", &amp;lt;45 = "Needs Attention"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Build RAG Context&lt;/strong&gt;&lt;br&gt;
Before any insight or chat response, &lt;code&gt;buildRagContext()&lt;/code&gt; pulls together: last 7 days of transactions, top 2 categories by spend, top 3 merchants, and detected behaviour patterns. This context is injected into every prompt so the agent actually talks about &lt;em&gt;your&lt;/em&gt; data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Generate Insights&lt;/strong&gt;&lt;br&gt;
The insight pipeline fires &lt;code&gt;{ type: WARNING | POSITIVE | NEUTRAL, message }&lt;/code&gt; structured outputs and stores them with a 3-minute cache. Patterns like "3+ food delivery orders in a week" or "fast fashion spend &amp;gt; $50" trigger specific recommendations.&lt;/p&gt;

&lt;p&gt;Agent memory persists per-user (keyed by Auth0 &lt;code&gt;sub&lt;/code&gt;) and feeds back into the next run — so the agent gets smarter about your habits over time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Auth0 for Agents
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf1grt0m9lfaua8pnzrd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf1grt0m9lfaua8pnzrd.png" alt="Auth0 Diagram" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Auth0 v4 runs entirely via Next.js middleware — no &lt;code&gt;/api/auth/*&lt;/code&gt; route files needed. One line sets it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth0.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Auth0Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@auth0/nextjs-auth0/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Auth0Client&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;auth0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every API route checks an internal agent scope before doing anything. The scope check is now backed by a &lt;strong&gt;fine-grained authorisation (FGA)&lt;/strong&gt; rule table (&lt;code&gt;lib/auth/fga.ts&lt;/code&gt;) that maps &lt;code&gt;resource + action → required scope&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/auth/fga.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canPerform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FGAResource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FGAAction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FGA_RULES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scopes (&lt;code&gt;read:transactions&lt;/code&gt;, &lt;code&gt;write:insights&lt;/code&gt;, &lt;code&gt;update:score&lt;/code&gt;) live as custom Auth0 claims — not OIDC scopes — which keeps agent permissions cleanly separated from OAuth. User preferences (&lt;code&gt;noCarOwnership&lt;/code&gt;, &lt;code&gt;lowIncomeMode&lt;/code&gt;) are stored as Auth0 custom claims so they survive across sessions.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Post-Login Action&lt;/strong&gt; (&lt;code&gt;lib/auth/actions/post-login.js&lt;/code&gt;) provisions new users on first login — setting &lt;code&gt;eco_tier: "starter"&lt;/code&gt; in &lt;code&gt;app_metadata&lt;/code&gt;, injecting custom claims, and enforcing MFA via &lt;code&gt;api.multifactor.enable("any")&lt;/code&gt;. Rolling sessions (&lt;code&gt;inactivityDuration: 1 day&lt;/code&gt;, &lt;code&gt;absoluteDuration: 7 days&lt;/code&gt;) keep users signed in without requiring frequent re-auth.&lt;/p&gt;

&lt;p&gt;The public &lt;code&gt;/demo&lt;/code&gt; page and &lt;code&gt;/api/upload/preview&lt;/code&gt; are excluded from the middleware matcher, so anyone can try the app without an account.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenClaw — the Event Bus
&lt;/h3&gt;

&lt;p&gt;OpenClaw is a lightweight in-process event system I built to wire up automation without reaching for a queue service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz4ped5n9ug221yvies02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz4ped5n9ug221yvies02.png" alt=" " width="798" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four workflows register on startup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;autoInsightOnUpload&lt;/td&gt;
&lt;td&gt;transactions_uploaded&lt;/td&gt;
&lt;td&gt;Runs the full agent pipeline, caches insights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;highImpactAlert&lt;/td&gt;
&lt;td&gt;high_impact_detected&lt;/td&gt;
&lt;td&gt;Pushes in-app alert notification with top offending categories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;weeklyReport&lt;/td&gt;
&lt;td&gt;weekly_report&lt;/td&gt;
&lt;td&gt;Full score + insights digest; pushes weekly notification to the bell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scoreImprovedCelebration&lt;/td&gt;
&lt;td&gt;score_improved&lt;/td&gt;
&lt;td&gt;Pushes a celebration notification when the score rises after upload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After an upload, &lt;code&gt;openClawChainedTrigger()&lt;/code&gt; fires the full sequence: &lt;code&gt;transactions_uploaded → score_calculated → insights_generated → score_improved&lt;/code&gt; (if the score increased). The route handler just returns the parsed data immediately while the pipeline runs.&lt;/p&gt;

&lt;p&gt;Vercel Cron fires &lt;code&gt;weekly_report&lt;/code&gt; every Monday at 09:00 UTC via &lt;code&gt;POST /api/cron/weekly-report&lt;/code&gt;, configured in &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Earth Day Design Choices
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deep forest green hero gradient — earth tones throughout, nothing corporate-blue&lt;/li&gt;
&lt;li&gt;Category breakdown with proportional spend bars instead of raw numbers&lt;/li&gt;
&lt;li&gt;Impact score dial animates from red → amber → green when the page loads&lt;/li&gt;
&lt;li&gt;Real AU vendor recognition — the shops people in Australia actually use&lt;/li&gt;
&lt;li&gt;Every transaction shows its impact colour inline so the pattern is visible at a glance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Interesting Engineering Decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why deterministic rules for categorisation, not LLM?&lt;/strong&gt;&lt;br&gt;
Rules are fast, free, and traceable. Every categorisation decision has an exact rule you can point to. I use the LLM only for the &lt;code&gt;"Other"&lt;/code&gt; fallback bucket — maybe 10–15% of transactions — where a keyword match doesn't exist. That keeps costs near zero and lets the rules engine run at build time on the demo page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why an in-memory store?&lt;/strong&gt;&lt;br&gt;
For a weekend build, an in-memory &lt;code&gt;Map&amp;lt;userId, UserMemory&amp;gt;&lt;/code&gt; is zero-infrastructure and good enough for single-server dev. Both stores are now anchored to &lt;code&gt;globalThis.__pl_*&lt;/code&gt; keys so HMR hot reloads in dev don't wipe your data. The entire storage layer is behind a &lt;code&gt;lib/store.ts&lt;/code&gt; interface — swapping to Redis or Postgres is one function replacement, not an architectural change.&lt;/p&gt;

&lt;p&gt;Agent memory specifically uses a &lt;strong&gt;file-backed store&lt;/strong&gt; (&lt;code&gt;.agent-memory/&amp;lt;fnv1a-hash&amp;gt;.json&lt;/code&gt;) with 30-day auto-expiry — so the agent's learned patterns survive server restarts, not just hot reloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why AU sample data, and can other countries use it?&lt;/strong&gt;&lt;br&gt;
The sample data is from an Australian bank (ANZ/CBA-style export), but the parser isn't locked to Australia. Any CSV that follows the &lt;code&gt;Date / Transaction / Debit / Credit / Balance&lt;/code&gt; column schema will parse correctly — regardless of the bank or country. The vendor categorisation rules are AU-focused right now (Woolworths, Chemist Warehouse, Didi, etc.), but the categorisation layer is just a map — adding UK, US, or EU merchants is additive. The "Why AU only" answer is really: nailing one set of vendors well beats vague coverage of five markets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Further During the Weekend
&lt;/h2&gt;

&lt;p&gt;The core build came together fast, so I kept pushing. Here's what landed after the initial submission:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real multi-turn chat with what-if and doc Q&amp;amp;A&lt;/strong&gt;&lt;br&gt;
The chat engine was keyword-matching and templated. It's now a real multi-turn conversation — conversation history is injected as context so the agent remembers what you asked. You can ask "what if I reduce food delivery by 30%?" and the agent runs an actual score simulation. You can ask "how much did I spend on groceries last week?" and it queries your real transaction data. The RAG scaffolding was already there; wiring it up to GPT-4o was the last step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File-backed persistent agent memory&lt;/strong&gt;&lt;br&gt;
Agent memory moved from an in-memory &lt;code&gt;Map&lt;/code&gt; (lost on restart) to per-user &lt;code&gt;.agent-memory/&amp;lt;fnv1a-hash&amp;gt;.json&lt;/code&gt; files with 30-day auto-expiry. In dev they live at &lt;code&gt;.agent-memory/&lt;/code&gt;, in prod at &lt;code&gt;/tmp/planetledger-memory/&lt;/code&gt;. The agent's learned patterns now actually survive hotfixes and deploys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-app notification bell&lt;/strong&gt;&lt;br&gt;
OpenClaw's &lt;code&gt;highImpactAlert&lt;/code&gt; and &lt;code&gt;weeklyReport&lt;/code&gt; were logging to console. Now they push structured notifications to an in-memory store, which a bell component polls every 60 seconds. Three notification types: 📊 Weekly Report, 🌱 Score Improved, ⚠️ High-Impact Alert. The &lt;code&gt;score_improved&lt;/code&gt; event is new too — fires automatically when an upload raises your score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chained OpenClaw pipeline + Vercel Cron&lt;/strong&gt;&lt;br&gt;
The upload flow now fires a full event chain: &lt;code&gt;transactions_uploaded → score_calculated → insights_generated → score_improved&lt;/code&gt;. One call, four events, all in-process. The weekly report fires via Vercel Cron every Monday 09:00 UTC (&lt;code&gt;vercel.json&lt;/code&gt; + &lt;code&gt;/api/cron/weekly-report&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth0 FGA, Post-Login Action, MFA, rolling sessions&lt;/strong&gt;&lt;br&gt;
The flat scope check got replaced with a proper FGA rule table (&lt;code&gt;resource + action → required scope&lt;/code&gt;). A Post-Login Action provisions new users on first login and enforces MFA. Rolling sessions mean users stay signed in across the week without re-authing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy hardening&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;email&lt;/code&gt; and merchant names are no longer stored in &lt;code&gt;UserContext&lt;/code&gt;, agent memory, or RAG prompts — they're PII-adjacent and don't need to be there. OpenClaw workflow logs pseudonymize user IDs via FNV-1a hash (&lt;code&gt;usr_XXXXXXXX&lt;/code&gt;). A &lt;code&gt;lib/privacy/sanitizer.ts&lt;/code&gt; module handles all redaction.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Persistent database&lt;/strong&gt;&lt;br&gt;
The transaction store is still in-memory (&lt;code&gt;Map&amp;lt;userId, UserMemory&amp;gt;&lt;/code&gt; anchored to &lt;code&gt;globalThis&lt;/code&gt; for HMR safety). Agent memory is file-backed now, but transactions, scores, and chat history need Postgres via Prisma for real persistence. The store interface is already abstracted — the migration is mostly additive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart parsing for all countries&lt;/strong&gt;&lt;br&gt;
The AU bank format is very specific. The next version should detect the bank format automatically — US (Plaid/OFX), UK (Monzo/Starling CSV), EU (SEPA), and AU — and route to the right parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real CO₂ data&lt;/strong&gt;&lt;br&gt;
Right now impact scores are proxy-based (category + spend → colour). The right approach is to integrate an actual emissions API (like Climatiq or Patch) to get grams of CO₂e per transaction category, so the score means something measurable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email / push delivery&lt;/strong&gt;&lt;br&gt;
In-app notifications work. Email or mobile push (Resend, FCM) would make the weekly report actually reach users where they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile-friendly upload&lt;/strong&gt;&lt;br&gt;
The upload panel works on desktop. A mobile-optimised flow — forwarding a bank statement email directly to PlanetLedger via a Cloudflare Email Worker — would massively reduce friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More AI agent capabilities&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Goal setting ("I want to reduce fast fashion spend by 40% this month") with progress tracking&lt;/li&gt;
&lt;li&gt;Carbon offset suggestions matched to your worst categories&lt;/li&gt;
&lt;li&gt;Peer comparison ("people with similar spending reduced food delivery by X%")- Surface proactive nudges (WARNING / TIP / CELEBRATION) from &lt;code&gt;lib/agent/nudges.ts&lt;/code&gt; — the engine is built, just needs an API route + UI hook&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best Use of Auth0 for Agents&lt;/strong&gt; — Auth0 v4 middleware with zero route handlers; FGA rule table (&lt;code&gt;resource + action → scope&lt;/code&gt;) backing every agent action; Post-Login Action provisioning with MFA enforcement; rolling sessions (&lt;code&gt;inactivityDuration&lt;/code&gt; + &lt;code&gt;absoluteDuration&lt;/code&gt;); custom claims for preferences, eco_tier, and organization_id; scope check on every API route before any agent action runs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best Use of GitHub Copilot&lt;/strong&gt; — Used end-to-end: scaffolding the agent pipeline, writing AU vendor categorisation rules, building the PDF regex extractor, generating type-safe API routes, implementing the FGA rule table and privacy sanitizer, designing the notification bell + polling architecture, building the chained OpenClaw pipeline, and iterating on UI components throughout.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>auth0challenge</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Revisiting Idris in 2026: Is It Ready for Real Work?</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Tue, 14 Apr 2026 00:19:30 +0000</pubDate>
      <link>https://dev.to/ujja/revisiting-idris-in-2026-is-it-ready-for-real-work-1mco</link>
      <guid>https://dev.to/ujja/revisiting-idris-in-2026-is-it-ready-for-real-work-1mco</guid>
      <description>&lt;p&gt;Idris has always promised something bold: &lt;strong&gt;encode correctness directly into the type system&lt;/strong&gt;. In 2026, with Idris 2 firmly established, it’s worth revisiting whether that promise translates into production-grade reality.&lt;/p&gt;

&lt;p&gt;Short answer: &lt;em&gt;yes, in the right places&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idris 1 vs Idris 2
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Idris 1&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proof-of-concept era&lt;/li&gt;
&lt;li&gt;Slow compiler, heavy memory usage&lt;/li&gt;
&lt;li&gt;Research-focused ergonomics&lt;/li&gt;
&lt;li&gt;Effectively legacy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Idris 2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete reimplementation&lt;/li&gt;
&lt;li&gt;Faster compilation and smaller core&lt;/li&gt;
&lt;li&gt;Multiple backends (Chez Scheme default, C available)&lt;/li&gt;
&lt;li&gt;Actively maintained and stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re evaluating Idris today, Idris 2 is the only sensible choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea, in code
&lt;/h2&gt;

&lt;p&gt;Idris doesn’t just type-check values — it type-checks &lt;em&gt;facts&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Length-safe vectors
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight idris"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Nat &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Type &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Type &lt;/span&gt;&lt;span class="kr"&gt;where&lt;/span&gt;
  &lt;span class="kt"&gt;Nil&lt;/span&gt;  &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="mf"&gt;0&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;S&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concatenation &lt;em&gt;proves&lt;/em&gt; length correctness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight idris"&gt;&lt;code&gt;&lt;span class="nf"&gt;append :&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="n"&gt;append&lt;/span&gt; &lt;span class="kt"&gt;Nil&lt;/span&gt;       &lt;span class="n"&gt;ys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;
&lt;span class="n"&gt;append&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="n"&gt;append&lt;/span&gt; &lt;span class="n"&gt;xs&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime checks. The compiler enforces the invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Totality: making partial logic illegal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight idris"&gt;&lt;code&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="nf"&gt;head :&lt;/span&gt; &lt;span class="kt"&gt;Vect&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;S&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="nb"&gt;head &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kr"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function cannot be called on an empty vector. Invalid states are unrepresentable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idris 2 ergonomics (the nitty-gritty)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compiler &amp;amp; performance&lt;/strong&gt;: Significantly faster than Idris 1, reasonable compile times&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error messages&lt;/strong&gt;: Improved structure, still proof-oriented and verbose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling&lt;/strong&gt;: LSP support exists, interactive editing is powerful, docs are uneven&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usable, but not luxurious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linearity and resource safety
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight idris"&gt;&lt;code&gt;&lt;span class="nf"&gt;useOnce :&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="n"&gt;useOnce&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linear types enable compile-time guarantees around resource usage: no double-use, no leaks, no implicit copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Idris ready for production?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where Idris shines
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Protocols and state machines&lt;/li&gt;
&lt;li&gt;Parsers, DSLs, validation-heavy logic&lt;/li&gt;
&lt;li&gt;Compilers and interpreters&lt;/li&gt;
&lt;li&gt;Correctness-critical business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where it struggles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Small ecosystem&lt;/li&gt;
&lt;li&gt;Hiring challenges&lt;/li&gt;
&lt;li&gt;Interop overhead&lt;/li&gt;
&lt;li&gt;Not ideal for fast-moving CRUD apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Idris works best as a &lt;strong&gt;correctness core&lt;/strong&gt;, not a default full-stack choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mainline development or niche tool?
&lt;/h2&gt;

&lt;p&gt;Idris isn’t trying to replace Go, Rust, or TypeScript.&lt;/p&gt;

&lt;p&gt;It’s for teams asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if the compiler enforced our invariants?&lt;/li&gt;
&lt;li&gt;What if tests were secondary to types?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, Idris 2 is stable enough for real systems, &lt;strong&gt;if&lt;/strong&gt; your team is willing to think in types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final take
&lt;/h2&gt;

&lt;p&gt;Idris is no longer just a research curiosity.&lt;/p&gt;

&lt;p&gt;Idris 2 is production-capable for the right domains, offering unmatched guarantees at compile time.&lt;br&gt;&lt;br&gt;
If correctness is your top priority, Idris is quietly excellent.&lt;/p&gt;

</description>
      <category>idris</category>
      <category>programming</category>
      <category>discuss</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fourbidden: A Serious AI Solution to 2+2, With Maximum Ceremony and No Resolution</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sun, 05 Apr 2026 10:04:34 +0000</pubDate>
      <link>https://dev.to/ujja/fourbidden-a-serious-ai-solution-to-22-with-maximum-ceremony-and-no-resolution-3p1g</link>
      <guid>https://dev.to/ujja/fourbidden-a-serious-ai-solution-to-22-with-maximum-ceremony-and-no-resolution-3p1g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;Fourbidden&lt;/strong&gt;, a fake AI product dedicated to solving one extremely serious global problem: &lt;code&gt;2+2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The joke is that the problem could not be smaller, but the app treats it like the final summit of machine reasoning.&lt;/p&gt;

&lt;p&gt;You ask a basic addition question, and instead of getting a basic addition answer, the app starts trying to &lt;strong&gt;sum up&lt;/strong&gt; its own importance.&lt;/p&gt;

&lt;p&gt;That means you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated loading statements that sound suspiciously strategic&lt;/li&gt;
&lt;li&gt;circular explainers that get more philosophical and less useful over time&lt;/li&gt;
&lt;li&gt;escalating terms and conditions that keep turning arithmetic into a compliance event&lt;/li&gt;
&lt;li&gt;procedural next steps designed to preserve momentum without producing closure&lt;/li&gt;
&lt;li&gt;dashboard widgets, warnings, and theatrical system language&lt;/li&gt;
&lt;li&gt;surprise interactions like panic overlays, chaos toasts, prank label swaps, and a hidden Konami-style chaos mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The T&amp;amp;C bit became one of my favorite parts of the whole thing.&lt;/p&gt;

&lt;p&gt;The user is effectively told that before they can move closer to understanding &lt;code&gt;2+2&lt;/code&gt;, they must first acknowledge expanding legal nonsense, accept more conditions, and agree to one more procedural step. Then another. Then another.&lt;/p&gt;

&lt;p&gt;So the core gag is not just overengineering. It is &lt;strong&gt;over-addition&lt;/strong&gt;: treating the smallest possible math question like a high-risk, high-governance product flow.&lt;/p&gt;

&lt;p&gt;It is a satire of software that cannot simply give you the answer, because it is too busy building a whole process around the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://ujjavala.github.io/fourbidden/" rel="noopener noreferrer"&gt;https://ujjavala.github.io/fourbidden/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Local version: &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Main routes:

&lt;ul&gt;
&lt;li&gt;Home: &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;About: &lt;code&gt;/about&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Services: &lt;code&gt;/services&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/ujjavala/fourbidden" rel="noopener noreferrer"&gt;https://github.com/ujjavala/fourbidden&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key route surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/explain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/loading&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/terms&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/steps&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/loop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/widgets&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I wanted the app to feel like a glossy software product that exists entirely to avoid resolving &lt;code&gt;2+2&lt;/code&gt; in a straightforward way.&lt;/p&gt;

&lt;p&gt;So instead of making a single punchline screen, I built a full fake workflow around the idea of “solving” the problem while constantly refusing to complete it.&lt;/p&gt;

&lt;p&gt;Tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 14 (App Router)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React + TypeScript&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AI (Gemini API)&lt;/strong&gt; to generate the app's overblown explanations, loading copy, legal nonsense, escalation steps, loop logic, and widget chatter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom CSS&lt;/strong&gt; for the bright cinematic UI, orbit effects, shimmer, flashes, and overly dramatic motion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lucide React&lt;/strong&gt; for icon-driven UI accents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation choices that shape the vibe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The main joke is taking the tiniest math problem and inflating it into a full-stack event.&lt;/li&gt;
&lt;li&gt;The interface looks polished enough to imply a seed round and a brand strategy deck.&lt;/li&gt;
&lt;li&gt;The T&amp;amp;C flow is central to the bit: the user keeps having to consent to more nonsense before progress can allegedly continue.&lt;/li&gt;
&lt;li&gt;The text stays intentionally circular and jargon-heavy so the app never gives the satisfaction of a clean conclusion.&lt;/li&gt;
&lt;li&gt;The click surprises add a second layer of April Fools chaos on top of the main workflow.&lt;/li&gt;
&lt;li&gt;There is a static fallback path so the absurdity still works even when live AI calls are unavailable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Leveraged Google AI
&lt;/h2&gt;

&lt;p&gt;Gemini is the narrative engine for the whole product illusion.&lt;/p&gt;

&lt;p&gt;I did not use it as a single chatbot response. I split the experience into multiple AI-backed roles so each part of the fake platform has its own voice and function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/explain&lt;/code&gt;: produces long-form ceremonial non-answers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/loading&lt;/code&gt;: turns waiting into executive messaging&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/terms&lt;/code&gt;: creates useless compliance clauses and acceptance loops around &lt;code&gt;2+2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/steps&lt;/code&gt;: invents process-heavy next actions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/loop&lt;/code&gt;: explains why completion is still not operationally appropriate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/widgets&lt;/code&gt;: generates fake platform telemetry and side-channel noise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To keep that experience stable, I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt caching with a short TTL&lt;/li&gt;
&lt;li&gt;retry and backoff handling for quota and rate-limit failures&lt;/li&gt;
&lt;li&gt;local fallback generators when Gemini is unavailable&lt;/li&gt;
&lt;li&gt;client-side static mode for static hosting environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That way the joke survives whether the AI is live, rate-limited, or completely absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Google AI Usage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google AI is not just providing flavor text here. It is powering the entire illusion that a hopelessly overbuilt software stack has been assembled to “solve” a single grade-school math prompt. Every stage of that ridiculous journey gets its own AI-generated nonsense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Ode to Larry Masinter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This app is proudly unnecessary. It manufactures protocol, ceremony, terms, gates, and pseudo-serious internet behavior around a task that should end instantly. It feels like the kind of thing that technically works while making arithmetic worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community Favorite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I think the joke lands quickly: everyone understands &lt;code&gt;2+2&lt;/code&gt;, and everyone also recognizes software that delivers process instead of outcomes. Combining those two ideas made it easy to build something immediate, silly, and annoyingly plausible.&lt;/p&gt;

&lt;p&gt;If it makes people laugh and also mutter, "why does this feel plausible," then it did its job.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
      <category>gemini</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Fri, 20 Mar 2026 11:35:27 +0000</pubDate>
      <link>https://dev.to/ujja/-141o</link>
      <guid>https://dev.to/ujja/-141o</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-story__hidden-navigation-link"&gt;I Built EchoHR: The HR System That Doesn’t Ghost You&lt;/a&gt;
    &lt;div class="crayons-article__cover crayons-article__cover__image__feed"&gt;
      &lt;iframe src="https://www.youtube.com/embed/D1zYQVfRA8w" title="I Built EchoHR: The HR System That Doesn’t Ghost You"&gt;&lt;/iframe&gt;
    &lt;/div&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Notion MCP Challenge Submission 🧠&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ujja" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" alt="ujja profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ujja" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ujja
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ujja
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
              
              &lt;div id="story-author-preview-content-3334932" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ujja" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ujja&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 12&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" id="article-link-3334932"&gt;
          I Built EchoHR: The HR System That Doesn’t Ghost You
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/notionchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;notionchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;47&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              63&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Big drop for EchoHR 🚀
🔧 Postgres queue + worker, idempotent + HMAC webhooks, retries/backoff, metrics
✨ Cleaner UX with hero video, logo, and clearer lifecycle
⚙️ More reliable setup + Docker flow (migrate seed worker)
Check out the blog for more 👇</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Tue, 17 Mar 2026 06:08:56 +0000</pubDate>
      <link>https://dev.to/ujja/big-drop-for-echohr-postgres-queue-worker-idempotent-hmac-webhooks-retriesbackoff-387</link>
      <guid>https://dev.to/ujja/big-drop-for-echohr-postgres-queue-worker-idempotent-hmac-webhooks-retriesbackoff-387</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-story__hidden-navigation-link"&gt;I Built EchoHR: The HR System That Doesn’t Ghost You&lt;/a&gt;
    &lt;div class="crayons-article__cover crayons-article__cover__image__feed"&gt;
      &lt;iframe src="https://www.youtube.com/embed/D1zYQVfRA8w" title="I Built EchoHR: The HR System That Doesn’t Ghost You"&gt;&lt;/iframe&gt;
    &lt;/div&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Notion MCP Challenge Submission 🧠&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ujja" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" alt="ujja profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ujja" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ujja
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ujja
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
              
              &lt;div id="story-author-preview-content-3334932" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ujja" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ujja&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 12&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" id="article-link-3334932"&gt;
          I Built EchoHR: The HR System That Doesn’t Ghost You
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/notionchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;notionchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;47&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ujja/i-built-echohr-the-hr-system-that-doesnt-ghost-you-1c2i#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              63&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Enablers Who Helped Me Code Forward</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Tue, 10 Mar 2026 03:31:44 +0000</pubDate>
      <link>https://dev.to/ujja/the-enablers-who-helped-me-code-forward-cai</link>
      <guid>https://dev.to/ujja/the-enablers-who-helped-me-code-forward-cai</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wecoded-2026"&gt;2026 WeCoded Challenge&lt;/a&gt;: Echoes of Experience&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sometimes the difference between giving up and moving forward is just one person who believes in you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I started coding when I was around nine years old.&lt;/p&gt;

&lt;p&gt;My first program was written in &lt;a href="https://en.wikipedia.org/wiki/Logo_(programming_language)" rel="noopener noreferrer"&gt;LOGO&lt;/a&gt;, where I spent hours writing tiny programs using a funny-looking triangular turtle that moved across the screen. You could tell it to move forward, turn, and draw shapes, and slowly patterns would emerge. To a kid, it felt magical. A few instructions and suddenly the computer was drawing something I had imagined.&lt;/p&gt;

&lt;p&gt;Back then I had no idea what a career in tech looked like. I just knew I loved making computers do things.&lt;/p&gt;

&lt;p&gt;One thing that made my journey different from many stories I hear today was the support I received at home. My family, especially my dad, supported my interest in computers wholeheartedly. At a time when many people still questioned whether girls should pursue technology, he never did. To him it was simple — if I enjoyed it, I should pursue it.&lt;/p&gt;

&lt;p&gt;That early encouragement made a huge difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Myth of the Solo Journey&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When people talk about careers in tech, the story is often about individual effort — perseverance, hard work, determination.&lt;/p&gt;

&lt;p&gt;And while those things matter, looking back I realise something else played an equally important role: &lt;strong&gt;enablers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Throughout my journey there were always people who gave me a small push forward at the right moment.&lt;/p&gt;

&lt;p&gt;Not huge dramatic gestures. Just small nudges that made a big difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The First Catalyst&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Early in my career I had a tech lead who noticed something about me before I fully recognised it myself.&lt;/p&gt;

&lt;p&gt;I loved &lt;strong&gt;clean code&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
I loved experimenting.&lt;br&gt;&lt;br&gt;
Hackathons excited me.&lt;/p&gt;

&lt;p&gt;Instead of letting that remain just a personal interest, he organised a &lt;strong&gt;hackathon within the team&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At the time it seemed like just a fun activity. But in reality, it gave me confidence. It validated that my curiosity and enthusiasm for building things mattered.&lt;/p&gt;

&lt;p&gt;Sometimes all someone needs is that small signal that their passion is worth investing in.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Finding My Voice&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Later in another organisation I faced a different challenge.&lt;/p&gt;

&lt;p&gt;I was technically confident, but &lt;strong&gt;public speaking terrified me&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Speaking up in meetings, presenting ideas, addressing a room full of people — those things didn't come naturally to me.&lt;/p&gt;

&lt;p&gt;An office head in that organisation noticed this and gently pushed me out of my comfort zone.&lt;/p&gt;

&lt;p&gt;Encouraging me to present.&lt;br&gt;&lt;br&gt;
Encouraging me to speak up.&lt;br&gt;&lt;br&gt;
Encouraging me to trust my voice.&lt;/p&gt;

&lt;p&gt;I still remember a very simple piece of advice he gave me.&lt;/p&gt;

&lt;p&gt;He said, &lt;em&gt;“Start small. Just say hi to people in the morning.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;His reasoning was simple. If I greeted people in the hallway or by the coffee machine, when I later saw them in a meeting room they wouldn’t feel like strangers anymore. The room would feel a little less intimidating.&lt;/p&gt;

&lt;p&gt;He also said something that stuck with me — smile. Maybe crack a small joke to ease the tension.&lt;/p&gt;

&lt;p&gt;It sounded almost too simple at the time, but it worked. Slowly those rooms full of unfamiliar faces started turning into rooms with colleagues I already knew, even if only through a quick morning hello.&lt;/p&gt;

&lt;p&gt;And little by little, speaking up didn’t feel so scary anymore.&lt;/p&gt;

&lt;p&gt;Today I speak about topics I care deeply about, especially &lt;strong&gt;identity and authentication in tech&lt;/strong&gt; — something I am incredibly passionate about.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Reality of Being Heard&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you’ve worked in tech long enough, you’ve probably seen this happen.&lt;/p&gt;

&lt;p&gt;Sometimes a woman's voice is ignored.&lt;/p&gt;

&lt;p&gt;Ideas get overlooked.&lt;br&gt;&lt;br&gt;
Comments go unheard.&lt;br&gt;&lt;br&gt;
Credit sometimes travels in unexpected directions.&lt;/p&gt;

&lt;p&gt;It happens.&lt;/p&gt;

&lt;p&gt;But another truth exists too.&lt;/p&gt;

&lt;p&gt;There are also people who &lt;strong&gt;amplify voices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;People who pause the room and say,&lt;br&gt;&lt;br&gt;
&lt;em&gt;"I think she was making a really good point."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;People who make sure ideas get the attention they deserve.&lt;/p&gt;

&lt;p&gt;Over time I’ve realised the tech industry isn't defined only by the people who silence voices.&lt;/p&gt;

&lt;p&gt;It’s also shaped by the people who make sure those voices are heard.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Recognising the Enablers&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Even today in my current workplace I am fortunate to work with amazing people who create that kind of environment.&lt;/p&gt;

&lt;p&gt;Looking back, my journey was never just about learning new technologies, frameworks, or systems.&lt;/p&gt;

&lt;p&gt;It was also about recognising the people who helped me move forward.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;enablers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mentors.&lt;br&gt;&lt;br&gt;
The leaders.&lt;br&gt;&lt;br&gt;
The colleagues.&lt;br&gt;&lt;br&gt;
The allies.&lt;/p&gt;

&lt;p&gt;The people who saw potential and helped unlock it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Paying It Forward&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from my journey is this:&lt;/p&gt;

&lt;p&gt;If you’ve had enablers in your life, the best thing you can do is &lt;strong&gt;become one for someone else&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Encourage the curious junior developer.&lt;br&gt;&lt;br&gt;
Support the quiet voice in the meeting.&lt;br&gt;&lt;br&gt;
Create spaces where people can experiment and grow.&lt;/p&gt;

&lt;p&gt;Sometimes a small push can change someone’s entire trajectory.&lt;/p&gt;

&lt;p&gt;I know it did for me.&lt;/p&gt;

&lt;p&gt;And to &lt;strong&gt;all the girls who want to code but feel like they can’t&lt;/strong&gt;,&lt;br&gt;&lt;br&gt;
to &lt;strong&gt;all the women and marginalised voices who sometimes feel unheard or silenced in tech&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Look around.&lt;/p&gt;

&lt;p&gt;There are often people who quietly support you, encourage you, and believe in you — sometimes even before you believe in yourself.&lt;/p&gt;

&lt;p&gt;Find those people.&lt;/p&gt;

&lt;p&gt;Recognise them.&lt;/p&gt;

&lt;p&gt;Hold on to them.&lt;/p&gt;

&lt;p&gt;Because once you identify the &lt;strong&gt;enablers&lt;/strong&gt; in your life, you’ll realise something powerful. You were never coding alone.&lt;/p&gt;

&lt;p&gt;And if you ever feel like you don’t belong in tech, remember this:&lt;/p&gt;

&lt;p&gt;Sometimes all it takes is one enabler to change your trajectory — and someday, that enabler might be you.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>wecoded</category>
      <category>dei</category>
      <category>career</category>
    </item>
    <item>
      <title>Rethinking AI Assistants: A Privacy-First Approach with Google Gemini</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Wed, 04 Mar 2026 11:09:32 +0000</pubDate>
      <link>https://dev.to/ujja/rethinking-ai-assistants-a-privacy-first-approach-with-google-gemini-4cm7</link>
      <guid>https://dev.to/ujja/rethinking-ai-assistants-a-privacy-first-approach-with-google-gemini-4cm7</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm" class="crayons-story__hidden-navigation-link"&gt;Building a Privacy-First Mobile Speech Assistant Using Google Gemini&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Built with Google Gemini: Writing Challenge&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ujja" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" alt="ujja profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ujja" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ujja
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ujja
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
              
              &lt;div id="story-author-preview-content-3293845" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ujja" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ujja&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 28&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm" id="article-link-3293845"&gt;
          Building a Privacy-First Mobile Speech Assistant Using Google Gemini
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/geminireflections"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;geminireflections&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gemini"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gemini&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;25&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ujja/building-a-privacy-first-mobile-speech-assistant-using-google-gemini-59pm#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              14&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
