Halton Meter Cloud

Connect your meter

Pair a local daemon to a Cloud workspace with one CLI command and a scoped token. Off by default, reversible, no data flows until you approve in the browser.

macOS 12+ · Python 3.11+ Reading time 2 min Updated May 11, 2026

One command, one token, reversible.

Pairing connects a single machine’s halton-meter daemon to a Cloud workspace. The pairing flow uses an eight-character Crockford-base32 one-time code that the daemon prints; you approve it in the browser, the backend mints a single-shot hm_sync_… token, and the daemon writes it to a 0600 file. Until you run cloud connect and approve in the browser, nothing flows. The daemon ships with cloud.enabled = false.

Prerequisites

Before you pair, you need:

  1. A running daemon. Verify with halton-meter status — the daemon row should be green and mitm bound on 127.0.0.1:8090. If it is not, see Install Halton Meter.
  2. A Halton Meter Cloud account. Sign up at cloud.haltonmeter.com; the dashboard itself lives at app.haltonmeter.com.
  3. A workspace. Created automatically on first sign-up. Switch workspaces from the avatar menu in the dashboard if you have more than one.
  4. Daemon v0.2.0 or later for terminal-driven pairing; v0.2.6+ if you’d rather pair from the browser without copy/paste (the dashboard detects a running local daemon via the loopback /v1/cloud/state route, then triggers POST /v1/cloud/connect for you). Upgrade with uv tool upgrade halton-meter, uv cache clean halton-meter then uvx halton-meter --version, or pipx upgrade halton-meter.

halton-meter cloud connect

Run the connect command on the machine that should be paired:

~ — pair the daemon
$ halton-meter cloud connect

# Output
 Requesting pairing code from api.haltonmeter.com…
 Pairing code: WXJ4-9KLM
 Approve at: https://app.haltonmeter.com/connect?code=WXJ4-9KLM
 Waiting for approval… (Ctrl-C to cancel)

The daemon opens a long-poll against POST /v1/pairing/poll while you approve in the browser. The pairing code is 8 characters of Crockford-base32 (no 0/O, no I/L/l/1 ambiguity) and expires after 10 minutes.

When you approve at app.haltonmeter.com/connect?code=WXJ4-9KLM, the backend mints a hm_sync_… token scoped to one workspace and one machine, returns it on the poll, and the daemon writes:

PathContents
~/.halton-meter/cloud-credentials.jsonRaw hm_sync_… token, chmod 0600, owner-readable only
~/.halton-meter/cloud.keyFernet symmetric key for the SQLite-mirror encrypted column
~/.halton-meter/config.toml [cloud] blockenabled = true, base_url = "https://api.haltonmeter.com", workspace_id = "..."
~/.halton-meter/db.sqlite cloud_state rowEncrypted api_key_ciphertext, workspace name, machine id, hostname snapshot

The cloud worker spawns inside the daemon process on the next supervisor tick (≤ 15s) and starts batching unsynced rows.

Common flags

  • --base-url <url> — override the cloud endpoint. Default is https://api.haltonmeter.com. Also overridable via the HALTON_METER_CLOUD_URL env var. Use this only for self-hosted or development backends. As of v0.2.1 this flag persists to config.toml; earlier versions required a manual edit.
  • --poll-interval-seconds <float> — how often the daemon polls for approval. Default 2.0.
  • --timeout-seconds <float> — hard cap on the pairing wall-clock time. Default 600.0 (matches the cloud-side 10-minute code TTL).

After approval, three commands confirm the link is healthy:

~ — verify pairing
$ halton-meter cloud whoami
 Workspace:  acme-co
 Machine id: dev-mbp-vk
 Hostname:   vk.local

$ halton-meter cloud status
 State:           ACTIVE
 Last sync:       3s ago
 Unsynced rows:   0
 Last reconcile:  never

$ halton-meter cloud sync
 POST /v1/requests/batch  →  200 OK  (1247 rows accepted)

cloud status reports one of four states:

  • ACTIVE — sync is running, last sync recent, no errors.

  • DEGRADED — sync is running but rows are accumulating faster than they upload (transport latency, large body queue). Transient — sync catches up on its own.

  • PAUSED — sync is paused for one of three reasons. As of v0.2.8 the classification is strict (only a real 401 sets paused_unauthorised):

    paused_reasonCauseRecover with
    paused_manualYou ran halton-meter cloud pause.halton-meter cloud resume.
    paused_unauthorisedHTTP 401 on /v1/requests/batch. Token is invalid.cloud resume (whoami-probes the existing token); if that confirms the token is genuinely revoked, cloud connect to re-pair.
    paused_forbiddenHTTP 403 — workspace owner removed this machine.Workspace owner re-invites the machine, then cloud resume. Do not cloud connect — that revokes the still-valid key.
  • NOT-CONFIGUREDcloud.enabled = false. Nothing flowing.

Add --json for the machine-readable shape (useful in scripts).

Cross-check against the cloud side: open app.haltonmeter.com/[workspace]/settings/devices — the machine should appear with the hostname and the timestamp of its last sync.

Pairing a second machine

Each developer’s laptop pairs once. Repeat the same flow on every machine. Cloud joins them into one workspace; the dashboard shows one view of every paired machine’s spend.

There is no “machine fingerprint” — the daemon picks a stable machine_id from ~/.halton-meter/machine.json (random UUID, generated on first init). If you re-image a laptop and want it to look like the same machine in the dashboard, copy machine.json over before pairing; otherwise it appears as a new device.

Disconnecting

~ — unpair
$ halton-meter cloud disconnect
 Revoking token at api.haltonmeter.com…
 Token revoked.
 Local: cloud.enabled = false; credentials wiped.

Disconnect is symmetric to connect:

  1. The daemon calls DELETE /v1/daemon/disconnect so the token is unusable immediately, even if the local files survive (stolen laptop case).
  2. ~/.halton-meter/cloud-credentials.json and ~/.halton-meter/cloud.key are removed.
  3. The [cloud] block in config.toml has enabled flipped to false.
  4. The cloud_state row in SQLite is cleared.

After cloud disconnect, the daemon keeps capturing locally exactly as before. halton-meter report keeps working. To re-pair, run cloud connect again — the machine appears as a new device unless you preserved machine.json.

To remove a machine from the workspace remotely (laptop lost, employee left), use the workspace settings page at app.haltonmeter.com/[workspace]/settings/devices. Revoking from the dashboard immediately invalidates the token; the daemon’s next sync attempt returns 401 and the worker transitions to PAUSED with paused_reason="paused_unauthorised".

Troubleshooting

halton-meter cloud connect reports daemon not running. Run halton-meter status and halton-meter start. Pairing requires the mitm port to be bound because the supervisor that runs the cloud worker is the same process.

Browser opens but /connect?code=... says “code not found”. The code expired (10-minute window). Cancel the CLI with Ctrl-C and re-run halton-meter cloud connect for a fresh code.

Pairing succeeds but cloud status shows PAUSED with paused_unauthorised. As of v0.2.8 this means a real 401. Run halton-meter cloud resume first — it whoami-probes the existing token and clears the pause if the token is still valid. If resume confirms the key is genuinely revoked, then run cloud connect.

cloud sync returns 422 with Field required. Upgrade the daemon to v0.2.1 or later — the from/tofrom_date/to_date query parameter rename was fixed there.

cloud.transport_error on every sync. If the message surfaces a placeholder TLD (.test, .example, .invalid, .local), v0.2.7 auto-heals the value on next boot. halton-meter stop && halton-meter start. For older daemons, edit ~/.halton-meter/config.toml and set base_url = "https://api.haltonmeter.com".

See also