In this article
- Part 2 ended without a dashboard on purpose
- First problem: GrowthOS could see attention but not outcomes
- Typefully became the source for my own X data
- Link clicks needed another guardrail
- Resolving t.co links was easier than adding another API
- X stopped being just a distribution channel
- Hermes analysed follower growth instead of just impressions
- SocialCrawl got exactly two paid searches
- Machine prioritisation should not become machine censorship
- The “don’t build a dashboard” rule had done its job
- Dashboard v0.1 immediately proved why I wanted one
- Then the dashboard lied about Umami
- Codex became the implementation tool
- GrowthOS finally became a persistent application
- Tailscale removed the last annoying part
- What is actually persistent now?
- What I still have not built
- The biggest remaining gap is not another agent
Question: Can I turn the Hermes-based GrowthOS experiment into a persistent marketing control room that uses real data without turning it into an over-automated agent platform? More specifically, can the system become easier to operate without turning every useful workflow into another always-on agent?
Yes. The useful step wasn’t adding more autonomy, but connecting GrowthOS to first-party analytics and social data, learning where model judgement needed calibration, running one deliberately small external-intent experiment, and only then building a browser interface around the workflows that had proved useful. By the end of the session, GrowthOS was running persistently on my home server as a private web application I could open through Tailscale, while Hermes, Codex and paid data sources still only ran when there was a reason to use them.
Hermes build diary — Part 3. Part 2 covered turning Hermes into a useful marketing agent. That earlier version was intentionally small, manual and mostly file-driven, which is important context for why the dashboard came later rather than first.
Part 2 ended without a dashboard on purpose
At the end of Part 2, GrowthOS was deliberately small. I had Hermes running against a directory of durable product context, Terra doing the judgement work, DDGS providing public web search, and a human approval boundary preventing the system from sending outreach or making production changes on its own.
There was no database, dashboard, orchestrator, cron schedule or persistent GrowthOS service. That wasn’t because I thought command-line interfaces were somehow purer; I was trying to avoid the fairly common solo-builder mistake of spending several days constructing infrastructure before knowing what the infrastructure was meant to support.
The rule was basically to prove a useful workflow first, and on August 9 that rule started producing a different answer. GrowthOS was now ingesting portfolio analytics, social performance, link activity and external market signals, so running everything through shell commands and reading Markdown reports was becoming more awkward than building a small interface.
The important part is that the interface requirement came from the work rather than from a speculative dashboard mock-up. By the time I started building it, I already knew the things I wanted to see and the decisions I wanted the system to help me make.
First problem: GrowthOS could see attention but not outcomes
The Umami pipeline was already producing a deterministic flow from pull-umami.py to latest.json, then through summarise-umami.py into umami-latest.md. The first useful job was to ask Hermes what those numbers actually said about the portfolio rather than simply collecting more of them.
The answer exposed a fairly fundamental measurement problem. GrowthOS could see visits, pageviews, visitors and referrers, but it could not yet connect that attention to signups, activation or money.
SearchTriage had the highest traffic volume among the SaaS products in the report, Old Stack Journal had the most visitors, and X/t.co traffic was appearing across SearchTriage, OSJ and Automation Receipts. What I did not have was meaningful conversion-event data, so the system could tell me where attention was appearing without yet telling me which attention was producing customers.
That distinction immediately became one of the main missing pieces in GrowthOS. A system that is meant to help decide what deserves my time needs to understand the difference between visible activity and commercial outcomes, otherwise it risks turning traffic into a stronger claim than the data supports.
The same analysis produced my first useful model-calibration lesson of the day. Terra initially described SearchTriage as having the strongest “engagement” because it had the most visits and pageviews, even though some strange /login activity might itself have been inflating those numbers.
I corrected the interpretation to activity or traffic volume rather than proven engagement. That sounds like a small wording change, but it matters if the model will eventually recommend where I spend time, because a loose label can quietly become a bad portfolio decision once it travels through enough summaries.
My current policy is to review the first few runs of a new Terra workflow closely, along with surprising results and consequential portfolio decisions. Once the workflow has been calibrated and the recurring mistakes are understood, I do not need another model re-analysing every Terra output forever, because the goal is to calibrate the workflow rather than create an infinite chain of AI supervisors supervising other AI.
Typefully became the source for my own X data
I had originally been considering SocialCrawl for pulling data about my own activity on X, but Typefully already contained much of what I needed. About 99% in fact, as I write most of my posts at night in Typefully for the next day, when I’m full of ideas. That led to a cleaner split in which Typefully handles owned social performance while SocialCrawl is reserved for external market, problem and lead discovery.
GrowthOS does not need to pay an external discovery service to rediscover posts and metrics I already own. Once Typefully was connected, I could pull a 30-day window containing 253 X posts and see the follower series move from 547 to 927, an increase of 380 followers or about 69.5%.
The same snapshot contained 74,451 impressions, 3,367 engagement actions and 526 profile clicks. I wrapped the raw data in a few small scripts, including pull-typefully.py, summary-typefully.py, resolve-typefully-links.py and summarise-typefully-growth.py, so the model would receive compressed evidence rather than a large pile of raw posts.
The first version contained a date bug, which was useful in its own way. Typefully returned 253 posts, but my UTC filtering reduced the report to 249 because the date boundary did not match the Auckland-local account window.
Once I corrected the filtering to respect the local date window, all 253 posts appeared again. That is exactly why I want deterministic ingestion before model analysis, because if the underlying script silently drops four records, no amount of clever reasoning on top makes the report correct.
Link clicks needed another guardrail
Typefully also exposed an attribution trap that would have been easy for a model to turn into fake precision. Its link_clicks value is frequently unavailable, and where it is available it is a post-level metric rather than clean per-link attribution.
If one post contains two links and reports 28 link clicks, I cannot safely say that one URL received 28 clicks and the other URL also received 28 clicks. That would be turning one known number into two invented numbers, so GrowthOS now preserves the distinction between what the API actually reports and what I might wish it reported.
This is becoming a recurring pattern in the project. Most of the useful safeguards are not elaborate AI safety systems; they are small data rules that stop ambiguous evidence from becoming confident conclusions later in the pipeline.
Resolving t.co links was easier than adding another API
X creates another analytics annoyance because visible outbound URLs are shortened through t.co. Rather than sending those URLs to a model or paying another service to identify them, I wrote a deterministic resolver that follows the redirects locally and caches the destination.
It resolved 32 out of 32 URLs successfully, which made some of the traffic much easier to understand. A SearchTriage post had produced six known clicks directly to the free SEO analyser, another had four known clicks to the homepage, a TextToDeck PeerPush launch post had nine known clicks, and OSJ article URLs could be identified properly as well.
Then there was Automation Receipts, where Umami showed 12 recent visits from t.co but I could not find an obvious matching Automation Receipts post or URL in the Typefully dataset. The obvious temptation would have been to build a much bigger attribution system until every visit had a satisfying story attached to it, but I left the discrepancy as an anomaly to investigate only if it becomes commercially useful.
That decision is important because GrowthOS is meant to help me choose where not to spend time as well as where to spend it. A strange number can be worth remembering without automatically becoming a new engineering project.
X stopped being just a distribution channel
The bigger commercial change came when I stopped treating X solely as somewhere to promote SearchTriage, OSJ and the other projects. The account itself was becoming a possible commercial asset, so follower growth, reach and relevant community formation were no longer just vanity metrics that had to justify themselves through immediate product sales.
My working hypothesis is that growing a sufficiently useful and relevant audience may become meaningful revenue eventually. That does not mean product revenue stops mattering, but it does mean X growth can be treated as its own commercial motion with its own leading indicators.
The timing made that more interesting because X changed its creator-monetisation system while I was building the workflow. The old assumptions I had been carrying were no longer the assumptions GrowthOS should use, which was a good reminder that commercial rules can move underneath a system even when the code stays the same.
That makes static project context dangerous if it is treated as permanent truth. GrowthOS needs durable context, but it also needs an easy way to revise the parts of that context that depend on external platforms.
Hermes analysed follower growth instead of just impressions
Once X growth became a commercial motion in its own right, I wanted something better than asking which posts had the most views. I created a deterministic daily evidence report covering posting activity, follower changes and the available attention metrics so Terra could reason over a smaller and more relevant dataset.
One day made the problem very obvious: July 30 produced 2,620 impressions, 135 engagements and 20 profile clicks, but zero follower gain. Other days had less impressive attention numbers but considerably stronger follower growth, which made it clear that the largest visible metric was not automatically the thing I should optimise.
I gave the compressed evidence to Hermes and asked Terra to identify repeatable growth hypotheses. Importantly, it returned two useful hypotheses rather than inventing a third just because the prompt allowed more.
The first was practical-builder identity and community, meaning posts that make it obvious who the conversation is for and then invite genuine discussion among people building small SaaS products, AI tools, PHP or WordPress projects, simple-stack software, or side projects around a day job. The second was lived constraint leading to a practical lesson, especially real experiences involving limited time, maintenance, simplicity, starting again, AI-assisted building and the less glamorous parts of keeping projects alive.
Technical and news commentary could still produce attention, but the evidence was weaker for follower growth, while direct product promotion appeared to be doing a different job again. Those conclusions now live as a durable GrowthOS experiment record rather than disappearing into a chat transcript, and they can be tested again as more data comes in.
There was also an appropriately mundane failure during this part of the build because my first shell paste for that experiment file became mangled. Rather than trying to patch content I no longer trusted, I overwrote the file cleanly and verified the result, which is a useful reminder that AI-agent infrastructure still contains a surprising amount of ordinary file checking.
SocialCrawl got exactly two paid searches
With owned social data handled by Typefully, I could use SocialCrawl for the job it was better suited to: finding external discussions that might reveal SearchTriage problems, customers or useful market language. I first checked whether credentials from an older pre-Hermes GrowthOS experiment had accidentally carried over, and they had not.
I stored a new credential in the server’s private user configuration and found the existing SocialCrawl account still had 60 of its original 100 free credits. Before spending any of them, I also had to correct my own first query idea because “GSC traffic drop” sounded relevant to SearchTriage without actually matching the product’s real job.
The problem I care about is closer to someone already having Google Search Console data but not knowing what it means or what to do next. That correction mattered more than the exact query syntax because it shifted the search away from generic SEO pain and toward the actual customer problem.
I spent exactly two SocialCrawl credits testing variations around that job, producing 14 result occurrences and 11 deduplicated Reddit posts. I also built freshness rules into the summariser so that posts seven days old or newer counted as live opportunities, eight to 14 days old counted as recent, 15 to 30 days old were mainly market-language evidence, and anything older than 30 days was rejected as a current opportunity.
That freshness policy came directly from an older GrowthOS mistake where ancient Reddit discussions had been surfaced as if they were live leads. I wanted the age rule to be deterministic rather than something the model could conveniently ignore when it found an interesting-looking result.
Hermes then investigated the candidates using free DDGS searches and the SearchTriage product context. The result was intentionally underwhelming, with one recent post showing strong problem fit but weak customer fit, while another older discussion provided better market language than acquisition potential.
There were no strong, current, ICP-aligned SearchTriage opportunities in those first two searches, so Hermes recommended stopping that specific line of paid query-tuning for now. SocialCrawl finished with 58 credits untouched. I count that as a successful experiment because two credits were enough to show that this particular SearchTriage Reddit query set had not yet earned more spend. That is very different from saying Reddit is a dead channel: tomorrow may surface better SearchTriage intent, and the same source may be much more useful for Automation Receipts, TextToDeck or Old Stack Journal.
Machine prioritisation should not become machine censorship
The Reddit experiment exposed another requirement that later shaped the interface. Hermes did not think the weak-but-interesting Reddit candidate deserved to become my recommended next action, and I agreed with that judgement, but I still wanted to be able to inspect it.
That led to four opportunity states in GrowthOS: ACTION for things strong enough to recommend, REVIEW for plausible things I may still want to inspect, MARKET SIGNAL for useful evidence or language that is not a current opportunity, and REJECT for stale or bad-fit items. The states let the system distinguish between what it recommends and what it merely preserves for human review.
That distinction matters because “What should I do next?” and “What else might I want to inspect?” are different questions. Collapsing them into one ranked AI list would hide useful information simply because it did not score highly enough to become a recommendation.
The “don’t build a dashboard” rule had done its job
Up to this point, GrowthOS had an explicit architectural bias against building a custom UI too early. By the afternoon, that restriction had served its purpose because I already knew I wanted to see next actions, portfolio metrics, X growth, monetisation progress, opportunities, experiments, todos, Kanban state, jobs, logs and graphs.
More importantly, those requirements had come from real workflows rather than speculative product design. I also knew I was not going to operate GrowthOS permanently by SSHing into a server and remembering which Python scripts to run, so a browser interface had become an operational requirement rather than a cosmetic extra.
I built the first dashboard with Python’s standard library, SQLite, HTML, CSS and vanilla JavaScript. There is nothing wrong with React, Node or npm, but this particular internal tool did not need them, and keeping the stack small made it easier to understand and deploy on the same old home server as the rest of the experiment.
I also kept a useful split between durable knowledge and operational state. The Control Room files remain the source for product knowledge, evidence, policies and rebuildable reports, while SQLite stores things such as todos, opportunity states, outcomes, experiments, settings and job history.
That division gives me a system where the important knowledge can survive a database rebuild while the UI still has somewhere sensible to keep mutable state. The files explain the system, and the database helps operate it.
Dashboard v0.1 immediately proved why I wanted one
The first version showed a next action, an X-growth graph, portfolio cards, opportunity review, jobs, a simple Kanban board and buttons for running existing data jobs. That was enough to change how GrowthOS felt because I could look at one browser page and understand the current state instead of remembering which script generated which report.
The first browser test still produced a confusing 404 even though diagnostics showed both /health and / returning HTTP 200 on the server. The application itself was fine, and the problem turned out to be the SSH tunnel and local port path rather than the GrowthOS code.
Once I corrected the tunnel mapping, the dashboard appeared and the incident became another small reminder that not every failure in an AI-heavy project is an AI problem. Sometimes it is still just networking, ports and the wrong assumption about which layer is broken.
Then the dashboard lied about Umami
Version 0.2 added X monetisation progress, experiments, editable opportunities, Hermes analysis buttons and job history. It also exposed a much better bug because the dashboard was scraping metrics back out of the human-readable Umami Markdown report.
That parsing produced the wrong SearchTriage number and failed to display Old Stack Journal correctly. The structured source data actually showed SearchTriage at 318, Old Stack Journal at 526, Automation Receipts at 159 and TextToDeck at 97.
The dashboard had no business reverse-engineering those values from prose when the original JSON already existed. The rule that came out of that bug is simple: Markdown is a good output format for humans and models, but applications should consume the structured source data whenever it exists.
Codex became the implementation tool
By this point I was also running into another practical limit because generating large Python files through chat and pasting them into PowerShell was becoming a bad software-development workflow. The roles therefore became clearer, with ChatGPT/Sol handling architecture, commercial reasoning and review, Hermes/Terra handling runtime GrowthOS judgement, and Codex handling mechanical repository implementation and refactoring.
I installed Codex CLI on the home server as a user-level tool, authenticated it, and ran it directly inside the current GrowthOS repository rather than mixing it with the older pre-Hermes project. Codex then built dashboard v0.3 as a separate version, splitting the original monolithic application into modules, templates and static assets while leaving the older versions untouched as rollback points.
It also fixed the analytics bug properly by reading the structured Umami JSON directly instead of scraping the Markdown report. Existing SQLite state was preserved, including the REVIEW opportunity, todos, experiment records, notes and job history, and the main routes, assets, CSRF behaviour and localhost-only binding were tested.
This is a division of labour I am increasingly comfortable with because I do not particularly care which model gets credit for writing a Python module. I care whether the architecture is understandable, the commercial assumptions are sensible, the code can be reviewed, and the resulting tool is useful.
GrowthOS finally became a persistent application
Until this point, the dashboard still had to be started manually, which no longer made much sense once the browser interface had proved useful. With explicit approval, I created a systemd service for dashboard v0.3 so it would start on boot, restart on failure and remain bound only to localhost.
The first health check failed because systemd had reported the service as started before the Python application had finished binding its socket. A few seconds later it was healthy, which gave me another straightforward operational lesson: a service manager saying a process has started is not proof that the application is ready.
I checked the actual process, restart count, journal, listening socket and /health endpoint rather than treating one green systemctl line as the whole test. The final state was what I wanted, with the service running, zero restarts, localhost-only binding and a healthy application endpoint.
Tailscale removed the last annoying part
A persistent dashboard is not very useful if I can only reach it while sitting on the same network or manually constructing SSH tunnels. I joined the server to my private Tailscale network and used Tailscale Serve rather than Funnel so the GrowthOS application could remain private while still being reachable over HTTPS from my own authenticated devices.
GrowthOS itself still listens only on localhost, so there is no public dashboard port sitting open on the internet. Tailscale provides the private route from my devices to the local application without changing that underlying binding.
I connected a client, opened the private GrowthOS address, confirmed it worked and bookmarked it. The operating workflow went from SSHing into the server, starting Python, opening another terminal, creating a tunnel and then opening the browser to simply opening the GrowthOS bookmark.
That is probably the least impressive architecture change in the whole series if you look only at the diagram. In practice, it is one of the most important because it turns the dashboard from something I can technically run into something I will actually use.
What is actually persistent now?
Despite adding systemd and a web interface, GrowthOS has not turned into an autonomous marketing machine. The persistent pieces are restrained: the dashboard runs under systemd, Tailscale runs on the server, and Tailscale Serve provides the private route to the browser interface.
Hermes is not continuously running, Codex is not continuously running, and SocialCrawl is not continuously searching Reddit. No marketing model is sitting there burning tokens all day looking for something to do, because Hermes starts only when a GrowthOS job needs model judgement, Codex runs when I deliberately have software-development work for it, and SocialCrawl is dormant with 58 credits remaining.
There are also still no GrowthOS cron jobs. The dashboard is persistent, but the external data is not automatically fresh, so opening GrowthOS reads the latest saved snapshots and I still need to trigger the Umami and Typefully refresh jobs when I want new data.
That gives me a very obvious next automation step, and it is not autonomous outreach or another agent. The sensible next move is to schedule the cheap deterministic data-refresh jobs so the evidence is already current when I open GrowthOS.
What I still have not built
Part 3 crossed a few boundaries I had deliberately protected in the earlier build, but most of the original restraint remains. I still do not have an orchestrator, autonomous outreach, automatic DMs, automatic X publishing, a public GrowthOS endpoint, a giant analytics warehouse, automated SocialCrawl lead harvesting, elaborate t.co attribution, a React frontend or recurring paid-model schedules.
The difference is that I now have somewhere for useful future workflows to live once the evidence says they deserve to exist. Earlier, “do not build it” meant I did not yet know whether the workflow was useful, whereas the browser control room now gives me a place to add a workflow after it has earned its way into the system.
The biggest remaining gap is not another agent
The original question behind GrowthOS was whether I could turn Hermes into a persistent marketing agent system around the products I am already building. After this session, I think the word “agent” is becoming less important than the sequence of evidence, judgement, correction and action around it.
The useful progression has been from raw analytics to deterministic compression, then model judgement, human correction, a small external-intent experiment, a deliberate stop decision, a commercial hypothesis, an interface requirement and finally a private browser control room. That progression matters more than whether the final system looks especially agentic.
The interesting behaviour is not that an AI can autonomously “do marketing.” It is that GrowthOS is beginning to help answer what evidence I have, what deserves my time, what I should ignore, and what happened after I acted.
The weakest part is now that last question because GrowthOS still needs better commercial outcome data. Email activity where useful, product signups and users, payments, better conversion events and Search Console data all matter more to me now than adding another agent to the stack.
After that, I want the cheap deterministic first-party collection jobs scheduled so the system is fresh before I open it after work. I started the day trying to give Hermes more marketing data and ended it with a persistent, privately accessible marketing control room running on an old home server, but the useful part is not the dashboard itself; it is that I finally had enough real workflows to justify building it.