Halton Meter is a local HTTPS proxy. Your LLM clients send their requests to a loopback port; Halton Meter terminates the TLS connection with a self-generated CA, parses the request and response, writes one SQLite row per call, and forwards the original request unchanged to the provider. No SDK changes. No code wrapper. The wire format the provider sees is identical to the one your client sent.
The architecture splits into two long-running processes, deliberately:
- The edge (
com.haltonlabs.meter.edge) owns the user-facing port8081. It is always on, has minimal dependencies, and stays bound across every daemon restart, crash, and upgrade. - The daemon (
com.haltonlabs.meter) owns the actual mitmproxy listener on internal port8090plus the FastAPI HTTP API on8765. This is the heavyweight process that does TLS interception, adapter parsing, cost computation, and SQLite writes.
This split is what makes the proxy safe to run on a developer’s machine. A daemon crash never breaks your tools; the edge falls through to a raw TCP tunnel. Read Fail-open behaviour for the full guarantee.
Three TCP listeners
┌─────────────────────────────────────────────────────────────┐
│ edge 127.0.0.1:8081 ← apps connect here │
│ daemon mitm 127.0.0.1:8090 ← edge chains here │
│ daemon api 127.0.0.1:8765 ← dashboards / curl read │
└─────────────────────────────────────────────────────────────┘
All three bind on 127.0.0.1. Nothing listens on a public interface.
The chosen tuple at last start is persisted to
~/.halton-meter/effective-ports.json so every component (edge,
watchdog, status, doctor) reads the same source of truth — important
when port discovery picks a fallback.
8080 is not used. The legacy single-process default before v0.1.6 was
127.0.0.1:8080; current defaults are 8081/8090/8765.
A request, end to end
- Your tool inherits
HTTPS_PROXY=http://127.0.0.1:8081(the edge) at spawn time — either throughhalton-meter run, your shell rc, orlaunchctl setenv. - The tool issues
CONNECT api.anthropic.com:443to the edge. - The edge consults a cached
/healthprobe of the daemon (asymmetric TTL: 1000ms when healthy, 200ms when unhealthy, invalidated immediately onConnectionRefused). - Daemon healthy → the edge chains through to
127.0.0.1:8090. mitmproxy terminates TLS, the matching adapter parses the request and response, the policy engine evaluates rules, and one row lands in~/.halton-meter/db.sqlite. - Daemon unhealthy → the edge opens a raw TCP tunnel directly to
api.anthropic.com:443, replies200 Connection Established, and shuttles bytes both ways without decryption. No metering, no blockage.
The edge never decrypts in passthrough mode. It is, by design, a dumb shuttle.
How traffic gets to the edge
halton-meter init chooses one of three routing strategies — none of
which use pf packet filter rules. Routing is entirely env-var and
networksetup:
| Mode | What’s set | Catches |
|---|---|---|
| env-only (default) | halton-meter run <cmd> only | The command you wrap, nothing else |
--apps | Shell rc + launchctl setenv user domain | New terminals, Spotlight/Dock-launched IDEs |
--full | All of the above + networksetup -setsecurewebproxy per active interface | Browsers and any GUI app that ignores HTTPS_PROXY |
Per-service bypass domains are preserved in --full; the pre-init
state is captured into ~/.halton-meter/system_state.json so
halton-meter uninstall can restore it exactly.
Why two processes
A single-process design — daemon listens directly on 8081 — was the
shape until v0.1.6. It had two failure modes that a developer-facing
tool cannot afford:
- Daemon crash breaks every tool. Anything with the proxy baked into its environ — a long-running Claude Code session, a Cursor window — would fail every subsequent request until the daemon came back. The fix-by-restart workflow contradicts the cardinal “zero workflow disruption” rule.
- Daemon upgrade breaks every tool. Same shape, different cause.
pipx upgrademomentarily releases port8081and anyCONNECTthat arrives in that window fails.
Splitting the user-facing port into a separate, minimal KeepAlive=True
edge process makes both failure modes invisible to your tools. The edge
is the architectural answer to fail-open.
Inspecting state
$ halton-meter status # overview
$ lsof -nP -iTCP:8081 -sTCP:LISTEN # edge
$ lsof -nP -iTCP:8090 -sTCP:LISTEN # daemon mitmproxy
$ lsof -nP -iTCP:8765 -sTCP:LISTEN # FastAPI If a row is missing, halton-meter doctor is the next stop — it walks
every layer (cert, certifi, edge, daemon, watchdog, system proxy) and
prints a copy-pasteable next-action per failure.
What’s next
- Fail-open behaviour — the full set of invariants that keep your tools running when the daemon is not
- Project tagging — how each intercepted request gets attributed to a project
- SQLite schema — what actually lands on disk for every captured request