Gates That Only Tighten
I built warchest to answer a question I kept running into when I talked about applying AI to real decisions: how do you let a model advise on something with financial consequences without ever letting it pull the trigger. Warchest is a multi-asset paper-trading platform. Three engines run today: crypto, forex, and equities. A fourth, prediction markets, ran for months and I killed it in July once recon showed the sample frame was dead. In each engine an LLM proposes a trade and a deterministic Python risk kernel decides whether anything happens. The model gets one schema-constrained call per decision; every gate that can veto it runs after that call, in code I can read start to finish. coinbot9000, the crypto engine, has run unattended on my home NUC since May 9, 2026 - as a hold-only shell for the first month, with the LLM-proposed, kernel-gated decision path live since June 12 - ticking every 30 seconds across five Coinbase pairs. fx joined it on July 12. Stonks cuts over around August 1 and still runs on my desktop until then, deliberately, because I don’t move a live engine before its rehearsal is proven.
The architecture decision that mattered most wasn’t the LLM integration. It was making the gate chain provably one-directional. Every gate in the kernel’s risk module can only convert an “allow” into a “blocked,” never the reverse, and nothing downstream can manufacture an allow that didn’t earn it through every prior gate. I didn’t take that on faith. A hypothesis-driven property test suite generates thousands of randomized decision responses, gate outcomes, and market regimes and asserts the monotonicity holds across all of them: filtering the model’s claimed channel alignment against what the packet actually carried can only shrink an allow into a blocked, an exposure cap can only shrink further, a regime gate outside a trending market can only shrink further. If a gate I add later breaks that property, the tests fail before it ships, not after a bad trade.
That discipline traces back to a real bug. The predecessor bot this platform absorbed once ran 87 minutes on stale ETH price data, because the feed connection stayed healthy for every other pair while that one symbol’s stream silently died underneath it. A stale price is unusable for both exits and new entries, and nothing caught it live. The kernel’s scheduler now checks feed staleness per symbol on every tick, independent of the socket’s own health, and skips any symbol whose last price is past threshold instead of trading on it. That’s the shape of every gate in the system now: something broke in production once, and the fix became a rule that can only make the system more conservative, never less.
The clearest proof that fail-closed works in practice came from a quota wall, not a market move. On July 1 I piloted a new decision provider on the fx engine: verdicts from Claude through my own subscription CLI instead of local Ollama, a 21:00-23:00 ET window at sonnet, low effort. Every one of the 24 ticks that window got a 429. The interactive coding session I’d run all evening had already burned through my plan’s rolling five-hour window before the pilot’s first call went out. Zero verdicts came back. The engine didn’t crash and didn’t trade blind: all 24 calls degraded to hold with risk_flag=llm_unreachable, fully written to the decision audit log, and the fx book sat flat the whole window. I re-armed the same pilot for 01:00-03:00 with no interactive session competing for quota, and it went 24 for 24 real verdicts at $0.43 nominal cost. Same code path, same gates, two different outcomes depending entirely on whether the model showed up. The kernel doesn’t distinguish a 429 from a timeout, a malformed response, or Ollama being asleep on the other machine - all of it collapses to the same hold state, and none of it is silent. Every decision row carries a per-rule gate trace, which matters because nearly every tick resolves to hold - the design target was 95%, and the measured rate in the real audit corpus is about 99.8% - so the operating question is never “did it trade,” it’s “why didn’t it,” and that’s a one-row read instead of a log dive.
None of this gets graded on vibes. Every engine declares its benchmark before it’s allowed to trade live: coinbot9000 against an equal-weight five-pair HODL, stonks against SPY buy-and-hold, and I pre-register the kill criteria before I have outcome data that could bias them. The stonks engine’s spec, written at day 10 of its benchmark window, locks in that the real edge verdict isn’t read until 30 closed trades exist, even though that runs past the nominal four-week checkpoint on July 31, because a top-five-of-thirty momentum book turns over too slowly for four weeks to mean anything. At day 10 the book was down 0.43% against an anchor while SPY was up 1.11%, and that gap is a dashboard line, not a verdict, in either direction. A separate kill line trips regardless of thesis if the book ever falls more than 15% from anchor while SPY hasn’t moved 5%. “Never loses money” isn’t a goal here. I’ve written into the docs that it’s a category error, on purpose. The goal is a system where the worst thing an unreliable model can do, on its worst day, is cost a rate limit and nothing else. That’s the actual deliverable: not a model that’s always right, but a kernel that makes being wrong survivable.