A Practical Home Energy OS with Home Assistant

How five vendors, two batteries, an EV, and a careful policy engine became one operating system for my house. There is a moment when a solar installation stops feeling like a set of appliances and starts feeling like a distributed control system. The interesting problem is no longer generation or storage alone. It is orchestration across partially trusted, independently operated control planes. For me, that realization came on a quiet afternoon when the house decided to charge the Rivian. The battery was full enough. The grid was still exporting. The car was home, plugged in, and below its target state of charge. The forecast and the optimizer both agreed it was a good charging window. Node-RED waited long enough to be sure it wasn’t a passing cloud-edge artifact, then pressed the ChargePoint start button through an allowlisted Home Assistant executor. A few seconds later, SPAN showed the EV circuit come alive. Charge the car when the sun is out. It sounds trivial. The interesting part is everything the system refuses to do. It will not start charging the car if the FranklinWH battery is near reserve. It will not keep charging if the house starts importing too much from the grid, or if the battery begins supporting the EV. It will not call arbitrary Home Assistant services. It will not trust a single integration when SPAN’s circuit telemetry can verify the physical result. This is the story of that system. It is also a small argument: a working home energy controller cannot be built by a single vendor today, because the substrate it needs is multi-vendor data fusion that the vendors themselves have no reason to ship. Before a single line of code was written, this entire orchestration was defined as a natural language policy document. It is a plain-English contract detailing exactly what the house is permitted to do, and more importantly, what it is forbidden from doing. For example, the policy explicitly dictates the conditions under which the house will act: 3.1 Normal Solar / Net-Metering Mode Objective: optimize useful energy value while allowing export. Rules: Do not raise EV or EcoFlow targets just to avoid export. Do not treat EcoFlows as dump loads. Do not create sustained grid import for discretionary charging. Do not allow Franklin to discharge into discretionary charging. EMHASS schedules are useful advisory inputs, not the only authority. The codebase is simply a strict, testable translation of that document—and the system is built to ensure the code never drifts from the contract. The Thesis: Five Vendors, One Policy The hardware in my house comes from five different companies that do not coordinate with each other in any meaningful way. FranklinWH (battery, inverter, transfer switch — the aGate + aPower stack) Enphase (microinverters, per-panel production) SPAN (smart panel, circuit-level real-time metering) EcoFlow (three Delta-series batteries used as critical-load UPSes and a V2L buffer) EMHASS (open-source home energy management as a planner/optimizer) The actuators add two more vendors: a ChargePoint CPH50 Level-2 charger and a Rivian R1T as the largest flexible load on the property. Each vendor publishes its own data through its own integration, in its own units, on its own cadence, with its own assumptions about what to do with it. Each vendor’s app and cloud can do interesting things in isolation. None of them, individually, can do what their combined telemetry makes possible. Home Assistant is the substrate that makes the combination possible. Not because HA is clever — it isn’t, particularly — but because HA is the place where five independent state graphs become one queryable state graph, and where a small custom controller can act on the fusion. The thesis of this writeup is straightforward: the interesting capabilities are emergent from the fusion, not present in any single product . The Reddit thread that landed in r/FranklinWH this week makes this concrete. A homeowner asked whether you could cap the FranklinWH battery at 80% while still discharging below it during peak hours. The current answer from FranklinWH alone is “no, not really” — the standby toggle that caps the SoC is too blunt for time-of-use arbitrage. The fusion answer is “yes, and it’s been running on my house for weeks.” Same hardware, completely different capability, because the controller knows things about solar export and grid prices and forecasts that no single vendor’s app can see. The Hardware Is In Service Of An Idea The solar array is large enough that the control problem is worth solving. 44 Silfab 440 W panels for 19.36 kW DC, behind Enphase IQ8AC microinverters. It’s not one simple south-facing plane. The roof has five different production surfaces, and they behave differently at different times of day: Array Panels DC kW Azimuth Tilt South 1 6 2.64 180° 21° South 2 7 3.08 180° 21° East 17 7.48 90° 21° West 1 7 3.08 270° 21° West 2 7 3.08 270° flat A single forecast curve does not describe this roof well. Morning, noon, and afternoon behave differently — east in the morning, south at midday, west arrays carrying the late afternoon — and the flat west array has its own losses-to-glare profile. The HA forecast model is split into five matching solar entries, one per array, so the system can reason about the day as a changing shape instead of just a daily total. The rest of the house adds the constraints the controller has to respect: The FranklinWH battery is the largest stationary storage on the property, around 27 kWh nominal. It is the only device authorized to write to the grid-tie inverter or to draw from / push to the AC main. The SPAN panel measures every breaker in real time. It is the only source that can tell you with certainty which physical circuit is actually drawing power right now. If FranklinWH says “house load is 2 kW” and SPAN says “main feed is 2.2 kW”, the 200 W discrepancy is something worth knowing about. The Enphase Envoy publishes per-microinverter production. It is the only ground truth for whether a specific array is performing or whether one of those 44 panels has a fault. The three EcoFlows are not interchangeable. Two — a Delta 2 Max and a Delta 3 Max — sit in-line as UPSes for critical computer loads and have to stay in pass-through at all times. The third — a Delta Pro 3 — is reserved as the standby buffer for Rivian V2L charging during a grid outage, so it sits dormant near its SoC cap. The Rivian R1T is the largest flexible load in the house, around 11 kW at maximum charge rate. It can be driven away at any time. It is also the only device on the property that can act as a generator via V2L. EMHASS is an MIT-licensed Home Assistant add-on that runs a discrete optimizer every five minutes over a 24-hour, 48-slot horizon. It produces a desired-watts target per deferrable load. It does not actuate anything. Home Assistant is the entity graph and event bus. Every reading from every vendor becomes a state-change event with a timestamp and recorded history. Node-RED is the live policy engine and actuator gate. It is the only thing on the property allowed to send a Franklin command, an EcoFlow charge-rate change, or a ChargePoint start/stop. The division of labor matters and is intentional. Each vendor stays in its lane. The new behaviour is between the lanes. Reading the House: Data Fusion in Practice The non-obvious capabilities all come from triangulating two or more vendors’ telemetry. A few concrete examples. Franklin SoC from two sources. The FranklinWH aGate exposes a Modbus interface as well as a cloud API. The controller subscribes to both. The Modbus side delivers SoC, active power, AC voltage and current, pack temperatures, grid connection — about 50 distinct signals — on a faster cadence and with no round trip to a server in another state. The cloud side is the source of truth for slower-changing values and for write operations. The controller runs a small template sensor called franklin_modbus_cloud_soc_delta that compares the two: – name : ” Franklin Modbus vs Cloud SoC Delta” state : > {{ (states(‘sensor.franklinwh_modbus_soc’) | float(0) – states(‘sensor.franklinwh_state_of_charge’) | float(0)) | round(2) }} unit_of_measurement : ” %” Enter fullscreen mode Exit fullscreen mode A persistent delta means something is wrong on one side. A delta that resolves on its own means I just caught a transient. Neither vendor’s app shows this — there’s only one number in each — but the difference between two numbers is what tells you whether to trust either. Cluster-only power from an EcoFlow. The SPAN panel has a breaker labelled “Lighting” that, for historical wiring reasons, carries both actual lighting fixtures and a compute cluster that sits behind a Delta 3 Max in UPS pass-through mode. SPAN can only see the breaker total. EcoFlow can only see the pass-through power on its specific input. Subtraction gives the lights-only number that no single sensor measures: lights_only_watts = span_lighting_breaker_watts – d3m_total_out_power_watts Enter fullscreen mode Exit fullscreen mode A snapshot this afternoon: SPAN reports 735 W, D3M reports 562 W passing through to the cluster, lights are using the remaining 173 W. Two vendors that don’t know about each other, one new metric. Solar surplus that the EV controller can actually trust. “How much surplus solar is there right now?” sounds like it should be one number. It isn’t. There are at least four candidate numbers, each measured differently: Enphase’s per-array production summed across all five arrays Franklin’s reported solar production (which is a derived value, not a direct measurement) The negative of SPAN’s main-feed power (because export is negative import) EMHASS’s load forecast for the current slot subtracted from one of the production numbers If they agree, the controller proceeds. If two agree and one disagrees by a small amount, the controller picks the most conservative and notes the discrepancy. If multiple sources disagree by a lot, the controller refuses to allocate the surplus to a discretionary load. The EV will not start charging on a single source’s claim of surplus, because a single source can be wrong, and the cost of getting it wrong is unintended grid import or an unexpected Franklin discharge. Storm awareness that doesn’t depend on the storm. The local NWS alerts, FranklinWH’s own native storm flag, and EcoFlow’s storm-protection status are three independent signals about the same expected weather event. The controller doesn’t need all three to agree before it switches the policy into pre-storm grid-fill mode — but it does require that the chosen safe-import budget come from SPAN’s main-feed headroom measurement rather than any vendor’s assumption. These are all small things. They add up to a controller that doesn’t get fooled by one bad sensor, one stale forecast, or one ambiguous cloud value. The Policy Engine: Dynamic Standby and the Soft Cap The Reddit thread referenced earlier was about something specific. A homeowner wanted to cap the FranklinWH battery at 80 % to avoid spending most of its life at 100 % (which is harder on lithium cells than people realize). FranklinWH has an undocumented standby toggle on its cloud API that holds the battery at the cap. The objection in the thread was sharp: if you turn standby on, the battery never discharges, so you lose your time-of-use peak shaving. The fix is dynamic standby. Rather than a single user-selected “max SoC” setting, the controller treats the standby toggle as one mode in a state machine driven by live conditions. The cascade for a normal day, with cap = 85 % , deadband = 2 % , and daylight threshold = 500 W , is: if battery . soc >= cap and grid . is_exporting (): # Earn our keep: hold the cap as long as we are pushing to the grid return Mode . SOLAR_HOLD ( profile = ” standby ” , charge = 0 , discharge = 0 ) elif battery . soc < ( cap - deadband ) and solar . surplus > 0 : # Under cap with room to charge: absorb the surplus return Mode . SOLAR_FILL ( profile = ” solar-fill ” , charge = solar . surplus ) elif solar . production > daylight_threshold and solar . surplus <= 0 : # Sun is up but home load consumes it all: don't drain the battery yet return Mode . SOLAR_HOLD ( reason = " daylight_deficit " ) else : # Evening, heavy clouds, or night: release the cap for peak shaving return Mode . SELF_CONSUMPTION ( profile = " time_of_use " , limits = " 20kW / 20kW " ) Enter fullscreen mode Exit fullscreen mode This is the soft cap homeowners are asking for. The battery holds at 85 % whenever there is real solar export to support that hold — exactly when the hold is earning its keep . The moment surplus disappears and solar drops below the daylight threshold, the controller releases standby and the battery is fully available to cover the evening load and TOU peak hours, exactly as it would be without any cap at all. Two additional branches cover edge cases: Storm pre-fill. When NWS alerts or HA stormwatch fire, the policy switches from NORMAL_NET_METERING to PRE_STORM_GRID_FILL . The controller charges Franklin from the grid up to a SPAN-derived safe-import headroom (because pushing storm pre-fill through your main breaker is exactly the wrong time to find out your service is undersized). Grid-preferred load active. When the A/C or another heavy intermittent load is running, the controller engages a discharge cap on Franklin so the heavy load draws from the grid rather than accelerating Franklin's reserve drawdown. The state machine is implemented as a 500-line pure-JavaScript module with no Home Assistant or Node-RED imports, which means it's testable in isolation. The current test suite has 144 cases covering: every state transition, every safety condition, every device-level shed and start scenario, every storm/calibration/outage override. The same module is what Node-RED imports at runtime to make every decision. A Bug, a Fix, and the Drift Sentinel Earlier this week the deployed system started failing in a specific way. The EcoFlow safety-shed logic, which I'll describe in a moment, was firing about every 30 seconds with the status failed . Over 24 hours, 335 of these failed events accumulated. Zero successes. The safety-shed is the controller's defensive backstop for the EcoFlows. If a device is actively pulling AC power to charge itself, and the house is importing from the grid, and the Franklin can't help, the controller has to assume that EcoFlow is now charging itself from the grid (or worse, from the Franklin's reserve). The shed command instructs that EcoFlow to stop charging without disabling its AC output to the loads it's protecting. The bug, when I traced it: the shed command was writing value = 0 to the EcoFlow's AC charging-power slider in Home Assistant. Each of those sliders has a non-zero minimum — 200 W for Delta Pro 3 and Delta 2 Max, 50 W for Delta 3 Max — so HA was returning HTTP 500 on every attempt. The executor caught the exception, recorded it as failed , and tried again 30 seconds later. The cooldown that was supposed to suppress retries used a signature that included the current grid-import wattage in the reason text, so the signature was different on each cycle and the cooldown never engaged. The fix was small. Pin the EcoFlow's target_soc to the device's current SoC (so the device thinks it's at target and stops charging through its own logic), and clamp the charging power to the device's minimum rather than zero. The cooldown signature was changed to a stable shape of {id, device, intent} per command, stripped of all volatile telemetry. A contract test was added to enforce that the shed handler never writes 0 again. The whole episode is in the project's implementation plan with the specific commit hashes, the SPAN/EcoFlow recordings that caught it, and the synthetic shed test that proved the fix. None of that is interesting. The interesting part is the meta-lesson. I built a small service called the drift sentinel that runs every five minutes on the HA host, independent of Node-RED. It does two things. First, it checks that the policy engine binary deployed on the host matches the policy engine binary in the repository and matches the version embedded in the Node-RED flow's bundled execution context. If any of the three checksums disagree, the sentinel raises an alert. Second, it re-runs the policy engine against the current HA state and compares the result to whatever Node-RED most recently published. If the live controller disagrees with what the latest policy would say, the sentinel raises an alert. The sentinel is the thing that would have caught the shed bug in the first hour instead of the first day. It is also the thing that proves, hour after hour, that the deployed system continues to match the source of truth — that nothing has been edited live, that no drift has accumulated, that the controller still does what its code says. The sentinel is the closest thing this system has to a "test in production." It's the same idea as a synthetic monitor for a web service: you run a small, regular, externally-verifiable check that the thing under test is in a known good state. For a home energy controller, that's the difference between a system that works on the day you wrote it and a system that keeps working on the day you forgot you wrote it. Refusing to Do Things The most important behaviours of this controller are the ones that produce no commands at all. In a typical 24-hour cycle the system emits two or three Franklin command bursts and zero EcoFlow or ChargePoint commands. That is not because nothing is happening. It's because the controller is busy refusing to do almost everything it considers. Some of the refusals are explicit: The EcoFlows' AC output cannot be disabled by any controller action, because two of them are UPSes for critical computer loads. There is a contract test that fails the build if anyone adds a switch.delta_*_ac_enabled write to the executor. The Delta Pro 3 is the Rivian-V2L buffer and is not allowed to be opportunistically charged by the normal energy manager. Charging it requires an explicit "v2l_bridge_topup" or "calibration_topup" intent that the policy engine has to emit on purpose. The Rivian's vehicle-side charge limit can never be set by the controller. The policy can recommend it. The dashboard can notify the user. The controller will not call any HA service that touches the Rivian directly. If a storm comes through and the EV needs to be at 100 %, the user does that themselves. Live write actions on Franklin require both a "live enabled" boolean to be on and a write-allowed gate per command to be set by the safety logic. Either gate being off means no command is sent, regardless of what the policy thinks should happen. Some of the refusals are temporal: After any Franklin command, a 15-minute same-action dwell prevents the controller from chasing small changes. A solar surplus that drops from 2,000 W to 1,500 W is not worth sending Franklin a new charge-limit command for. A drop from 2,000 W to 0 W is. After any successful executor write, a per-physical-signature cooldown prevents the same command from being re-sent. The 30-minute EcoFlow cooldown is what kept the safety-shed loop from melting down the integration before the bug fix landed. Some of the refusals come from cross-source disagreement: Solar surplus has to be visible in at least two of {Enphase production, negative SPAN main-feed power, Franklin's reported export} before the EV is allowed to begin charging. If only one source claims surplus, the others probably know something the first one doesn't. Storm mode is engaged on the union of NWS alerts, FranklinWH native storm flag, and HA stormwatch, but the storm-fill budget itself is gated on a separate SPAN-derived safe-import headroom. The signals can disagree about whether a storm is coming; they cannot disagree about how much current the service drop can carry. The point of all these refusals is that doing nothing is the default. Acting requires evidence. Acting automatically , against actuators that touch hardware, requires either redundant evidence or explicit human override. This is the part the FranklinWH product team is going to have the hardest time reproducing inside their own app, by the way. A safe home energy controller doesn't look like a feature list. It looks like a long list of conditions under which the feature deliberately does not run. What I Would Build Next The version of the system described here works. It has been live for several days under real conditions, it has caught one real bug and recovered from it cleanly, and the drift sentinel has stayed aligned across multiple code deploys. The road from here is not particularly long. Three pieces are queued in my project tracker as backlog items: A 15-minute EMHASS optimizer step. Today EMHASS plans in 30-minute slots, which is fine for the Rivian's three-hour median session but loses some accuracy for the EcoFlows. Halving the slot length should give better surplus matching at the cost of more optimizer iterations. A first-class Rivian V2L bridge mode. Today the system has a standby_bridge device role baked into the EcoFlow executor, but the path from "Rivian plugged into Delta Pro 3 during a grid outage" to "Delta Pro 3 is allowed to AC-charge despite gridConnected = false" is still partially manual. The acceptance criteria are written; the implementation will land before the next storm season. The compute cluster as a load-forecast feature. EMHASS's load forecast is currently a one-dimensional curve. The Delta 3 Max pass-through power is a cluster-only meter that has its own daily and weekly pattern (GPU jobs, builds, idle nights). Promoting it into a separate forecast feature should give the optimizer better predictions about evening surplus availability. Three larger questions remain, and I'd actively welcome correspondence about them. The first is policy under explicit time-of-use rates , not net metering. Today my policy assumes export is always valid and grid is a seasonal battery. If my utility migrates me to a true TOU rate plan with peak / off-peak / super-off-peak buckets, the policy needs to learn explicit peak avoidance and off-peak charging. The architecture supports it; the policy doesn't have the branches yet. The second is HVAC as a deferrable load. EMHASS can model thermal loads, but the comfort / equipment-protection / occupancy constraints make it a much riskier addition than EV or battery charging. I have a feasibility ticket open and explicitly deferred until the rest of the system has accumulated more soak time. The third is the harder one: what happens when the vendors actually do start shipping the integrated features they should have shipped? If FranklinWH adds a soft cap with TOU release in v2.x of the aGate firmware, do I retire my dynamic standby logic and just use theirs? Probably yes — most of the value of running the controller comes from work the vendors don't do, not work they could do. The data fusion across all five remains a thing only HA can do. The day Franklin solves the soft-cap problem natively is the day I delete fifty lines of state-machine code and keep the other thousand. An Operating System For A House Residential energy systems are becoming distributed control systems. The transition isn't dramatic and there isn't a specific switch you flip. It happens when you stop reading vendor dashboards and start reading the fusion view; when "what is the house doing right now" becomes a single question with a single answer instead of five different vendor apps to consult; when "should we charge the car?" becomes a function call against a controller rather than a manual decision. It happens when refusing to do something becomes the default and acting becomes the exception. When five vendors' state graphs collapse into one policy and one history. When a contract test gates the next deploy and a drift sentinel gates the live system. When the most boring possible outcome — four hours of steady state on a sunny afternoon with the battery at cap and no commands flowing — is recognized as the point of the work, not the absence of it. The Reddit thread that pushed me to write this up asked a small question: can you cap the battery at 80 % but still discharge during peak hours? The answer involves a 1,000-line policy engine, a drift sentinel, a multi-vendor data fusion, and three EcoFlows that nobody asked about. That isn't the right answer for everyone. For some homes it's wildly more than is needed. For mine, after the third storm season and the second EV and the compute cluster on a shared breaker, it turned out to be exactly enough. If you have a similar enough setup that any of this resonates, the implementation is in Home Assistant + Node-RED + a small JavaScript policy engine + about a dozen Python helpers, with EMHASS as the optimizer. None of it is proprietary. Most of it is small. The interesting parts are the boundaries between vendors, the trust placed in their telemetry, and the orchestration that happens between their lanes. Happy to compare notes. The system in this writeup runs on a residential install in the Pacific Northwest. All the integrations are off-the-shelf Home Assistant components except the policy engine and the safety logic, which are this project's own code. The drift sentinel runs as a systemd timer service on the HA host. The full ticket trail and architectural decisions are tracked in a private Plane workspace; happy to share specific commit references on request.

Accessibility question: is nesting interactive elements bad?

I am currently writing a gallery script for myself and ran into an interesting accessibility question. I have a list of galleries with links to each of them. I also wanted to provide a checkbox to allow users to select several galleries and merge or download them. The HTML I use is the following. An unordered list with labels and checkboxes and links inside the label.

Enter fullscreen mode Exit fullscreen mode Given the right CSS and some breathing space this works well with a mouse and keyboard. You can click next to the link to check the checkbox and on the link to navigate to the gallery. It also works using a keyboard. You can tab through the list and check/uncheck using the space bar. The following screencast shows what that looks like. Now, it feels wrong though. I am mixing two interaction modes here, navigation and selection, one being link based and the other form based. I am wondering if that creates any issues for screenreader users. The other thing I am wondering about is if there is an issue with nesting all in the label as some older assistive technology didn’t like that. I can work around that using for and ids :

Enter fullscreen mode Exit fullscreen mode The question though is if that is still an accessibility issue and if it doesn’t make more sense to show the navigation as links and create a toggle to switch to the selection use case? What do you think? You can play with the demo page here .

I Accidentally Built an AI Employee Out of Scripts and Bad Sleep Habits

The kitchen table had become infrastructure without anyone formally deciding it should. Two laptops sat open because one had quietly developed thermal problems months earlier and now worked better when left mostly alone. There was dust trapped under keycaps, tangled USB cables wrapped around a cheap mouse, and a notebook filled with diagrams that looked increasingly less like project planning and more like someone mapping utility lines under a city. The apartment was warm in the way apartments get when machines have been running for days. Not dangerously warm. Just enough that you notice when you walk back into the room. I woke up because the fan noise never stopped. That was unusual. Before going to sleep, I had queued a few repository checks and left some scripts running because I wanted documentation updates waiting for me in the morning. Nothing ambitious. Just housekeeping. Overnight, though, those scripts had triggered other scripts. Logs generated summaries. Repository changes triggered review tasks. An AI model categorized failures, updated markdown files, and generated issue notes for problems I had completely forgotten existed. The strange part was not that it worked. The strange part was realizing I had slowly crossed a line where work continued happening after I stopped participating. Nobody sets out intending to build an AI employee. The phrase itself creates the wrong picture. It makes people imagine artificial coworkers replacing humans, glowing dashboards, or expensive orchestration diagrams with arrows pointing everywhere. In practice, the systems that become genuinely useful usually emerge from irritation. You get tired of repeating something. Then you automate it. Then the automation creates another annoying bottleneck. Then you automate that too. Eventually you wake up surrounded by scripts that know your workflow better than some coworkers do. That process was gradual enough that I barely noticed it happening. Repetition Is Expensive in Ways That Are Hard to Notice Before building any of this, my workflow had become bloated with small acts of reconstruction. Open repositories. Check test results. Read logs. Copy information into notes. Open an LLM. Paste context. Forget context. Reconstruct context. Repeat. None of these tasks individually felt substantial. Together they consumed entire afternoons. This is one of the uncomfortable things about modern technical work. The exhausting part is often not the complexity. It is the context switching. Every transition between systems creates overhead. Every dashboard, notification, browser tab, and disconnected note creates tiny taxes on attention. AI tools can actually worsen this stage initially because they dramatically increase output while leaving workflow structure untouched. You suddenly generate more code, more documentation, more ideas, more summaries, and more tasks without creating systems to contain them. For a while I was producing information faster than I could metabolize it. That forced a different question. Not: “How do I make the model better?” Instead: “Why am I still manually touching this step?” That question turned out to be dangerous because nearly every repeated action started looking suspicious. The First Useful Automation Was Almost Embarrassingly Small People often expect a turning point story here involving some sophisticated agent framework. It was a shell script. That script did four things. Run tests. Collect outputs. Store logs. Generate summaries. That was enough. Not because the script itself was powerful, but because it introduced persistence into places where persistence did not exist before. Soon another script checked dependencies across projects. Another scanned repositories for stale TODO comments. Another watched directories and categorized outputs. Scheduled tasks started running overnight because unused CPU time felt wasteful. I added notifications only for unusual events because constant alerts train you to ignore alerts entirely. Eventually I realized the individual scripts mattered less than the relationships between them. Automation systems rarely become useful through intelligence alone. They become useful through continuity. Schedulers matter. Storage matters. Logging matters. Boring infrastructure matters more than people want it to. Cron jobs are not glamorous. Filesystem watchers are not glamorous. Append only logs are not glamorous either. Still, these simple pieces create something important: work that persists without requiring continuous attention. An AI Employee Is Mostly Scheduling Wearing a Fancy Hat The phrase “AI employee” survived because it is marketable. The reality is much less cinematic. What people actually need is usually persistent labor. A useful automation system notices events, performs constrained tasks, stores outputs, and surfaces exceptions. That is closer to what most teams require than some fully autonomous digital coworker wandering around repositories making independent decisions. My setup eventually stabilized into something like this: Repository activity triggered watchers. Watchers triggered scripts. Scripts gathered information and passed constrained tasks to models. Results entered storage layers where later scripts could categorize or summarize them. Notifications only appeared when thresholds were crossed. Notice how little of that description involves prompting. Prompting culture sometimes treats language models as the center of the universe. Infrastructure quietly determines whether those outputs become useful or disappear into folders you never reopen. The more systems I built, the more obvious this became. Memory beats intelligence surprisingly often. Overnight Systems Change Your Relationship With Time The first genuinely unsettling moment happened after setting up overnight repository sweeps. I woke up expecting maybe a few reports. Instead there were dozens. Documentation updates. Dependency warnings. Suggested refactors. Issue summaries. Risk rankings. Generated notes explaining architectural weaknesses I had forgotten existed. Some recommendations were excellent. Some were nonsense. One confidently suggested removing code responsible for authentication because it misinterpreted usage patterns. That experience permanently changed how I think about autonomous systems. People talk about AI mistakes as if mistakes are exceptional. Mistakes are the operating environment. The goal is not creating systems that avoid failure. The goal is building systems where failure remains visible. That requires review layers. Stored outputs. Audit trails. Approval checkpoints. The moment automation becomes invisible, reliability starts degrading. Machines repeat errors more consistently than humans do. That consistency is useful if you can observe it. Dangerous if you cannot. Physical Spaces Quietly Reshape Technical Systems Something else changed that I did not expect. The room changed. Folders became cleaner because messy storage created automation failures. Desk layout changed because notifications constantly entering peripheral vision became exhausting. I separated monitoring screens from active work screens. Started keeping handwritten checkpoints because physical notes created friction against impulsive task switching. Bought cheap notebooks specifically because expensive notebooks made me weirdly protective of blank pages. These details sound unrelated until you live inside automated systems long enough. Interfaces train behavior. Physical environments train behavior too. When your projects operate continuously, organization stops being aesthetic preference and becomes system reliability. Small environmental choices become infrastructure. The Mistakes Were More Educational Than the Successes One automation loop accidentally generated documentation updates using stale assumptions for several days. Another duplicated issue reports so aggressively that repositories became harder to navigate afterward. I once built a notification system that sent updates for everything because more visibility sounded useful. After two weeks I had trained myself to ignore notifications entirely. Failure patterns taught more than successful runs ever did. A few rules survived repeated mistakes: Keep raw outputs separate from approved outputs. Timestamp everything. Build kill switches. Prefer append only logs. Constrain scope aggressively. These principles sound boring because they are. Most reliability practices are boring. That is partly why people skip them. Bad Sleep Habits Were Not the Solution, But They Exposed the Problem I would love to pretend this system emerged through disciplined optimization. It mostly emerged through accumulated annoyance and poor sleep. Fatigue changes your tolerance for friction. Repeated actions become unbearable faster. Opening the same dashboards every morning started feeling absurd. Rebuilding project context repeatedly felt absurd. Discovering failures hours late felt absurd. Exhaustion exposed inefficiencies that motivation had previously hidden. That does not make sleep deprivation useful. It makes friction easier to notice. The actual solution was building systems that reduced dependence on constantly available attention. Human focus fluctuates. Projects do not stop existing when focus disappears. Persistent systems help bridge that gap. Start Smaller Than You Think You Need People consistently begin automation projects at the wrong scale. Multi agent research swarms. Autonomous startup operators. Complex orchestration graphs. Meanwhile, documentation remains outdated and dependency updates go unchecked. Start with one task. One repeated annoyance. One responsibility. Create something that reviews pull requests nightly. Summarizes logs. Categorizes research notes. Generates documentation snapshots. Then leave it running. Observe failure patterns. Expand slowly. The useful systems rarely arrive fully formed. They accumulate. Right now, writing this, several scripts are running in the background. Not because I particularly enjoy automation theater. Mostly because somewhere between repository watchers, scheduled jobs, and piles of generated reports, I realized completed work waiting in the morning changes how projects feel. Projects stop depending entirely on your current energy level. That shift is subtle at first. Then one day the laptop fan is still running when you wake up, the machine spent the night organizing problems you forgot existed, and the line between tools and coworkers becomes slightly harder to locate than you expected.

How the 80/20 Rule and GitHub Copilot Saved My Abandoned App From Code Graveyard

GitHub “Finish-Up-A-Thon” Challenge Submission This is a submission for the GitHub Finish-Up-A-Thon Challenge What I Built AlkaFocus is a high-performance, mobile-first productivity engine engineered to help developers focus on what truly moves the needle. Far too many productivity apps treat all tasks equally, leading to cognitive fatigue and developers drowning in low-impact administrative work. Built with React Native (Expo Router), TypeScript, Zustand, and NativeWind (Tailwind CSS) , AlkaFocus bridges the gap between structured time management and the 80/20 Pareto Principle. It forces you to isolate the critical 20% of engineering effort that yields 80% of your real results, tracking your daily momentum in real-time. Demo Try the Code GitHub Repository: alphonsekazadi / AlkaFocus AlkaFocus A productivity engine for developers that enforces the 80/20 rule, surfacing the top 20% of high-impact work and eliminating burnout through disciplined focus. Overview AlkaFocus is a mobile-first productivity app built to help developers isolate and execute the most critical tasks. It is designed around a modern three-tab native experience, delivering a premium dashboard, a focused Pomodoro timer workflow, and actionable performance analytics. Core Architecture Built with a strong native-first architecture and scalable React tooling: React Native + Expo : Native mobile performance with Expo SDK stability. Expo Router : Native tab-based routing with a declarative file-system structure. TypeScript : Strong typing throughout the app. Zustand : Lightweight state management with persistent storage. AsyncStorage : Offline persistence to retain task state and app preferences. NativeWind : Tailwind-style styling for fast UI polish. EAS Build : Production-ready Android APK distribution configuration. High-Impact Feature Breakdown Core Experience Three-Tab Architecture Home / … View on GitHub Expo Sandbox / Production APK: https://expo.dev/accounts/alphonsekazadi/projects/AlkaFocus/builds/0035e608-fa6b-49f4-946a-87148bc5cbe5 2.📱 See It in Action The Comeback Story Where it Started AlkaFocus began as a personal passion project—an offline utility script that sat abandoned and unpolished in my GitHub repositories. It lacked routing, lacked state management persistence, and suffered from a rigid, non-scannable design layout. It was a classic “half-finished dev project” buried under a stack of Master’s degree studies and software engineering workflows. The Transformation The GitHub Finish-Up-A-Thon was the ultimate catalyst to revive it. Over an intense development sprint, I refactored the entire architecture from the ground up: State Store Overhaul : Replaced local reactive loops with a persistent state engine via Zustand and AsyncStorage to ensure user task data safely survives device reloads. Native Layout Engineering : Migrated to Expo Router to structure a smooth, three-tab modular layout pipeline (Timeline, Timer, Review) with dynamic custom tab bars. Hardware Styling Upgrades : Built production-grade, hardware-pinned headers that stay perfectly static while views scroll smoothly beneath them. A Fully Fluid Dynamic UI : Programmed an instant Dark/Light mode toggle system that updates the entire visual language in real-time, complete with a hardware StatusBar listener to flip phone system icons dynamically on iOS and Android devices. My Experience with GitHub Copilot As a developer balancing software architecture and cybersecurity research, speed and focus are my primary levers. GitHub Copilot acted as a top-tier pair-programmer throughout this entire crunch period, dramatically accelerating my development velocity. How Copilot Accelerated the Sprint: Context-Aware Tailwind Layouts: Copilot instantly accurately predicted deep utility string tokens for NativeWind, allowing me to build premium dark slate blocks (bg-slate-900) and handle light theme overrides without breaking native layout trees. Algorithmic Precision for Pareto Insights : Writing the conditional analysis logic to evaluate a user’s performance arc was effortless. Copilot helped me seamlessly parse array elements inside the Zustand store to compute exact data insights, calculating if a developer is actually focusing on high-impact sprints or burying momentum in administrative noise. Refactoring Safety: When transitioning from a standard standard page routing configuration to a slide-up native presentation sheet modal (presentation: “modal”), Copilot helped me correctly map my core app layout tree constraints, saving me hours of local bundler troubleshooting. Thanks to Copilot, I cut boilerplate setup by an estimated 70%, enabling me to focus 100% of my cognitive energy on architectural design and mobile user experience.

The Complete Developer’s Guide to the Baileys WhatsApp Bot: Setup, Scaling, and VPS Deployment

WhatsApp has become the default operating system for daily communication in regions like India. For modern web platforms—particularly in EdTech, local logistics, or localized services—forcing users to log into a complex desktop portal often results in a steep drop-off in user engagement. When building LoopLearnX (an automated homework evaluation and tutoring tool for CBSE students), we realized that students rarely log in to a web dashboard on a desktop to upload their homework. Instead, they do their homework on physical notebooks, snap a picture, and expect instant grading. Integrating a custom, self-hosted WhatsApp interface directly into our Next.js application was not just a convenience—it was the single most critical driver of student engagement. This guide details the technical blueprint of how we built a resilient, memory-aware WhatsApp AI Bot using @whiskeysockets/baileys and Next.js, hosted on an Oracle Cloud VPS. We will cover the exact production failures we encountered, learnings learned, and why custom self-hosting beats off-the-shelf agent frameworks. 🚀 1. Why WhatsApp & Baileys? The Engagement Multiplier For many demographics, WhatsApp represents friction-free engagement. Users don’t need to remember passwords, manage active sessions, or learn a new user interface. By bringing our platform inside a messaging channel, we instantly enabled frictionless student homework submissions. The Bot Core: Why Baileys? To connect an application to WhatsApp, you have two primary routes: The Official WhatsApp Business Cloud API: Extremely restrictive, expensive (per-conversation pricing), and requires Facebook Business Verification. It strictly forbids sending arbitrary free-form text or non-template messages outside a 24-hour window. Baileys ( @whiskeysockets/baileys ): A high-performance, headless, WebSocket-based implementation of the WhatsApp Web protocol. It allows you to programmatically control a WhatsApp account (including standard consumer or business accounts) with full messaging flexibility, zero per-message charges, and native support for modern features like multi-file authentication state. The Hybrid Architecture To keep operations lightweight, we split the application into a two-tier architecture : The Gateway (Ubuntu VPS): Runs a lightweight Node.js daemon using Baileys to maintain WebSocket connections with WhatsApp servers 24/7. It listens to incoming messages, handles media download streams, and converts payloads into clean base64 data to pass forward. The Logic Engine (Vercel Serverless): A secure Next.js API route that handles heavy database transactions (Supabase), state transitions, and LLM evaluations (Gemini-2.5-Flash). [Student WhatsApp] │ ▼ (WebSocket 24/7 connection) [Node.js VPS Gateway (Baileys + PM2)] │ ▼ (HTTP POST with x-bot-secret) [Next.js Serverless Route (Vercel)] ├── 1. Authenticate Request ├── 2. Query Student Profile & History (Supabase) ├── 3. Classify & Evaluate Intent (Gemini API) └── 4. Write new Submission Record (Supabase) │ ▼ (JSON Reply) [Node.js VPS Gateway (Safe Queued Output)] ──► Sent back to Student WhatsApp Enter fullscreen mode Exit fullscreen mode 🛠️ 2. Step-by-Step Code Walkthrough Part A: Setting up the Baileys Client ( index.js ) The core responsibilities of index.js on the VPS are maintaining the WebSocket session, managing authentication states, rendering QR codes for linking, and mounting an Express endpoint to monitor status. // index.js require ( ” dotenv ” ). config (); const { default : makeWASocket , useMultiFileAuthState , DisconnectReason , } = require ( ” @whiskeysockets/baileys ” ); const { Boom } = require ( ” @hapi/boom ” ); const pino = require ( ” pino ” ); const express = require ( ” express ” ); const qrcodeTerminal = require ( ” qrcode-terminal ” ); const qrcode = require ( ” qrcode ” ); const { handleIncomingMessage } = require ( ” ./bridge ” ); const app = express (); const PORT = process . env . PORT || 3000 ; let sock = null ; let botStatus = ” starting ” ; let currentQrImage = null ; async function connectToWhatsApp () { // 1. Initialize multi-file authentication state const { state , saveCreds } = await useMultiFileAuthState ( ” auth_info_baileys ” ); sock = makeWASocket ({ auth : state , printQRInTerminal : false , // We render custom QR inside terminal & web UI logger : pino ({ level : ” silent ” }), }); // 2. Listen for connection state updates sock . ev . on ( ” connection.update ” , async ( update ) => { const { connection , lastDisconnect , qr } = update ; if ( qr ) { botStatus = ” qr_needed ” ; // Render QR in terminal qrcodeTerminal . generate ( qr , { small : true }); // Generate Data URL QR for web UI status page currentQrImage = await qrcode . toDataURL ( qr ); } if ( connection === ” close ” ) { const shouldReconnect = lastDisconnect ?. error instanceof Boom ? lastDisconnect . error . output ?. statusCode !== DisconnectReason . loggedOut : true ; botStatus = shouldReconnect ? ” disconnected ” : ” logged_out ” ; console . log ( ” Connection closed. Reconnecting… ” , shouldReconnect ); if ( shouldReconnect ) { connectToWhatsApp (); } } else if ( connection === ” open ” ) { botStatus = ” connected ” ; console . log ( ” ✅ WhatsApp WebSocket Connected successfully! ” ); } }); // 3. Save updated credentials on session changes sock . ev . on ( ” creds.update ” , saveCreds ); // 4. Mount incoming message listener sock . ev . on ( ” messages.upsert ” , async ( m ) => { if ( m . type === ” notify ” ) { for ( const msg of m . messages ) { if ( ! msg . key . fromMe ) { await handleIncomingMessage ( sock , msg ); } } } }); } // Simple web UI endpoint for linking & status monitoring app . get ( ” / ” , ( req , res ) => { res . send (

LoopLearnX Bot Status

Current Status: ${ botStatus }

${ botStatus === " qr_needed " && currentQrImage ? Scan QR Code : "" } ); }); app . listen ( PORT , () => { console . log ( Express status server running on port ${ PORT } ); connectToWhatsApp (); }); Enter fullscreen mode Exit fullscreen mode Part B: Creating a Resilient Message Handler ( bridge.js ) The bridge.js file handles payload filtering, captures typed text, and handles complex media streams. One of the biggest issues in production is text messages arriving empty at Vercel. WhatsApp packs text differently based on messaging schemas. We wrote a nested parser that extracts text under all possible client payloads. Additionally, when receiving an image, the bot downloads the file buffer, converts it to base64, and triggers our serverless endpoint: // bridge.js const axios = require ( ” axios ” ); const { downloadMediaMessage } = require ( ” @whiskeysockets/baileys ” ); const API_URL = process . env . LOOPLEARN_API_URL ; const BOT_SECRET = process . env . WHATSAPP_BOT_SECRET ; async function handleIncomingMessage ( sock , msg ) { const jid = msg . key . remoteJid ; if ( ! jid || jid . endsWith ( ” @g.us ” )) return ; // Skip group chats const phone = jid . replace ( ” @s.whatsapp.net ” , “” ); const content = msg . message ; const imageMsg = content ?. imageMessage ; const isText = !! ( content ?. conversation || content ?. extendedTextMessage ?. text ); // 1. Text Message Processing Route if ( isText ) { const textBody = content ?. conversation || content ?. extendedTextMessage ?. text || “” ; if ( ! textBody . trim ()) return ; await callApi ( ” /api/whatsapp/receive ” , { phone , messageType : ” text ” , textBody : textBody . trim (), }) . then (( data ) => { if ( data ?. replyText ) queueMessage ( sock , jid , data . replyText ); }) . catch (() => { queueMessage ( sock , jid , ” ⚠️ System check failed. Please try again. ” ); }); return ; } // 2. Multimodal Photo Homework Route if ( imageMsg ) { queueMessage ( sock , jid , ” 📸 Photo mila! Evaluate ho raha hai… thodi der ruko. ⏳ ” , ); let imageBuffer ; try { // Securely download the encrypted media buffer from WhatsApp servers imageBuffer = await downloadMediaMessage ( msg , ” buffer ” , {}); } catch ( e ) { console . error ( ” Image download error: ” , e . message ); queueMessage ( sock , jid , ” ❌ Photo download fail. Please try again. ” ); return ; } const imageBase64 = imageBuffer . toString ( ” base64 ” ); const mimeType = imageMsg . mimetype || ” image/jpeg ” ; await callApi ( ” /api/whatsapp/receive ” , { phone , imageBase64 , mimeType , messageType : ” image ” , }) . then (( data ) => { const reply = data ?. replyText ?? ” ⚠️ Evaluation failed. Dobara try karo. ” ; queueMessage ( sock , jid , reply ); }) . catch (( e ) => { console . error ( ” API error: ” , e . message ); queueMessage ( sock , jid , ” ⚠️ Server connection timeout. Please try again. ” , ); }); return ; } } async function callApi ( path , body ) { const res = await axios . post ( ${ API_URL }${ path } , body , { headers : { ” Content-Type ” : ” application/json ” , ” x-bot-secret ” : BOT_SECRET , }, timeout : 90000 , // 90-second timeout — Gemini Vision can be slow }); return res . data ; } Enter fullscreen mode Exit fullscreen mode 🚫 3. Crucial: Solving the \”Ban & Crash\” Problem (Rate-Limiting Queues) If your bot sends multiple API calls instantly to the same recipient or pushes bulk updates simultaneously, WhatsApp will trigger a session ban. We mitigated this risk using an asynchronous, rate-limited memory queue: const sendQueue = []; let sending = false ; function queueMessage ( sock , jid , text ) { sendQueue . push ({ jid , text }); processSendQueue ( sock ); } async function processSendQueue ( sock ) { if ( sending || ! sendQueue . length ) return ; sending = true ; while ( sendQueue . length ) { const { jid , text } = sendQueue . shift (); try { await sock . sendMessage ( jid , { text }); } catch ( e ) { console . error ( ” WebSocket send error: ” , e . message ); } // Artificial delay mimicking natural human interaction patterns await sleep ( 1500 + Math . random () * 1500 ); } sending = false ; } Enter fullscreen mode Exit fullscreen mode 💡 4. Production VPS Deployment & Management To run the Node.js Baileys gateway in a professional VPS environment, you must secure your server with PM2 process monitors and fail-safes.

Step 1: Install VPS Dependencies Connect to your Ubuntu server: sudo apt update

&& sudo apt upgrade -y curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash – sudo apt install -y nodejs sudo npm install -g pm2 Enter fullscreen mode Exit fullscreen mode

Step 2: PM2 Configuration ( ecosystem.config.js ) Create a custom configuration file

Warning: You must run only 1 instance to prevent authorization lock conflicts: // ecosystem.config.js module . exports = { apps : [ { name : ” looplearnX-bot ” , script : ” index.js ” , instances : 1 , // DO NOT USE MAX (Cluster mode breaks Baileys) autorestart : true , watch : false , max_memory_restart : ” 500M ” , restart_delay : 5000 , // Wait 5s before rebooting on crash env : { NODE_ENV : ” production ” , }, }, ], }; Enter fullscreen mode Exit fullscreen mode Start the bot and make it persistent across system updates: pm2 start ecosystem.config.js pm2 save pm2 startup Enter fullscreen mode Exit fullscreen mode To monitor logs and check performance status: pm2 logs looplearnX-bot pm2 status Enter fullscreen mode Exit fullscreen mode 🧠 Why Build Custom Instead of Using Off-the-Shelf Agents (Hermes, Landbot)? When setting up a WhatsApp integration, many teams consider wrapper services like Hermes, Coze, or standard flow builders like Landbot. Here is a technical breakdown of why we rejected off-the-shelf agents in favor of a custom Baileys/Next.js stack: Evaluation Metric Off-The-Shelf Agents (e.g. Hermes, Landbot) Custom Self-Hosted Stack (Baileys + Next.js) API & Database Integration Restricted to webhooks and limited UI components. Direct access to server-side Postgres (Supabase client), executing transactions natively. Memory Architecture Generic system chat history (context window size limitations). Custom Memory Context Routing . We query previous attempts for that exact homework plan ID and feed that specific context straight to Gemini. Hinglish & Direct Tone Tuning Very hard to enforce strict localized prompt guidelines consistently. Full controller prompts. The model speaks in second-person direct Hinglish ( “Aapne” instead of “Student ne” ). Pricing Scaling Per-message/per-run markup pricing (can grow to thousands of dollars). $0 SaaS Fees. You only pay for a $3 VPS (Oracle/Hetzner) and raw token consumption on Gemini API. Summary Integrating the Baileys WhatsApp Bot with Next.js on an Oracle Cloud VPS completely transformed the adoption curve of our LoopLearnX EdTech platform. Instead of fighting friction on desktops, students now have an active personal AI tutor in their pockets. Self-hosting using Baileys gives you total database sovereignty, complete control over token pricing, and the ability to customize your conversational workflows with zero platform restrictions. The key to operational success is keeping your VPS thread-safe, deploying rate-limited queues, and handling serverless timeout boundaries gracefully. Naveen Gaur is a WordPress Performance Specialist & Full-Stack Consultant specializing in speed optimization, Core Web Vitals, and technical audits for high-performance websites. Naveen Gaur | WordPress Performance Specialist & Full-Stack Consultant WordPress Performance Specialist & Full-Stack Consultant | Technical SEO · Emergency Recovery · Custom Web Apps | Helping Founders Fix What Others Can’t naveengaur.com

An LLM API call, in 4 GIFs

Statelessness and cost-saving tips This is the first post of series Building TinyAgent where we are going to build a small agent from scratch in Node.js with no frameworks just the API calls. But before we write an agent, we need to understand what actually happens when you call an LLM. If you’ve only ever used a SDK, you’ve probably never seen the raw request and understand how it works. Six lines of code, an API key, and it just works but you have no idea what happened when request was dispatched and response was printed on the screen. 1. The request Here is the sample API call with each and every section explained in detail. A few things worth noticing in the API call. The API is stateless : Every new API call does not remember previous call context. If you want a chatbot that “remembers” earlier messages, you hold the messages array and resend the whole thing every time. max_tokens is a hard stop, not a target. If you hit the target the response stops mid sentence. The API call pattern is universal. Different URL, Authorization: Bearer instead of x-api-key, the system prompt lives inside messages rather than at the top level. But it’s the same POST, the same JSON, the same {model, messages, max_tokens}. Once you understand the shape, switching providers is just a find-and-replace. 2. The response The API answers with a JSON blob. There are ~10 fields in it, but only four actually matter: The one which is mostly skipped is: stop_reason . It tells you why the model stopped, and in real systems and there could be possible reasons behind it: end_turn → finished naturally, you’re done max_tokens → hit your ceiling, response is truncated tool_use → model wants to call a tool (next post!) stop_sequence → matched one of your stop strings Enter fullscreen mode Exit fullscreen mode If you only check the text and ignore stop_reason , you will ship a bug at some point. The response looks fine right up until it doesn’t. The other field worth burning in: usage . It shows you how many tokens went in and came out. You want this number in your logs from day one not after you get a surprise bill. 🤯 3. Tokens I keep saying “24 input tokens.” Here’s what that means: Things that surprise people and is worth noting: Words don’t equal tokens. “Unbelievable” is one word but four tokens. The tokenizer splits on common substrings, not spaces. Code costs more than it looks def add(a, b): is 8 tokens. Every bracket and comma is its own token. JSON is expensive. {“a”:1} is 7 tokens. If your tool schemas are bloated, they’re quietly eating into your budget on every single request. Non-English costs more Japanese, Hindi, Arabic tend to run 2–4× the token count of the same content in English. If you’re building for a global audience, this changes your cost math a lot. Rule of thumb for English prose: ~1 token ≈ 4 characters ≈ 0.75 words. For everything else, run it through the tokenizer yourself before assuming. 4. The bill Two meters run on every call. They are priced differently Output tokens cost roughly 3–5× more than input tokens. That’s the one number to internalize about LLM pricing. cost = (input_tokens / 1,000,000) × input_price + (output_tokens / 1,000,000) × output_price Enter fullscreen mode Exit fullscreen mode Three things that follow from the asymmetry: Long prompts are cheap. Long responses are expensive. Stuffing 50 KB of context into a system prompt is fine. Asking for 50 KB of output is roughly 5× more expensive. “Thinking” tokens count as output. Reasoning models bill their internal thought at the output rate, even though you don’t see it. Tool schemas eat input on every call. They get resent with every request, just like the system prompt. At $0.006 per call, 100k calls a day is $600/month from one small feature. Add usage logging now, not when you get the alert. 🚨 5. The whole thing in 20 lines Here is the complete code of the API call we have discussed above: Jasmin2895 / TinyAgent No dependencies and no install setup it is just Node file with API key. Three things to try before the next post Run it and watch the numbers Make ten calls, change the prompt length, see how usage moves. You’ll build a real instinct for cost faster this way than reading any doc. Set max_tokens: 20 and ask for something long. Watch it cut off. Check stop_reason. This is a bug you’ll hit in production eventually better to meet it on purpose right now Build a multi-turn chat by hand. Keep a messages array, push each user message and each model reply onto it, and resend the whole thing every turn. Once you do this, you’ll immediately understand why long conversations get expensive you’re paying for the full history on every call. What’s next In the upcoming post series we will expand the ability of the TinyAgent to actually handle lot of things than just responding. Happy Coding! 👩‍💻

Hermes Mentor — A Local AI Agent That Gets You Out of Tutorial Hell

Hermes Agent Challenge Submission: Build With Hermes Agent This is a submission for the Hermes Agent Challenge : Build With Hermes Agent What I Built Every developer knows the feeling. You’ve watched 50 hours of YouTube tutorials. You “know” React. You “know” Python. Then you sit down to build something real — and you freeze. Not because you’re not smart. But because watching is not building . This is tutorial hell. I’ve lived it. I’ve watched juniors at work live it for months. Hermes Mentor is a fully local, privacy-first AI mentorship agent that pulls you out of it — not with more tutorials, but with a personalised project roadmap built from scanning your actual GitHub repos. Here’s what it does: 🔍 Audits your real GitHub repos — reads every public repo, checks languages, CI/CD configs, test files, README quality 🧠 Identifies your exact skill gaps — local LLM via Ollama reasons across your code to find what’s actually missing 🗺️ Generates a 4-week project roadmap — real projects, each one closing a specific gap, no tutorials 📬 Sends daily Telegram challenges — every weekday at 08:30, your nudge, hints if stuck, celebration when you ship 💬 Two-way Telegram agent — reply with your repo link and Hermes reads it, tracks your progress, creates TODO tasks 💾 Persistent memory — your developer profile lives in ~/.hermes/memory/ , updated every run 🔒 100% local and private — Ollama runs the LLM on your machine, nothing leaves your box Everything runs on WSL2. One command to start. Demo The audit running — 20 repos scanned in real time The roadmap printed — gaps identified, 4 weeks planned Telegram Message Hermes memory — your developer profile saved automatically Cron installed — daily nudges set up for 08:30 weekdays Daily nudge delivered — Week 1 Day 2 challenge on Telegram 🤯 The moment it became a two-way agent This is the screenshot that made me realise this project was something else entirely. I sent my GitHub repo link to the bot on Telegram after completing the CI/CD challenge. Hermes didn’t just reply with text. It: Read the GitHub repo link and understood the context Ran a terminal command — echo ‘Pipeline check is successful. Opening PR for review.’ Created a real Pull Request on GitHub — TheCoderAdi/Basic_Calculator · Pull Request #1 Marked the TODO as completed — “Create pull request for CI/CD pipeline changes.” → status: completed This is Hermes Agent’s tool use, terminal access, GitHub integration, and task planning all firing together in real time — over Telegram — powered entirely by a local LLM on my machine. No cloud. No API keys. A fully autonomous agent that reviewed my work, opened a PR, and closed its own task. All from a Telegram message. Landing Page thecoderadi.github.io Code Repository: github.com/TheCoderAdi/hermes-mentor Project Structure hermes-mentor/ ├── mentor_agent.py ← Core agent: GitHub audit + roadmap + Telegram ├── hermes_cron.py ← Daily nudge scheduler (weekdays 08:30) ├── setup.sh ← Automated setup wizard ├── requirements.txt ├── .env.example ├── hermes-mentor.html ← Project landing page ├── config/ │ └── hermes-config.yaml ← Hermes Agent config (Ollama + Telegram) └── skills/ └── github-audit-mentor.md ← Reusable Hermes skill file Enter fullscreen mode Exit fullscreen mode My Tech Stack Layer Tool Agent orchestration Hermes Agent (NousResearch) Local LLM Ollama — qwen2.5-coder:7b GitHub data PyGithub — GitHub REST API Messaging python-telegram-bot — Telegram Bot API Scheduling Hermes cron + Linux crontab Memory Hermes persistent memory — ~/.hermes/memory/ Skill system Hermes skill files — agentskills.io format Environment WSL2 on Windows Language Python 3.12 How I Used Hermes Agent Hermes Mentor doesn’t just mention Hermes — every core capability is actively used. Here’s exactly how: Persistent Memory — the agent remembers you forever After every GitHub audit, Hermes writes USER_TheCoderAdi.md directly into ~/.hermes/memory/ . Every future Hermes session loads this file automatically. The agent already knows your skill level, active roadmap week, and past struggles — without you ever re-explaining yourself. This is what turns a one-shot script into a real mentor. It builds a relationship with you over time. Skill Learning (GEPA Loop) — gets smarter every run The github-audit-mentor.md skill file is not static documentation. After every audit it gets updated with the latest findings — gaps found, roadmap generated, developer profile. This is Hermes’ Generate-Evaluate-Patch-Apply loop making the skill more accurate and personalised with each developer it touches. Cron Scheduling — autonomous daily action hermes_cron.py registers a weekday 08:30 cron job that auto-advances through your roadmap week and day, firing the right personalised Telegram nudge every morning. The user does nothing after setup. Hermes just shows up, every day, like a real mentor. Two-Way Telegram Gateway — live agent conversations The most unexpected moment in building this: when I started hermes gateway and sent my own GitHub repo link to the bot, Hermes read the message , recognised the URL, created TODO tasks with in_progress and pending status, and replied with next steps. That’s Hermes’ tool use, task planning, and messaging gateway all firing together in real time. Multi-Step Agentic Reasoning — the full loop Fetch repos → Extract language/CI/test signals → Reason about gaps → Generate targeted project per gap → Deliver via Telegram → Save to memory → Update skill file → Listen for replies → Track progress → Plan next steps Enter fullscreen mode Exit fullscreen mode Each step informs the next. This is what separates Hermes Mentor from a chatbot. Local LLM via Ollama — private by design The entire reasoning layer runs on your machine through Ollama. No OpenAI. No Anthropic. No billing. Your GitHub activity, your learning gaps, your daily habits — stay on your box. This felt philosophically aligned with Hermes itself — an open source agent you run on your own infrastructure. Why I Built This I’m an SDE at KFintech and I’ve been building things for years. But I remember tutorial hell clearly — watching videos for months, feeling productive, then sitting down to build something real and freezing completely. The problem isn’t knowledge. It’s the gap between watching and doing. Roadmap.sh gives you a static path. GitHub Copilot helps you write code. But nobody looks at what you’ve actually built and tells you specifically what to build next to close your specific gaps. That’s Hermes Mentor. And watching it scan my own GitHub, find my real gaps, generate a roadmap, send it to my Telegram, and then reply when I shared my repo — that moment reminded me why I love building things. Built by Aditya Swayam Siddha · @TheCoderAdi

Hack your AWS CLI to add CloudShell support and turn your terminal into a bastion

I’ve been using AWS CloudShell from the Console for a while. It’s convenient: a pre-authenticated shell in your browser, right there in the AWS Console. But I always wondered: why can’t I use it from my terminal? Why is there no aws cloudshell command? Turns out, you can make it happen. The API exists, it’s just not public. And once you have CLI access to CloudShell, you can do interesting things with it, like using a VPC-attached CloudShell as a bastion to reach your private RDS instances. Checkout the companion repository as you read through this blog post. CloudShell: an undocumented API AWS CloudShell has no official SDK or CLI support. But the Console has to talk to something , right? By looking at what the browser does when you open CloudShell, you can reverse-engineer the API. Thankfully, Jérémie Guyon already did that work and published a boto3-compatible service model. His work made this whole thing possible. The API is straightforward: create environments, start/stop them, create sessions, upload/download files. The session mechanism uses SSM’s WebSocket protocol under the hood, which means session-manager-plugin (the same binary that powers aws ssm start-session ) can connect to CloudShell sessions. Teaching the AWS CLI a new trick The AWS CLI has a little-known feature: aws configure add-model . Give it a JSON service model, and suddenly the CLI knows about a new service. AWS uses this internally for private previews. (The boto3 model from Jérémie’s repo just needs a “version”: “2.0” field added at the top level to become CLI-compatible.) Run: aws configure add-model \ –service-model file://cloudshell-cli-model.json \ –service-name cloudshell Enter fullscreen mode Exit fullscreen mode That’s it. Now I have aws cloudshell with tab completion and everything: $ aws cloudshell help AVAILABLE COMMANDS create-environment create-session delete-environment describe-environments get-environment-status start-environment stop-environment … Enter fullscreen mode Exit fullscreen mode Connecting to CloudShell from the terminal The workflow is simple: # Create or find an environment aws cloudshell create-environment –region eu-west-1 # Wait for it to be RUNNING aws cloudshell get-environment-status –environment-id –region eu-west-1 # Create a session and connect session-manager-plugin ” $( aws cloudshell create-session \ –environment-id \ –session-type TMUX \ –tab-id ” $( uuidgen | tr ‘[:upper:]’ ‘[:lower:]’ ) ” \ –q-cli-disabled \ –region eu-west-1 \ –query ‘{SessionId:SessionId,TokenValue:TokenValue,StreamUrl:StreamUrl}’ \ –output json ) ” eu-west-1 StartSession Enter fullscreen mode Exit fullscreen mode And you’re in. A full shell on a CloudShell instance, from your terminal. No browser needed. The credentials problem There’s a catch. When you use CloudShell from the Console, AWS injects your credentials automatically via a PutCredentials API call. This uses your console session token (the cookie-based auth from your browser login) to feed temporary credentials into the container’s metadata endpoint. When you connect programmatically, that doesn’t happen. The container’s credential endpoint returns a 500 error. You need to inject credentials yourself: # Run locally, then paste the output into your CloudShell session aws configure export-credentials –profile my-profile –format env Enter fullscreen mode Exit fullscreen mode Not ideal, but it works. The bastion use case Here’s where it gets interesting. You can create a VPC-attached CloudShell environment: aws cloudshell create-environment \ –environment-name db-access \ –vpc-config ‘{ “VpcId”: “vpc-abc123”, “SubnetIds”: [“subnet-private-1”], “SecurityGroupIds”: [“sg-allowed-by-rds”] }’ \ –region eu-west-1 Enter fullscreen mode Exit fullscreen mode Put it in the same security group that your RDS allows, and suddenly you can connect to your database directly from the shell: mysql -h my-instance.xxx.eu-west-1.rds.amazonaws.com -u admin -p Enter fullscreen mode Exit fullscreen mode No EC2 bastion instance to maintain. No SSH keys to manage. No hourly cost when you’re not using it (CloudShell is free). The environment suspends after 20 minutes of inactivity and you can keep it alive with aws cloudshell send-heart-beat . What doesn’t work (and I tried..) I spent a fair amount of time trying to make CloudShell work as a proper port-forwarding bastion, so you could use local tools like DBeaver against a remote RDS through it. Here’s what I found: SSM-based port forwarding doesn’t work. ECS, for instance, registers containers as SSM targets. Its SSM identifier is undocumented but once you know it, it works well, as I have described in a previous blog post . This way you can run aws ssm start-session –document-name AWS-StartPortForwardingSessionToRemoteHost . SageMaker notebooks have kinda the same behaviour. CloudShell instances/containers seem not to be registered as SSM managed instances. Or if they are, it’s hidden and as of today, no one at AWS leaked their ID format 🙂 I tried every combination of environment ID, session ID, and prefix format I could think of. None of them work. Local port forwarding through the PTY doesn’t work either. The session is a terminal, not a raw TCP stream. You can’t pipe binary MySQL protocol data through it. I even tried setting up an ncat relay inside CloudShell and tunneling through the session. The relay works fine internally, but there’s no way to expose it as a local TCP port on your machine. UDP hole punching is theoretically possible but requires the CloudShell to have internet access (NAT Gateway on its subnet), and even then you’re fighting NAT symmetry issues on both ends. I got STUN working from CloudShell, but the full hole punch is fragile and impractical for production use. So what is it good for? Honestly, quite a lot: Quick database access without maintaining a bastion EC2 instance. Connect, run your queries, disconnect. Free. Automation. You can script command execution on CloudShell via Python + session-manager-plugin . Useful for running things inside a VPC without deploying a Lambda or Fargate task. Debugging network connectivity. Spin up a CloudShell in a specific subnet/SG combination and test what can reach what. File transfer (from public environments). The get-file-upload-urls and get-file-download-urls APIs give you presigned S3 URLs. The main limitation is that you’re stuck running commands inside the shell. You can’t use it as a transparent tunnel for local tools. For that, you still need an EC2 instance with SSM agent, or an ECS task with execute-command enabled. Try it yourself I published the model and a sample script here: github.com/psantus/cloudshell-cli Installation is one command. The whole thing is a single JSON file that teaches your AWS CLI a new service. Just remember: this is an undocumented API. AWS can change or break it at any time. Don’t build anything mission-critical on top of it. But for quick VPC access from your terminal? It’s pretty great.

Reviving a 12K+ Star Abandoned Library: toastr-next v3 🍞

GitHub “Finish-Up-A-Thon” Challenge Submission This is a submission for the GitHub Finish-Up-A-Thon Challenge What I Built toastr-next v3 a complete revival of CodeSeven/toastr , one of the most-starred abandoned JavaScript libraries on GitHub with 12,000+ stars and no meaningful commits since 2016. toastr was the go-to notification library for millions of developers. But time wasn’t kind to it. it required jQuery, had no TypeScript, no dark mode, no accessibility, and its Gulp + LESS build chain was completely dead. It was a library everyone knew but nobody could use in a modern project without guilt. I picked it up, stripped it to the bones, and rebuilt it from scratch for 2026. From this 👇🏻: // 2015 — drag in jQuery just to show a toast < script src = " jquery.min.js " >< /script> / / 30 KB < script src = " toastr.min.js " >< /script> / / 5 KB // Total: ~87 KB of dead weight toastr . success ( ‘ Hello! ‘ ); Enter fullscreen mode Exit fullscreen mode To this 👇🏻: // 2026 — zero dependencies, full TypeScript, ~4 KB gzipped import { toastr } from ‘ toastr-next ‘ ; const toast = toastr . success ( ‘ Hello! ‘ ); await toast . dismissed ; // Promise API! Enter fullscreen mode Exit fullscreen mode ~4 KB gzipped. No jQuery. No bloat. Just toasts. 🍞 Demo Dark mode — toasts firing with progress bar Light mode — same demo, toggled with the ☀️ button 🌐 Live Demo: toastr-next.vercel.app/ 📦 npm: npmjs.com/package/toastr-next 🐙 GitHub: github.com/Divyesh-5981/toastr The Comeback Story Where it was The original repo — abandoned since 2016, 12k stars, zero recent activity Problem Detail jQuery required ~87 KB overhead just to show a notification No TypeScript No types, no IntelliSense, no safety No dark mode Hard-coded colors, no CSS variables No accessibility Screen readers couldn’t detect toasts at all JS-driven animations Layout thrash, janky on low-end devices No Promise API No way to await a toast dismissal No keyboard support Couldn’t dismiss with Escape key Dead build toolchain Gulp + LESS, completely unmaintained since 2016 No ESM support Global UMD only, no tree-shaking What I built 🏗 TypeScript Rewrite (Zero Dependencies) No jQuery: 100% strict TypeScript with full JSDoc. Universal Formats: Ships in ESM, CJS, UMD, and IIFE (works from Vite to raw