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). Derived from your public IP by your own node, never stored as a full IP 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 sent:
- Your IP address (the request reaches us over TLS, but is not written to access logs or the database for telemetry beats).
- 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.
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 decides where we put public relays next.
- 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 deliberately drops the Remote-Addr /
X-Forwarded-For headers before writing the row. We never learn
which IP sent the beat; only the country_code that your own node
filled in 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.