
Yageno AI: A Plug-In AI Layer for Any IoT Project
I built a plug-in AI layer on AWS that turns any IoT sensor into decisions. Here is the Heat and Water problem, the stack, and six days of bugs.
The Journey
I built Yageno AI alone for the AWS Bharat Builds Tour, Track 02: Heat and Water. I was also in the middle of my college CIA exams. This post covers the problem, the stack, and what fought back, from my first ever AWS sign-up to an AI that can decide when to turn on a sprinkler.
The Problem
Heatwaves dry the soil. Droughts, leaks and badly timed irrigation waste water. Floods and tankers need someone watching. Many people already have cheap internet-connected sensors for soil moisture, temperature, tank level or flow, but the readings end up in a serial monitor or a one-off dashboard.
Turning readings into decisions ("is this a leak or normal use?", "should the pump run?") needs custom software and someone to watch it. Small farms, colleges, housing societies and local water teams rarely have that.
The Idea
Yageno AI is software, not hardware. It is a plug-in, plug-out platform: any device with an internet connection sends its readings to one API and receives commands back. On top of that, Claude on Amazon Bedrock:
- reads the data and explains it in plain language,
- answers questions about the past ("when did the soil last dry out?"),
- switches actuators when a person asks, or automatically when a hazard appears,
- shares everything in a team chat that shows alerts and who did what.
The app is organised into projects. Farm is the working demo. The others are planned and shown in the app as "Soon":
- Forest fire: smoke, heat, and early fire alerts.
- Water network: tanks, pipes, and leaks.
- Air quality: AQI on streets and in schools.
- Traffic signals: signal timing and congestion.
- Add project: connect a new set of sensors and actuators.
About the Demo Device
The farm demo runs on a small hardware node (an Arduino Uno Q plus an ESP32) that I built a month before the hackathon. It is not part of this entry. It is only a stand-in so the software can be shown with real sensors. It is powered from my MacBook, so it is online about 5 to 6 PM IST, and outside that hour the app shows the last known data. The stored history stays in DynamoDB, so the AI can still summarise it and answer questions about the past, and only the live controls wait for the device. Everything built during the hackathon is the software.
The Stack
- Amazon API Gateway: the public HTTPS front door for the web app and for devices.
- AWS Lambda (Python 3.12): a FastAPI backend wrapped with Mangum.
- Amazon Bedrock: Claude Haiku 4.5 for chat, tool use and automated decisions.
- Amazon DynamoDB: chats, device state, stored readings, events and the shared chat.
- IAM, CloudWatch and Amazon Q: permissions, logs, and help finding my way around and setting up resources.
Day 1 (October 6): First Time on AWS
My first day ever with an AWS account. Hackathon rules said project work starts on October 8, so I used the day for a practice run: deploy a tiny FastAPI app to Lambda.
Signing in from my IDE took several tries. I had to install the
uv package manager to get the AWS CLI working, and the browser sign-in kept timing out. Amazon Q was really useful that first day: it helped me find my way around the console and work out what to set up, which saved me a lot of time as a first-time user. Then three things fought back.The Mac vs Linux Binary War
I zipped my app on my Mac and Lambda replied
No module named 'pydantic_core._pydantic_core' . My Mac had compiled the C extensions for macOS, and Lambda runs Linux. Instead of using Docker, I told pip to download the Linux builds directly:Bash
pip install -t package -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all:
The Mangum Mystery
Next came
The adapter was unable to infer a handler to use for the event . Lambda's default test button sends dummy JSON, not a web request, so I wrote a test event that looks like an API Gateway request.The API Gateway Final Boss
My public URL returned
{"detail": "Not Found"} . I blamed my code for hours. The real cause was the route AWS had auto-created, ANY /First_Fastapi_webapp , which blocks every sub-path. I replaced it with a greedy proxy route, ANY /First_Fastapi_webapp/{proxy+} . At 12:35 AM the page finally loaded.Day 2 (October 7): Giving It a Brain
The goal was to connect the web app to Amazon Bedrock with Claude Haiku 4.5.
- The cross-region paradox: my first call failed with a
ValidationExceptionsaying on-demand throughput was not supported. In Sydney (ap-southeast-2) newer models need an inference profile. Adding theau.prefix to the model ID fixed it.
- The phantom deployment: the same error kept coming back because I had edited
app.pybut forgot to re-zip before uploading.
- The 3-second assassin: Lambda defaults to a 3-second timeout and 128 MB of memory, so replies died mid-sentence. I raised them to 30 seconds and 256 MB.
- The fake stream: text arrived all at once instead of token by token, because API Gateway buffers the whole response.
Day 3 (October 8): Kickoff During Exams
The hackathon clock started, and I barely touched the project because of exams. I only planned: stable cloud first, then memory in DynamoDB, then the first real project. A small working demo beats a big broken one.
Day 4 (October 9): Deploy Without the CLI
- Deploy bottleneck:
aws lambda update-function-codekept failing withConnection was closed before we received a valid response. I switched to one complete zip uploaded through the Lambda console.
- Quota trouble: on a fresh AWS account my Bedrock token and request quotas for Claude were stuck and blocked requests. I went back to my older account, where the default limits were already active, and the AI worked immediately.
- Runtime alignment: compiled
pydantic-corefiles failed again until the Lambda runtime and my wheels were both Python 3.12 on Linux x86_64.
- Browser caching: new JavaScript and CSS did not appear after deploys. Adding version parameters such as
?v=3to the file paths forced fresh downloads.
- A regex bug: my word-by-word text animation ignored its 20-word speed limit because of an accidentally escaped regular expression. I replaced it with
trim().split(" ").
- UI overhaul: I merged a new design with better scrolling, a sidebar, rename and delete popups, and new themes.
Day 5 (October 10): Memory and the Prefix Problem
I worked through the night to give Yageno AI a persistent memory.
Planning Before Coding
Auditing my DynamoDB plan found traps that would have broken the app:
- Temporary chats lose context unless the browser sends its own history.
- Saving a user message before Bedrock answers can leave two user turns in a row after a retry, and Claude rejects that. So the prompt and reply are saved together, only after success.
- API Gateway cuts requests at 29 seconds, so there is no second model call for chat titles. The title comes from the first four words.
- "Thinking" mode eats output tokens, so I raised
max_tokensto 2000 for it.
- Branching gets its own endpoint instead of sending a dummy prompt to the model.
With Amazon Q's help I created the
YagenoChats table (partition key sessionId , on-demand billing) and gave the Lambda role the permissions it needed. I stored timestamps as text to avoid boto3 Decimal crashes.The Ghost Bugs
On desktop, fonts, logos and the sidebar failed with 500 errors, and my console showed
Unexpected token '<', "<!-- SPDX-" . CloudWatch logs showed two causes:- My
zip -ucommand had only updated some files, so fonts, the favicon and logos were missing from the package.
- API Gateway adds the stage and function name to the path, so
/api/chatsarrived as/default/First_Fastapi_webapp/api/chats. No route matched, and the catch-all returned my HTML page instead of JSON.
The fix was a small wrapper that strips the prefix before FastAPI sees the request:
Python
class StripPrefix:def __init__(self, inner):self.inner = innerasync def __call__(self, scope, receive, send):if scope["type"] == "http":path = scope["path"]m = _PREFIX.match(path)base = m.group(1) if m else path.rstrip("/")if base and path.startswith(base):scope = dict(scope)scope["path"] = path[len(base):] or "/"scope["raw_path"] = scope["path"].encode()await self.inner(scope, receive, send)
I also added a script that adds a missing trailing slash, and a
getJSON helper that rejects HTML responses with a clear error. I now package everything with zip -r so no asset is missed.Day 6 (October 11): Plugging In the First Project
A Second Table for Device Data
YagenoStream holds stored readings, events and the shared Farm chat. Its partition key is pk , its sort key is ts (milliseconds), and a TTL attribute exp removes old data. To keep the database small, a full reading is stored every 3 hours, plus whenever something changes and whenever an anomaly starts or ends (heat above 32 C, soil at 30% or below).The Device Protocol
A device posts its readings to the API with a device key. The reply carries any pending commands. A command expires after 15 seconds and runs only once. After 20 seconds of silence the device shows as offline. This same small protocol is what any future project will use.
The Devices Page
It has sensor and actuator cards, a 180 degree servo dial, and timed irrigation and sprinkler controls (5 seconds, 1 minute or 10 minutes). For a while the actuators looked broken and the page said "connection lost". The cause was that the demo device was still running its old client code. Updating it fixed that, and I also made the page wait for 3 failed polls and show a reason before it reports "connection lost".
Automated Decisions
When flame, a flying object or bird sound is detected, the device waits 5 seconds to see which signals appear together. It then calls
/api/device/decide , and Claude on Bedrock returns a sprinkler duration, whether to sound the alarm, and a reason in plain words. If Bedrock fails, a simple rule-based fallback takes over. The decision appears in the Farm chat as a card.Tools and the Shared Farm Chat
- Claude reads the database through two tools,
get_sensor_historyandget_events, asking only for the metrics and time range it needs.
- It acts through
control_actuator(do it now) andshow_controls(show the same control card as the Devices page). Neither is available in temporary chats.
- The Farm chat is shared. Messages can be retracted while pending, a team-only mode keeps the AI out, and alerts look different from info. A DynamoDB conditional lock means only one AI reply runs at a time.
What Fought Back Today
- Two notice types shared one key, so one hid the other.
- A bird decision put the fire decision on cooldown, so each hazard now has its own cooldown.
- The 5-second window dropped signals while an earlier decision was still running.
- Some commands were announced twice in the chat.
What I Liked About AWS
- Lambda let me ship a whole backend as one function with no servers to look after, and it costs almost nothing when idle.
- API Gateway gave me a secure public endpoint in minutes that both a browser and a device can call.
- Bedrock put Claude behind my AWS account and IAM role, with no separate API key to leak. Tool use turned the chat into something that can read data and control devices.
- DynamoDB gave me on-demand pricing, TTL for automatic cleanup, simple time-range queries, and conditional writes for the AI lock and for retracting messages.
- CloudWatch and Amazon Q found the missing static files on Day 5 when the console only said "Fail to request credentials". Amazon Q was also really useful on my first day, 6 October, when I signed in and was finding my way around.
What Could Be Better
- A clear guide for FastAPI behind API Gateway would have saved me hours on stage prefixes and routes.
- The 29-second API Gateway limit is shorter than Lambda's, and nothing warns you about it.
- Bedrock errors about model IDs, inference profiles and regions do not say what to change.
- Lambda packaging is manual, and forgetting one file gives a runtime 500.
- Quotas on a new account can block you with no obvious reason.
Honest Limits
- Farm is the only working project. Forest fire, Water network, Air quality and Traffic signals are planned, and the Add project flow is not built yet.
- The demo device is online only about an hour a day.
- The flying-object sensor means "birds or something airborne", not object detection.
- Login is username only, which is fine for a demo and not for production.
- Replies arrive as a whole and the UI reveals them word by word. True token streaming is not done.
What's Next
The Water network project is next, since tanks, pipes and leaks sit right at the heart of Heat and Water. After that, I want to finish the Add project flow so anyone with an internet-connected sensor can plug it in without touching my code.
Try it: Yageno AI live demo (pick any username; there is no password). The demo device is only powered about an hour a day, so outside that hour you will see the last stored data.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article