A small, self-hosted X (Twitter) stats dashboard. Track follower growth and per-post metrics (impressions, likes, replies, reposts) for one or more accounts, with daily history and a per-post drill-in. Ships as a single Go binary.
- Account switcher for any number of accounts
- Current follower count with 7 and 30 day deltas
- Follower growth chart built from daily snapshots
- Recent posts table (last 30 days), retweets excluded, replies hidden behind a toggle
- Per-post drill-in with impressions, likes, replies and reposts over time
- Light and dark themes (follows the system, remembers your choice), responsive down to phone width
- Optional "clickthroughs from X" card if you feed in referral visit counts
| Post drill-in | Mobile, light |
|---|---|
![]() |
![]() |
your snapshot job server
(any X API client) HTTPS +-------------------------------+
raw JSON bundle ------------> | Caddy (TLS, basic auth) |
POST /api/ingest Bearer | /api/ingest: token only |
| everything else: basic auth |
| | |
| xstats serve (127.0.0.1) |
| embedded frontend + API |
| | |
| SQLite (xstats.db) |
+-------------------------------+
- One binary.
xstatsembeds the static frontend withgo:embedand usesmodernc.org/sqlite, so it builds withCGO_ENABLED=0and has no runtime dependencies. - SQLite. Tables:
accounts,follower_snapshots(one row per account per day),post_metrics(one row per post per day),clickthroughs(one row per day). Re-ingesting a day upserts, so it is always safe to rerun. - Ingest API.
POST /api/ingestwithAuthorization: Bearer <token>accepts a snapshot bundle. The token comes fromXSTATS_INGEST_TOKEN; if it is unset, ingest is disabled. - Read API.
GET /api/data(all accounts, history, recent posts) andGET /api/post/<id>(daily history for one post). - Caddy terminates TLS with automatic certificates and puts basic auth in front of everything except
/api/ingest.
xstats does not call the X API itself. Collect data with whatever client you like and hand it the raw JSON.
Requires Go 1.23+.
go test ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/xstats ./cmd/xstats
bin/xstats ingest --db data/xstats.db snapshot.json
bin/xstats serve --db data/xstats.db --addr 127.0.0.1:8080
Open http://127.0.0.1:8080. Handy URL options: ?theme=dark|light, ?acct=<handle>, #post/<id>.
scripts/seed.py writes 30 days of made-up snapshots for two fictional accounts (example_one, example_two). The screenshots in this README come from it.
scripts/seed.py /tmp/seed 30
bin/xstats ingest --db /tmp/demo.db /tmp/seed/*.json
bin/xstats serve --db /tmp/demo.db --addr 127.0.0.1:8080
One JSON file per day. The user and posts objects are the X API v2 responses as returned, so you can save them without reshaping.
{
"date": "2026-10-14",
"accounts": {
"example": {
"user": { "id": "123", "username": "example", "name": "Example",
"profile_image_url": "https://...",
"public_metrics": { "followers_count": 100, "following_count": 50, "tweet_count": 400 } },
"posts": [ { "id": "1846...", "text": "Hello", "created_at": "2026-10-14T15:00:00.000Z",
"in_reply_to_user_id": "",
"public_metrics": { "impression_count": 120, "like_count": 4, "reply_count": 1,
"retweet_count": 2, "quote_count": 0 } } ]
}
},
"clickthroughs": { "visits": 12 }
}datedefaults to today if omitted.clickthroughsis optional.- Posts starting with
RT @are skipped. Posts starting with@or within_reply_to_user_idare flagged as replies. - Reposts shown in the UI are
retweet_count + quote_count.
Suggested X API v2 calls per day (3 reads for two accounts):
GET /2/users/by?usernames=a,b&user.fields=public_metrics,profile_image_url,nameGET /2/users/:id/tweets?max_results=50&tweet.fields=public_metrics,created_at,in_reply_to_user_idper account
Push a day to a running server:
STATS_URL=https://stats.example.com snapshot/push.sh snapshot-2026-10-14.json
push.sh reads the token from ~/.config/xstats/ingest_token (override with XSTATS_TOKEN_FILE) and passes it to curl on stdin, never on the command line.
Target: a fresh Ubuntu 22.04 or 24.04 server (512 MB is plenty). Nothing is compiled on the server.
- Point a DNS A record (for example
stats.example.com) at the server's IPv4 address. Caddy needs this to get a certificate. - Create an ingest token on your machine:
mkdir -p ~/.config/xstats && (umask 077; openssl rand -hex 32 > ~/.config/xstats/ingest_token) - Build, then deploy:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/xstats ./cmd/xstats read -rs BASIC_AUTH_PASSWORD && export BASIC_AUTH_PASSWORD XSTATS_HOST=203.0.113.10 XSTATS_DOMAIN=stats.example.com SSH_KEY=~/.ssh/id_ed25519 deploy/deploy.sh unset BASIC_AUTH_PASSWORD
Put these in ~/.config/xstats/deploy.env (mode 0600, see .env.deploy.example) so deploy.sh and push.sh pick them up automatically. Keep BASIC_AUTH_PASSWORD out of that file and export it only when deploying.
| Variable | Default | Meaning |
|---|---|---|
XSTATS_HOST |
required | Server IP or hostname for SSH |
XSTATS_DOMAIN |
required | Public hostname served by Caddy |
BASIC_AUTH_PASSWORD |
required | Dashboard password |
XSTATS_AUTH_USER |
admin |
Dashboard username |
XSTATS_SSH_USER |
root |
SSH user |
SSH_KEY |
~/.ssh/id_ed25519 |
SSH private key path |
XSTATS_TOKEN_FILE |
~/.config/xstats/ingest_token |
Ingest token file |
XSTATS_ACCOUNTS |
unset | Optional tab order, e.g. alice,bob; first is the default |
STATS_URL |
required for push | Base URL used by snapshot/push.sh |
deploy.sh installs Caddy from its official apt repo, creates an xstats system user, installs the binary to /usr/local/bin, the unit from deploy/xstats.service, and the token in /etc/xstats/env (mode 0640). The password is sent over SSH stdin and hashed on the server with caddy hash-password; only the bcrypt hash is stored. The script is idempotent.
Each deploy keeps the previous binary as /usr/local/bin/xstats.prev and backs up the database to /var/lib/xstats/xstats.db.bak-<timestamp>.
To update only the binary later:
scp bin/xstats root@$XSTATS_HOST:/tmp/xstats.new
ssh root@$XSTATS_HOST 'install -m 0755 /tmp/xstats.new /usr/local/bin/xstats && systemctl restart xstats'
mockup/ holds the original static design with example data.
MIT, see LICENSE.


