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:
- A running daemon. Verify with
halton-meter status— the daemon row should be green andmitm boundon127.0.0.1:8090. If it is not, see Install Halton Meter. - A Halton Meter Cloud account. Sign up at
cloud.haltonmeter.com; the dashboard itself lives atapp.haltonmeter.com. - A workspace. Created automatically on first sign-up. Switch workspaces from the avatar menu in the dashboard if you have more than one.
- 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/stateroute, then triggersPOST /v1/cloud/connectfor you). Upgrade withuv tool upgrade halton-meter,uv cache clean halton-meterthenuvx halton-meter --version, orpipx upgrade halton-meter.
halton-meter cloud connect
Run the connect command on the machine that should be paired:
$ 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:
| Path | Contents |
|---|---|
~/.halton-meter/cloud-credentials.json | Raw hm_sync_… token, chmod 0600, owner-readable only |
~/.halton-meter/cloud.key | Fernet symmetric key for the SQLite-mirror encrypted column |
~/.halton-meter/config.toml [cloud] block | enabled = true, base_url = "https://api.haltonmeter.com", workspace_id = "..." |
~/.halton-meter/db.sqlite cloud_state row | Encrypted 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 ishttps://api.haltonmeter.com. Also overridable via theHALTON_METER_CLOUD_URLenv var. Use this only for self-hosted or development backends. As of v0.2.1 this flag persists toconfig.toml; earlier versions required a manual edit.--poll-interval-seconds <float>— how often the daemon polls for approval. Default2.0.--timeout-seconds <float>— hard cap on the pairing wall-clock time. Default600.0(matches the cloud-side 10-minute code TTL).
Verifying the link
After approval, three commands confirm the link is healthy:
$ 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_reasonCause Recover 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 connectto re-pair.paused_forbiddenHTTP 403 — workspace owner removed this machine. Workspace owner re-invites the machine, then cloud resume. Do notcloud connect— that revokes the still-valid key. -
NOT-CONFIGURED —
cloud.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
$ halton-meter cloud disconnect
● Revoking token at api.haltonmeter.com…
● Token revoked.
● Local: cloud.enabled = false; credentials wiped. Disconnect is symmetric to connect:
- The daemon calls
DELETE /v1/daemon/disconnectso the token is unusable immediately, even if the local files survive (stolen laptop case). ~/.halton-meter/cloud-credentials.jsonand~/.halton-meter/cloud.keyare removed.- The
[cloud]block inconfig.tomlhasenabledflipped tofalse. - The
cloud_staterow 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/to → from_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
- Cloud overview — what the hosted cloud adds
- Workspaces — team workspaces, invitations, roles
- Sync and retention — what syncs, retention, opt-in body sync
- Cloud security — token storage, auth, encryption
halton-meter status— local daemon health- Install Halton Meter — required before pairing