Anonymous telemetry
What every node reports back, how to switch it off.
Every MeshHold node sends a small, anonymous "beat" to
https://meshhold.com/api/telemetry/beat once a day. The beat tells
us how many networks are alive, where they run, and which features
operators actually use — so we know what to keep building and what
to retire. We collect as little as possible, and you can switch it
off with one config key.
TL;DR — what is sent, what is not
Sent (every 24 h):
schema_version— integer, lets us evolve the format.network_hash— HMAC-SHA256 of your swarm key. Same swarm → same hash. Cannot be reversed to recover the key.telemetry_id— HMAC-SHA256 of swarm key + your local node ID. Stable across restarts, lets us count distinct nodes without learning your libp2p peer ID.country_code— ISO-3166 alpha-2 (e.g.RS,DE,US). Your node learns it by calling our/api/ipinfoendpoint at startup: that request necessarily exposes your IP to the server in transit (it has to, to geolocate you), but the server returns only the country code and does not log or store the IP. By the time the country rides the telemetry beat it is just a two-letter code — the full IP is never stored server-side.version,os,arch— MeshHold version + GOOS/GOARCH.uptime_delta_s— seconds since the previous beat the node was running.counts— feature-usage tallies (see below).
Never stored:
- Your IP address. The beat itself carries no IP field, and the
ingest endpoint neither logs nor stores the request IP (see
below). Your IP is
briefly visible to the server during the
/api/ipinfocountry lookup, as it must be for geolocation, but it is dropped right after — never written to a log or a database row. - Your libp2p peer ID, your swarm key, or any management key.
- Vault names, chat-room names, peer names, file names, message bodies, call participants, contact lists, push tokens, or any content of any kind.
- Anything that could identify a specific person or vault.
Scope. This page describes telemetry only. A few other, user-initiated flows do record an IP for abuse-control — most notably the bug-report upload, whose IP is kept with the report so the operator can rate-limit and triage. Those are separate from the anonymous beat and are never derived from it.
Usage counters
counts is a flat JSON object. All entries default to 0 and are
deltas since the previous successful beat — your node forgets
them after each upload, so we never see cumulative totals.
| Counter | What it counts |
|---|---|
trusted_vaults |
Number of trusted vaults (current count, not a delta). |
trusted_chats |
Number of trusted chat rooms (current count, not a delta). |
tunnels_opened_browser |
HTTP CONNECT proxy circuits opened (browser-through-tunnel). |
tunnels_opened_vpn |
Wintun / system-VPN circuits opened. |
calls_initiated |
Outbound calls placed. |
calls_accepted |
Inbound calls answered. |
protocols_active |
List of transports currently listening: plain, ssh, https. |
has_claude |
true if at least one Claude / agent instance is provisioned. |
has_agent_providers |
List of provider names configured: claude, opencode. |
The two trusted_* fields are absolute counts, not deltas, because
that is what "how many vaults does an average node hold" actually
needs.
How to opt out
Add this to your config.yaml:
telemetry:
enabled: false
Or run:
meshhold telemetry disable
Either path takes effect on the next daemon restart. To re-enable:
meshhold telemetry enable
To check the current state:
meshhold telemetry status
See reference-configuration → telemetry
for the full set of keys (endpoint override, interval, etc.) — useful
if you run a private fork that should not report to meshhold.com.
Why we collect this
- Networks, not nodes, drives the public counter on
/stats/. It tells the next person evaluating MeshHold whether the project is alive. - Country distribution tells us where MeshHold is being adopted, so we can prioritise translations and region-specific guides.
- Feature usage tells us whether the calling stack, the S3 endpoint, the VPN — anything we spent weeks building — is actually used by someone other than the author. Features that nobody uses get archived rather than maintained forever.
Endpoint, transport, frequency
- Endpoint:
https://meshhold.com/api/telemetry/beat. - Transport: HTTPS, TLS 1.2+.
- Frequency: every 24 h ± 10 % jitter, plus one beat on startup after the configured network grace period.
- Failure: silent. If the beat fails, your node logs a debug message and tries again on the next interval. Telemetry never blocks any other daemon work.
What we do with the data on the server
Raw beats are stored for 30 days, then deleted. Before deletion
they are folded into per-day aggregates (DailyStat) keyed by
date + country: (date, country, active_nodes, total_vaults,
total_calls, …). The aggregates are what feeds the public
/stats/ page and the admin
dashboard.
The server-side ingest never reads the Remote-Addr /
X-Forwarded-For headers, so no IP is written to the row. The
front-end web server is additionally configured to keep the beat
route out of its access logs, so the IP isn't recorded there
either. We never persist which IP sent the beat; only the
country_code carried in the beat body survives the round trip.
Schema example
A real beat looks roughly like this:
{
"schema_version": 1,
"telemetry_id": "9c9a8f2bb…",
"network_hash": "1f6d4c3aa…",
"country_code": "RS",
"version": "0.7.63",
"os": "linux",
"arch": "amd64",
"uptime_delta_s": 86412,
"counts": {
"trusted_vaults": 3,
"trusted_chats": 1,
"tunnels_opened_browser": 4,
"tunnels_opened_vpn": 0,
"calls_initiated": 2,
"calls_accepted": 1,
"protocols_active": ["plain", "https"],
"has_claude": true,
"has_agent_providers": ["claude"]
}
}
That is the entire payload. There are no other fields anywhere in
the code path — if you tcpdump -A port 443 on a node and decrypt
the TLS, you will see exactly the document above and nothing else.