An Android home-screen widget + web dashboard that shows your AI subscription usage limits at a glance — Claude (Anthropic), Codex (ChatGPT), Droid (Factory), and DeepSeek: the 5-hour rolling window, the weekly window, reset countdowns, and remaining API balance.
Gauges are color-coded by how much of the window you've used: 🟢 green < 70% · 🟡 amber 70–90% · 🔴 red ≥ 90%.
| Android widget | Web dashboard |
|---|---|
![]() |
![]() |
- Multi-provider: Claude, Codex (ChatGPT plan), Droid (Factory), DeepSeek —
any provider whose credentials are missing simply shows
—; the rest still work. - Android widget (Kotlin / Jetpack Glance):
- Wide → all providers side by side; small → one provider, tap ◀ ▶ to cycle.
- Responsive size buckets incl. foldable cover-screen / inner-screen cells.
- Dark & light themes (follows the system setting).
- Refresh via WorkManager (~15 min), on tap, and on widget add.
- Web dashboard (single static HTML file, zero build step) — same data, same design language, works as a browser tab or a "Mac widget" style small window.
- Per-model weekly caps: when Anthropic exposes a model-scoped weekly limit (e.g. a per-model sub-row in their usage panel), the Claude card splits into ALL / per-model micro-bars automatically — the label comes from the API.
- Droid Core fallback: once Factory's Standard pool is exhausted the tile flips to the Core pool (with a CORE badge) and back automatically.
- Resilient bridge: per-provider isolation, retries, 60s cache, last-good fallback with staleness cutoff, OAuth auto-refresh for Claude and Codex.
Usage limits are per-account, not per-machine — any process logged into your Claude / ChatGPT account sees the same numbers. So a single always-on bridge running wherever you're already logged in can serve them, and the clients stay dumb: fetch one normalized JSON, render it.
[Android widget] ┌──> api.anthropic.com (Claude OAuth usage)
[Web dashboard ] --HTTPS + bearer --> [bridge /usage] ──> chatgpt.com (Codex rate-limit headers)
├──> api.factory.ai (Droid billing limits)
└──> api.deepseek.com (balance)
| Provider | Source | Data |
|---|---|---|
| Claude | GET api.anthropic.com/api/oauth/usage |
5h + weekly utilization %, reset times, per-model weekly cap |
| Codex | streamed POST chatgpt.com/backend-api/codex/responses, read the x-codex-* rate-limit response headers (body aborted → ~zero quota) |
5h + weekly used %, reset times |
| Droid | GET api.factory.ai/api/billing/limits |
Standard + Core pools, 5h + weekly |
| DeepSeek | GET api.deepseek.com/user/balance |
remaining balance |
The bridge is a single zero-dependency Python file (bridge/bridge.py, stdlib
only, Python 3.8+). Run it on a machine that is always on and already
logged into the CLIs you want to monitor (Claude Code, Codex). A cheap
always-on VPS or home server is ideal.
- Claude —
~/.claude/.credentials.json(created byclaudelogin). Override withCLAUDE_CREDS. - Codex —
~/.codex/auth.json(created bycodexlogin, ChatGPT plan). Override withCODEX_AUTH. - DeepSeek (optional) — an API key, via env
DEEPSEEK_API_KEYor a file (DEEPSEEK_KEY_FILE, default~/.ai-usage-gauge/.deepseek_key). - Droid / Factory (optional) — an API key, via env
FACTORY_API_KEYor a file (FACTORY_KEY_FILE, default~/.ai-usage-gauge/.factory_key).
The widget authenticates to the bridge with a static bearer token you choose:
mkdir -p ~/.ai-usage-gauge
python3 -c "import secrets; print(secrets.token_urlsafe(32))" > ~/.ai-usage-gauge/.bridge_token
chmod 600 ~/.ai-usage-gauge/.bridge_tokenOr supply it inline with the USAGE_BRIDGE_TOKEN env var.
python3 bridge/bridge.py # listens on 127.0.0.1:8787Endpoints:
GET /usage— bearer-authed, returns normalized JSON for all providers.GET /health— no auth.
Test locally:
curl -H "Authorization: Bearer $(cat ~/.ai-usage-gauge/.bridge_token)" localhost:8787/usageSee deploy/ for ready-to-edit systemd units and notes. In short:
# edit the unit paths first, then:
sudo cp deploy/ai-usage-bridge.service /etc/systemd/system/
sudo systemctl enable --now ai-usage-bridge| Var | Default | Purpose |
|---|---|---|
LIMITS_BRIDGE_PORT |
8787 |
Listen port |
USAGE_STATE_DIR |
~/.ai-usage-gauge |
Base dir for the token/key files |
USAGE_BRIDGE_TOKEN |
— | Bearer token (overrides the file) |
BRIDGE_TOKEN_FILE |
$USAGE_STATE_DIR/.bridge_token |
Bearer token file |
CLAUDE_CREDS |
~/.claude/.credentials.json |
Claude OAuth creds |
CODEX_AUTH |
~/.codex/auth.json |
Codex OAuth creds |
DEEPSEEK_API_KEY |
— | DeepSeek key (overrides the file) |
DEEPSEEK_KEY_FILE |
$USAGE_STATE_DIR/.deepseek_key |
DeepSeek key file |
FACTORY_API_KEY |
— | Factory (Droid) key (overrides the file) |
FACTORY_KEY_FILE |
$USAGE_STATE_DIR/.factory_key |
Factory key file |
FACTORY_TIER |
standard |
Default Droid pool to surface (standard / core) |
BRIDGE_ERROR_LOG |
$USAGE_STATE_DIR/bridge-errors.log |
Per-provider fetch failure log |
BRIDGE_FETCH_ATTEMPTS |
3 |
Retries per provider fetch |
USAGE_LAST_GOOD_MAX_AGE |
600 |
Seconds a stale last-good value may be served |
The widget needs to reach the bridge over the internet. Pick one:
- Cloudflare Tunnel (recommended, stable): requires a domain on Cloudflare.
cloudflared tunnel login, create a named tunnel, route a subdomain tohttp://localhost:8787. Stable public HTTPS URL, no open ports. - Cloudflare quick tunnel (free, no domain):
cloudflared tunnel --url http://localhost:8787→ prints a random*.trycloudflare.comURL. Works instantly, but the URL changes on restart. Seedeploy/for a systemd unit + helpers that detect the current URL and can notify you (pluggableNOTIFY_CMD) when it rotates. - Tailscale / WireGuard: keep it fully private. Note Android allows only one VPN at a time, so this conflicts with other VPN/proxy apps you run.
The bridge listens on 127.0.0.1 only — it is never directly exposed; the tunnel
terminates TLS and forwards to it.
web/vibegauge.html is a single static file — open it in any browser (or
host it anywhere static). On first load it asks for your bridge URL and
bearer token, stored only in that browser's localStorage. No build step,
no dependencies.
A prebuilt debug APK is attached to each GitHub Release. To build it yourself:
Requirements: JDK 17, Android SDK (platform 35 + build-tools 35), no Android Studio needed.
cd android
echo "sdk.dir=$ANDROID_HOME" > local.properties
./gradlew :app:assembleDebug
# → app/build/outputs/apk/debug/app-debug.apkSideload the APK (enable "install from unknown sources"). Open VibeGauge, enter your bridge URL and bearer token, tap Save & refresh, then add the widget from your launcher's widget picker.
The repo includes a Robolectric-based render harness that rasterizes the real widget to PNGs — no device or emulator needed:
cd android
./gradlew :app:testDebugUnitTest --tests '*WidgetRenderTest'
# → writes /tmp/widget-*.png for visual inspection- The bridge caches upstream results for 60s (so redraws don't hammer providers).
- The widget refreshes via WorkManager every ~15 minutes (Android's minimum periodic interval; may stretch longer under Doze/battery optimization), plus on add and on tap (tapping forces an immediate fetch).
- Worst-case staleness ≈ up to ~15 min + up to 60s. A tap gets you data no more than ~60s old.
- The bridge binds to
127.0.0.1only; expose it exclusively through your tunnel. GET /usagerequires the bearer token (constant-time comparison);/healthis auth-free and returns no data.- Provider credentials never leave the bridge host and are never sent to clients — the JSON contains only percentages, timestamps, and balances.
- On Android, the bridge URL + token are stored in EncryptedSharedPreferences.
- On web, they live in that browser's
localStorageonly — never embedded in the HTML file itself. - No secrets belong in this repo: state files live outside the tree (see
.gitignore).
Kotlin · Jetpack Glance · WorkManager · OkHttp · EncryptedSharedPreferences · Robolectric render tests · Python stdlib bridge · single-file static web UI. minSdk 26, target 35.
Issues and PRs welcome. Useful starting points:
PLAN.md— original design/architecture notes and provider API findings.DESIGN_SPEC.md— the widget's visual spec (size buckets, ring geometry, palette).bridge/bridge.py --diagnose-claude— dumps the raw Anthropic usage response for debugging provider changes.
MIT — see LICENSE.

