Autonomous Trading Agent Maintenance Triage & Alerting

Autonomous AI Trading Agent

v2.6 · Daily Production
Claude / Anthropic API MCP Robinhood GitHub Actions Pushover Python

The problem

I wanted a system that would actually place trades against a real brokerage account, every day, without me sitting at a screen making the call each time. Not a backtester, not a paper-trading toy. Something with real money behind it, built on a thesis I actually believe in: AI infrastructure spending across chips, power, data centers, and the companies that support them.

I had never written production code before this. No CS background, no bootcamp. Everything here was learned by building it.

Getting it wired up

The core loop is simple to describe and took real iteration to get right: pull market data, reason about it with Claude against the investment thesis, decide whether to act, place the trade through Robinhood via MCP, log what happened, and notify me on my phone through Pushover so I'm never surprised by what the agent did overnight.

The part that isn't obvious from the outside is state. A trading agent that runs once a day as a scheduled job doesn't remember anything between runs unless you make it. GitHub Actions spins up a fresh environment every single time, so the agent's memory of what it already owns, what it decided last time, and why, has to be written somewhere and read back in reliably on the next run.

⚠ What went wrong

Opening a committed state file on GitHub one day showed a wall of text like eyogICJhY2NvdW50X251bWJlciI6... instead of JSON. The instructions told the agent to base64-encode content itself before pushing it, an assumption left over from an earlier design that called GitHub's raw API directly with curl, which does need that. In practice the execution environment had no authenticated curl access, so the agent had always been pushing through a GitHub tool that already encodes internally. Pre-encoding on top of that produced a double-encoded blob, not corrupted data, just wrapped in one extra layer nothing downstream knew to strip.

The next morning's session opens by reading that file back and asserting it parses as valid JSON. A base64 blob fails that check immediately, which triggers a full halt by design: no trading, an alert fires, and the agent waits for a human. The actual failure mode was the agent waking up and being unable to read its own memory of the account at all.

✓ What changed

Confirmed the theory before touching any code: ran the blob through a base64 decoder by hand and got back clean, complete JSON, proof nothing was lost, just wrapped wrong. The fix was to stop pre-encoding entirely and push raw content through the GitHub tool directly, matching how state was already (correctly) being handled elsewhere in the same codebase. The same bug existed in a second place, the session-log push, months later, since fixing it for state data hadn't meant checking whether logs used the identical pattern. They did.

Auditing the validator

Beyond the encoding bug, two structured audit passes over the trade-validation logic turned up real, reproducible issues, each confirmed by actually constructing the failing input rather than reading code and guessing. A proposal with a dollar amount passed as text instead of a number crashed the validator outright with a raw Python traceback. The first patch coerced the type inside the per-proposal loop, downstream of where the crash actually happened, so re-running the same reproduction produced the identical crash. The real fix normalized every amount to a number at the very top of the function, before anything summed the list.

A separate audit caught something quieter: four small positions with 2% portfolio targets could never be topped back up after their initial purchase, because the rule requiring a 2-point gap before buying more was mathematically unreachable for a position that small, it would have to hit zero first. No test had ever held one of those positions at a nonzero value, so nothing caught it until the math was checked by hand.

What was harder than expected

The Robinhood integration surfaced friction that had nothing to do with trading logic. Quote data caps at 10 symbols per call against an 18-stock universe, so every historical pull needed manual splitting. Reconciling account math once turned up a real gap, about $31 on one test run, traced down to a single stock that had moved almost 9% in pre-market trading between two different "current price" fields the API returns; using the wrong one for position math while the account total used the other made the numbers stop adding up.

Earnings data originally meant scraping Yahoo Finance by hand, cookie authentication, retry logic, inconsistent response shapes, using only Python's standard library since package installation isn't available in the execution environment. That approach eventually hard-failed when Yahoo started returning 403 on every request. It got replaced outright, not patched, when Robinhood shipped native earnings tools inside the same MCP connection already being used for everything else, one less fragile dependency instead of a harder-fought one.

Where it stands

v2.6, running daily, with a documented history of audit sprints along the way, each one a response to something that broke or something I didn't trust yet. The tiered position sizing across 18 stocks in four risk tiers came out of wanting the thesis itself to be legible: not "the AI picked some stocks," but a structure I can explain and defend.

Currently open: a live trade audit caught the agent citing a trim rule that doesn't exist in either governing document, trading about 1.15 points over target instead of the documented 3-point threshold. Root cause traced to ambiguous wording sitting near the trim rule for that specific stock. A tightened-language fix is drafted; a code-level check that verifies a trim's stated trigger against the actual computed gap, rather than relying on wording alone, is proposed but not yet built. Listed here rather than smoothed over, because it's true.


Facility Maintenance Triage & Alerting

n8n · Claude · RAG
n8n Claude (Haiku) RAG Google Sheets Slack API Webhooks

The problem

This one started as a gap I noticed against a job posting, not a personal itch. Roles building AI into real operations kept naming the same category of tool: agent and workflow orchestration platforms like n8n, Make, Copilot Studio. I'd built the trading agent entirely in Python and Claude's API directly, real experience, but not in that specific tool family. So I picked a problem worth solving and built it in the tool I needed to learn, rather than padding a resume with a claim I couldn't back up.

The domain: industrial maintenance triage. A technician reports an issue, an AI classifies it, urgent things get escalated immediately, everything gets logged. It mirrors real operations automation without requiring domain knowledge I don't have, and it's a close cousin to the kind of equipment monitoring a geothermal or industrial operations team actually needs.

Building it as a real pipeline, not a demo

The version that would have been fastest to build: intake form triggers Claude, Claude decides urgency, urgent stuff pings Slack. That's a demo. It's also a bad design, because it has no answer for what happens when the AI gets something wrong, and no record of anything that wasn't urgent.

So the actual pipeline does three things a shortcut version wouldn't: every ticket gets logged permanently regardless of urgency, not just the escalated ones. The AI's classification is grounded in that equipment's real history (RAG) before it reasons about the current issue, so it's not just pattern-matching on the text of the report. And the AI's own output gets checked before anything trusts it, a malformed response gets routed to a human-review queue instead of silently corrupting the log.

⚠ What went wrong

The hardest bug in the whole build looked, from every angle I could check, like it shouldn't exist. The escalation listener's webhook verified correctly with Slack. The right event (reaction_added) was subscribed. The right OAuth scope was present. The workflow was published and saving executions. And still: reacting to a message in Slack produced nothing, not even a failed execution, just silence.

The actual cause was sitting on a completely different settings page: a small notice reading "Socket Mode is enabled. You won't need to specify a Request URL." Socket Mode is a separate, mutually exclusive delivery method, a persistent connection that had been silently intercepting every event and routing it away from the webhook entirely. The endpoint was correctly built, correctly verified, and was never going to receive anything while that one toggle stayed on.

✓ What changed

Disabled Socket Mode, reinstalled the app, reacted again. The request headers on the delivery that finally arrived showed x-slack-retry-num: 3, confirming Slack had genuinely been retrying delivery the whole time, correctly, to an endpoint that could never receive it while the wrong setting was on.

Choosing the harder architecture on purpose

Before building the acknowledgment tracker, there were two honest options: a scheduled workflow polling the tracker sheet every few minutes for stale alerts, roughly a 45-minute build, or a real-time webhook reacting directly to a Slack emoji reaction, architecturally more interesting and genuinely at risk of stalling out on auth. I chose the webhook upfront, time-boxed against falling back to polling if it wasn't working within about 90 minutes. It worked, in that same session, after clearing the Socket Mode issue above and a smaller one before it: n8n's webhook node auto-responds with a generic acknowledgment by default, but Slack's own verification handshake requires echoing back a specific challenge value, which needed an explicit Respond-to-Webhook node before the URL would verify at all.

A failure mode specific to visual workflow tools

The most repeated bug across the whole build wasn't really one bug, it was the same subtle failure appearing three separate times. In a node-based tool like n8n, a reference to $json resolves to whatever node sits immediately upstream, not to a specific named source. After restructuring the workflow so every ticket logs before the urgency check runs, instead of only urgent ones, three previously working things broke at once: the urgency switch stopped routing anything, the Slack alert text came back nearly empty, and later, the same thing happened again when wiring in acknowledgment fields. Nothing about those expressions had changed, the node sitting in front of them had. Fixed by replacing the implicit references with explicit ones tied to a named node, so the reference stays correct regardless of what gets inserted upstream later.

Telling real bugs from test artifacts

A few things that looked broken during testing weren't. The first real test of the malformed- output fallback, deliberately telling the AI to omit a field, didn't trigger the fallback at all, because the prompt itself had two conflicting format blocks left over from earlier edits, and the model correctly followed the more specific one. That was a flawed test, not a working system failing to catch a real problem. Once the prompt was cleaned up to one unambiguous block, the same test correctly routed to human review. Worth being honest about in a build like this: some of the debugging time went to figuring out whether something was actually broken before assuming it was.

The equipment knowledge base got a similar honesty pass after the fact: the specific service records are necessarily fictional, no public source for a real plant's maintenance logs exists, but the underlying failure modes referenced (mineral scaling in heat exchangers, brine corrosion, non-condensable gas handling) were revised to reference real, documented geothermal-industry issues rather than arbitrary invented text.

Where it stands

Two n8n workflows (the main triage pipeline and the escalation listener), a live dashboard reading directly from the underlying sheets with no backend server, and a README that doubles as the design doc, covering the architecture, the decisions above, and the limitations I'm upfront about: this is a documented working prototype, not a production deployment, and the README says so directly rather than overselling it.

The dashboard itself hit one more real issue worth naming: fetching the Google Sheet directly from the browser failed outright, since Google's export endpoint doesn't send the CORS headers browsers require for a cross-origin fetch, true for any site trying to read it, not specific to this setup. Since the dashboard is static and has no server of its own to proxy the request through, the fix was a script-tag injection technique against Google's visualization query endpoint instead of a normal fetch call, the same underlying method Google's own charting library uses, since script tags aren't bound by the same cross-origin restriction.

Planned next: replacing the equipment lookup with real vector-based retrieval grounded in actual failure modes, pulling source documents from SharePoint instead of hand-typed entries, and a conversational agent on top of the pipeline that can decide which tool to call rather than following a fixed path.