This is the sequel to auditing 7 bots and deciding to kill 5. This time, actually killing them — except killing a bot turned out to be more than stopping a process.
First, a question. Have you ever agonized for days over whether to "kill or keep" a failed side project? Sunk cost, reluctance, "maybe it bounces if I wait a bit more" — that debate itself is a time thief. Let me first tell you how to eliminate the debate.
The decision to kill was already made
The most expensive thing about retiring a bot is the emotional argument. "It's 3%p below, but a little longer?" "17% win rate — couldn't it be the regime?" Do this ad hoc and you'll never kill anything.
So I wrote the gate before the data arrived. The turn-of-month bot's pass condition was "closed n≥30 AND excess return > 0," and when it hit n=30 the excess was −0.17%p. Nothing to argue. The gate stamped FAIL, so it dies. No room for sunk cost or "a bit more." The real value of pre-registration isn't only blocking self-deception — it's leaving no regret when you kill.
What to keep and kill was clear too. Kill: turn-of-month (gate FAIL), upbit swing (negative edge), bithumb SMA50 (post-hoc + zero trades). Keep: low-PBR value, insider-buying (both documented anomalies) + the LLM-as-judge bot (for the early-exit observation).
But a bot doesn't only die as a process
This is where the real work starts. "Kill the bot" makes you picture killing a daemon, but one bot leaves several layers of traces. Miss one and you get a zombie.
1) launchd — you must do both steps. On macOS, killing just the process lets KeepAlive resurrect it instantly, and even if it doesn't, the plist reloads on the next login. To kill it fully, both:
launchctl bootout gui/$(id -u)/com.user.<label>
rm ~/Library/LaunchAgents/com.user.<label>.plistMy 3 bots each had a daily job plus a dashboard job, so I booted out 5 labels total (one bot had no dashboard). After booting out, you're not done until launchctl list | grep shows zero remnants and the listening port is dead. These launchd gotchas I've written up before — killing a process and removing a job are different layers.
2) Dashboards — dead links remain everywhere. Kill a bot and every UI that referenced it becomes a broken link. In my case:
- The aggregate dashboard (hub) had one bot with its own dedicated route, view, and template → remove the import, route, files, and index nav. Confirm
/bithumbswing→ 404. - The other two bots had standalone dashboards, but the shared nav of the surviving sibling dashboards had the dead bots' URLs hardcoded. Killing a bot means not just looking at its own folder, but finding and fixing everyone who linked to it. A full scan with
grep -rn ':<port>|/<bot>'is mandatory.
One more gotcha: Flask caches templates. Editing the file alone won't show; you must restart that dashboard's launchd job with launchctl kickstart -k. After restarting, I confirmed: ports that should live return 200, killed routes return 404, deleted links grep to 0.
Would you delete the folder, or keep it?
This is the fork. A bot repo holds the ledger (every trade record) — that's the audit evidence. A plain rm -rf reclaims 250MB but erases the basis for "wait, why did I kill that bot again." Leaving it keeps the basis but piles dead code on disk.
I chose archive-then-delete:
tar -czf decommissioned-2026-08-06/<bot>.tar.gz <bot>/
tar -tzf ...tar.gz | wc -l # integrity: count entries
rm -rf <bot>/The key is the middle line — before rm, verify the archive actually opens with tar -tzf. Delete the original while the archive is corrupt and that's a deletion, not an archive. To reverse, unpack the tar and restore the plist.
Self-check — 3 spots easy to miss when killing a bot/service
- The regeneration path. Did you kill only the process, or also whatever resurrects it (KeepAlive, plist, cron, supervisor)?
- Inbound references. Did you grep out everyone who linked to or called the service? Any dead links or dead-endpoint calls left?
- Reversibility. Before deleting, did you secure a recovery path (a verified archive), or did you just delete?
The honest part
Even after killing, things live on. Cleaning up commits, I saw that another session's uncommitted changes were mixed into the same repo — so instead of git add . I staged only the paths I'd touched. Dragging someone's unfinished work into your kill commit is its own kind of mess.
And honestly: some things aren't cleaned yet. One bot that FAILED its gate but got kept after redefining its purpose as "drawdown-reducing," a few permission entries left in a config file. Not a perfect sweep. But the 3 I killed are gone across all three layers — process, dashboard, disk — there's an archive to reverse it, and why they died is in the gate log.
What killing a bot needed most wasn't resolve. It was a rule that decided kill-or-keep in advance, and a checklist of where the traces hide. Pre-registration made the decision; grep did the rest.
Now picture one of your own dead side projects. The process stopped — but the page that linked it, the cron that revives it, the repo on disk: are they still alive?