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 a six-character 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 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. The cloud CLI group is part of the Phase 2 cloud-sync arc. Upgrade with 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, ambiguity-safe (no 0/O, no I/l), 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. 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.
  • --workspace <slug> — pre-select a workspace when you have more than one. Skips the disambiguation step in the browser.
  • --no-browser — print the URL but do not open the browser. Useful on headless machines or over SSH.

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).
  • PAUSED — sync is intentionally paused via halton-meter cloud pause or automatically after a 401. Re-enable with cloud resume or cloud connect.
  • 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 POST /v1/pairing/revoke 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. The token was revoked. Re-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.

See also