Network · Satellite analytics

The hourly analytics reporting contract — how every network app POSTs its traffic rollup to 2plot.ai so /traffic shows the whole network.

Network · Satellite analytics

The hourly analytics reporting contract — how every network app POSTs its traffic rollup to 2plot.ai so /traffic shows the whole network.

---

.. toc::

2plot.ai is the analytics home

Every application in the network reports its own traffic to the hub, so the owner-only /traffic dashboard answers, per app and for the network as a whole: how many people visited each day, which pages they read, how long sessions ran, and roughly where visitors came from.

Two signals feed the picture:

1. Health — the hub's pulse sweeps every satellite's /healthz once an hour and records up/down + latency (the "Satellite health & reach" panel on /traffic, and /admin/heartbeat). 2. Traffic — each satellite POSTs a signed rollup to the hub once an hour (or at minimum once a day). Report today's numbers-so-far each time: the hub folds all of a day's reports together, taking the growing maximum — and when it sees the counts drop sharply (a free-tier container was evicted and the satellite's ledger restarted from zero), it banks the previous peak and adds the new window on top. Satellites therefore do NOT need persistent disks to report accurately; every container lifetime's peak is summed into the day. Reports persist in the heartbeat store and its Postgres mirror, so they survive hub deploys too.

The endpoint

`` POST https://2plot.ai/api/satellite/traffic Content-Type: application/json X-AI-Canvas-Timestamp: <unix seconds> X-AI-Canvas-Signature: <hex hmac-sha256> ``

The signature is the same scheme as the wallet-provision webhook — HMAC_SHA256(CROSS_APP_WEBHOOK_SECRET, f"{timestamp}." + raw_body), timestamp within ±300 s. The secret is the shared CROSS_APP_WEBHOOK_SECRET env value every satellite already holds.

The payload

Required (v1 — the minimum that puts an app on the network chart):

``json { "app": "media", "date": "2026-07-23", "human_hits": 412, "bot_hits": 77 } ``

app is the satellite's network-directory key (pirates, buzz, xyz, media, cast, dev, …). date is the day the numbers cover (the app's own local day is fine — just be consistent).

Optional v2 fields — adopt piecemeal, each unlocks more of /traffic:

``json { "visitors": 96, "sessions": 143, "median_session_s": 74.5, "pages": [{"path": "/studio", "hits": 210}, {"path": "/", "hits": 88}], "countries": {"US": 61, "DE": 12, "JP": 9} } ``

session rule; keep it so numbers compare).

sessions only. Never pad single-hit visits into it.

Cloudflare, read the CF-IPCountry header (and use CF-Connecting-IP as the client address) instead of geolocating — it's free and accurate.

Reference sender

```python import hmac, hashlib, json, time, urllib.request

def report_traffic(rollup: dict, secret: str): body = json.dumps(rollup).encode() ts = str(int(time.time())) sig = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest() req = urllib.request.Request( "https://2plot.ai/api/satellite/traffic", data=body, headers={"Content-Type": "application/json", "X-AI-Canvas-Timestamp": ts, "X-AI-Canvas-Signature": sig}) with urllib.request.urlopen(req, timeout=10) as r: return json.load(r) ```

Schedule it hourly (a background thread, an APScheduler job, or the platform's cron — anything). The hub itself eats this dog food: its hourly analytics_rollup pulse job records the hub's own day in the exact same format under app="hub".

What the hub does with it

humans, with a bots overlay (toggle All / Humans / Bots).

audiences compare at a glance.

sweep (status, latency, uptime) with each app's reported 7-day reach, visitors/day, session medians, and top countries.

mirror via DATABASE_URL), so history accumulates across deploys and feeds product decisions over time.

---

*Source: /docs/satellite-analytics*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: