AI usage limits + homelab node health in a floating side dock for macOS.
UsageDock is a native, local-first macOS utility that hugs the left or right edge of your screen. A thin sliver sits on the edge; hover it and the dock slides out with circle meters for every API provider and health rings for your homelab nodes. Click a circle for that item's detail, or expand everything with the arrow. No Electron, no telemetry, no account.
Forked from NOTCHY by Ian Silva (MIT) — see License & attribution.
- Floating side dock — sliver → strip → expanded. Drag the strip to flip sides (left ↔ right, persisted). Click a circle for per-item detail; the arrow expands everything.
- 10 providers, each showing what its dashboard shows: real % quota (Claude, Codex, OpenAI, Gemini, ElevenLabs, OpenCode Zen/Go) or credit balance (OpenRouter, DeepSeek, fal.ai, Perplexity).
- CLI auto-detect — Claude, Codex and Gemini read the login your official CLI already stored. No key to paste.
- Node diagnostics — CPU / RAM / disk / temperature / load / uptime /
latency + top-5 processes for any machine running
node_exporter. - In-app banners at 25 / 50 / 75 / 90 % usage. No system permission needed.
- Refresh, settings and add-provider live in the dock strip toolbar (↻ refresh all · ⚙ settings · + add provider).
- Keys live in the macOS Keychain, never in files or logs. Zero telemetry.
| Minimum | |
|---|---|
| macOS | 12.0 Monterey |
| Chip | Apple Silicon (arm64) |
| Xcode CLT | Any version with Swift 5.9+ |
| Xcode (full) | Optional — needed for the XcodeGen build path |
DMG release: grab UsageDock-Installer.dmg from the
latest release,
open it, and drag UsageDock into Applications. Builds are ad-hoc signed —
on first launch: right-click the app → Open → Open, or
xattr -dr com.apple.quarantine /Applications/UsageDock.app.
Or build from source (5 minutes):
xcode-select --install # if you don't have CLT yet
brew install xcodegen # project generation
git clone https://github.com/<you>/UsageDock.git
cd UsageDock/swift-project/UsageDock
xcodegen generate
xcodebuild -project UsageDock.xcodeproj -scheme UsageDock \
-configuration Release -derivedDataPath build build
open build/Build/Products/Release/UsageDock.appUse Release. Debug builds link a
UsageDock.debug.dylibthat crashes with a non-Apple signing identity. First Keychain read prompts once per provider — click Always Allow.
No-Xcode alternative: bash scripts/build.sh (swiftc + ad-hoc signed, local use
only — never distribute that binary).
Open the dock → ⚙ → Providers (or + Add provider in the strip) and set up each one. Keys are stored in the Keychain and sent only to that provider.
| Provider | Shows | How to connect |
|---|---|---|
| Claude | 5 h session + weekly quota | No key — auto-detects the Claude Code CLI login (Keychain Claude Code-credentials / ~/.claude/credentials.json). No CLI? Paste a claude.ai session cookie instead. |
| Codex | ChatGPT-plan 5 h + weekly | No key — npm i -g @openai/codex && codex login; reads ~/.codex/auth.json. |
| Gemini | Code Assist per-model quota | No key — sign in with the gemini CLI; reads ~/.gemini/oauth_creds.json. A plain API key gives "Connected" only. |
| OpenAI | Monthly spend vs hard limit | Paste an OpenAI API key (sk-…) from platform.openai.com. |
| OpenRouter | Credits used vs purchased (%) | Paste your OpenRouter API key; open the detail to see $used of $limit. |
| DeepSeek | Remaining balance | Paste your DeepSeek API key (platform.deepseek.com). |
| ElevenLabs | Characters vs monthly limit | Paste your ElevenLabs API key. |
| Perplexity | Connected / desktop token | Paste a Perplexity API key (or the desktop token). |
| fal.ai | Credit balance | Paste a fal key. Billing needs an admin-scoped key: a plain inference key is accepted by the API but returns 403 for GET /v1/account/billing — UsageDock then shows "Permission denied — this key cannot read account billing" instead of a misleading "expired" error. Rotate/scope the key at fal.ai/dashboard. Keys do not need a fal prefix. |
| OpenCode | Zen/Go limits: rolling (5 h) + weekly + monthly | Paste the auth cookie from opencode.ai (DevTools → Application → Cookies → opencode.ai). Fallback: if ~/.local/share/opencode/auth.json exists, OpenCode shows connected-only. |
Claude / Codex / Gemini / OpenCode use undocumented internal endpoints that can change without notice. If one breaks, open an issue.
The provider reads the Go subscription limits from the opencode.ai dashboard's
/_server RPC (Wasp "server-fn" wire format): a rolling ~5 h window plus
weekly and monthly windows, each with a reset countdown — the same cards the
dashboard shows. The RPC id/args are workspace-specific and deliberately
not compiled in. Configure your own once:
defaults write com.usagedock.UsageDock opencodeZenGoURL \
'https://opencode.ai/_server?id=…&args=…'Capture it from your browser session: DevTools → Network → find the /_server
call on the Go page → copy its full request URL. (The id query value is
also the X-Server-Id header; the app handles that automatically.)
When the cookie expires the card shows "Sign in again" — paste a fresh cookie.
UsageDock polls any machine that runs
node_exporter (Prometheus text
format, default port 9100) and shows a health card per node: CPU, RAM, disk,
temperature, load, uptime, scrape latency, and top-5 processes.
Docker quick start (adjust the bind address — prefer a Tailscale/WireGuard IP so the exporter is reachable only over your private network):
docker run -d --name node_exporter --restart unless-stopped \
-p 100.64.0.1:9100:9100 \ # <— your private IP, not 0.0.0.0
-v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/rootfs:ro \
-v /home/<user>/node_exporter_textfile:/var/lib/node_exporter/textfile:ro \
prom/node-exporter:v1.8.2 --path.rootfs=/host --path.procfs=/host/proc --path.sysfs=/host/sys \
--collector.textfile.directory=/var/lib/node_exporter/textfile--collector.textfile.directory is only needed for the top-5 process card
(step 2); drop that mount if you don't want it.
A 30 s user systemd timer runs a collector that writes instantaneous CPU/MEM
deltas (from /proc, not ps — ps %CPU is a lifetime average) into a
.prom file the exporter exposes:
# on the node, as the user (no root needed):
mkdir -p ~/node_exporter_textfile ~/.local/bin ~/.config/systemd/user
install -m 755 docs/node-top5.py ~/.local/bin/node-top5.py
sed 's|^OUT = .*|OUT = "'"$HOME"'/node_exporter_textfile/processes.prom"|' \
docs/node-top5.py > ~/.local/bin/node-top5.py
# fix the textfile mount above to match this path
cp docs/node-top5.{service,timer} ~/.config/systemd/user/
XDG_RUNTIME_DIR=/run/user/$(id -u) systemctl --user enable --now node-top5.timerThe
.promfile must be world-readable (chmod 644) because the exporter container runs asnobody.
⚙ → Nodes → Add node — name, host (IP or hostname), port. Poll interval:
10 s / 30 s / 1 min. Host may hold several addresses — tailnet IP, LAN IP,
mDNS name — comma-separated; UsageDock tries them in order and uses the first
that answers, so a node survives a logged-out VPN or a new DHCP lease. The card
shows the address that actually answered. Config persists in
~/Library/Application Support/UsageDock/nodes.json (local-only — node
addresses never leave your machine or get committed).
macOS 15+ gates LAN traffic behind Local Network privacy: without the grant macOS fails the connection with
Local network prohibited(visible vialog show --predicate 'process == "UsageDock"') while a tailnet address keeps working. Allow UsageDock under System Settings → Privacy & Security → Local Network, and keepNSLocalNetworkUsageDescriptionin the Info.plist.
CPU% is computed from counter deltas between polls (first scrape shows —).
| Action | Result |
|---|---|
| Hover the edge sliver | Strip slides out with circle meters |
| Click a circle | Detail for that provider/node only |
| Click the top/bottom arrow | Expand everything (all providers + node cards) |
| Drag the strip across the edge | Flip dock side (persisted) |
| ↻ / ⚙ / + in the strip | Refresh all / settings / add provider |
| Click outside | Collapse |
Circle meters: real % arc for quota providers, full ring + balance amount for balance providers, checkmark for connected-only. Nodes show a health ring (colour-coded by the worst metric).
cd swift-project/UsageDock
brew install xcodegen
xcodegen generate
xcodebuild -project UsageDock.xcodeproj -scheme UsageDock \
-configuration Release -derivedDataPath build build \
CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY="UsageDock Dev Signer"
open build/Build/Products/Release/UsageDock.app- Stable signing identity (one-time): a self-signed cert makes the Keychain "Always Allow" stick across rebuilds. Without it, ad-hoc signing changes the identity every build and macOS re-prompts per credential.
- ATS: the only plain-HTTP traffic is your own node_exporter instances
(raw IPs), which ATS has no exception mechanism for — so
NSAllowsArbitraryLoadsis enabled. Node metrics stay on your network. - Tests:
scripts/test.sh— aswiftcharness compiling the real sources (hosted XCTest crashes on LSUIElement apps, so tests run headless via the harness pattern). Run it after any provider/node change.
- Provider credentials live in the macOS Keychain
(
com.usagedock.UsageDockservice — migrated from the pre-renamecom.notchylimit.NotchyLimitservice). Never logged, never in the repo. - Zero telemetry: the app talks only to the providers you configure and the nodes you add.
- Node metrics are plain-HTTP by design — bind node_exporter to a private
(Tailscale) address, never
0.0.0.0.
MIT. Fork of NOTCHY by Ian Silva;
upstream code keeps its MIT copyright, fork additions are MIT. See
LICENSE / swift-project/UsageDock/LICENSE.