How much of your Claude Code and Codex subscription is left, at a glance, on every screen you own: a GNOME tray, a Wear OS watch, Android and iPhone home screens. Several Claude accounts pool together.
One Go program, aiusage-server, reads the usage on the desktop, owns pairing
and alerts, and serves a small JSON API. Everything else is a client of that
API: the Linux tray (aiusagebar), the Wear OS app, the
Android app and widgets and the iPhone app and widgets.
Closing any client leaves the server running.
Every client draws your accounts the same way: each account's week is a thread, bright for what's left and faint where it's been used, with a bead where it runs out. Claude's accounts twist round the outer ring (orange, tan and further shades), Codex's round the inner one (greens).
| Wear OS: the orbit tile | Tap a ring: Claude | Tap a ring: Codex |
|---|---|---|
![]() |
![]() |
![]() |
On the watch the orbit is a tile and the app's first page. Tap a ring and it blurs slightly, the family's session and week left circle over it, slowly turning, and its mark sits in the middle; tap again to put it back. The blur and lettering are drawn once per data change and cached, so a tap costs nothing but a redraw.
| Android: orbit | Android: threads | Android app |
|---|---|---|
![]() |
![]() |
![]() |
| Android: dash | Session out | Tap card |
|---|---|---|
![]() |
![]() |
![]() |
| iPhone: orbit and threads | iPhone app | App icon (the ring) |
|---|---|---|
![]() |
![]() |
Screenshots of the phones use made-up accounts (the debug fixture); the watch's are live.
- Wear OS (
wearos/): the orbit as a tile and as the app, with the tap-a-ring figures; a Setup page (provider, pairing, the API-cost estimate); two complications (% left and time to the reset that matters) and the server's alerts, including "Claude's back" when a spent limit resets. - Android (
android/): five widgets in Nothing OS's style: the pooled 1x1 ring, the 2x2 ring, the 2x2 orbit, the 4x2 dash and the 4x1 threads. The app shows each family's Session and Weekly cards (the share of the whole family left, what the next reset brings back, pace, a bar split per account, a row per account). Tapping a widget opens a compact card: both families' limits, a five-hour history graph of the pooled session with upcoming reset steps, and every account's week as threads. - iPhone (
ios/): the same app and tap card, and Android's widgets where iOS has a size for them: the orbit (small), the dash (medium), the threads (medium, with the selected family's session left, burn rate and what's usable now under them; also on the Lock Screen) and the pooled ring (Lock Screen). iOS has no 1x1 Home Screen widget, so the ring is the app's icon. - Linux tray (
aiusagebar):- Tray icon = two small meters (green/orange/red, grey when stale) for the
label's two numbers. Tray text = one provider's windows (
C1 5h 42% · 7d 61%) or, with two Claude accounts, each account's tighter limit (Claude 42 · 17%). - Click menu shows:
- Session (5h) and Week (7d) limit utilization and when each resets,
for every provider the server serves (Claude, Claude 2, Codex)
- read straight from Anthropic's own cached usage data in
~/.claude.json(the same numbers Claude Code itself shows you), so the percentages and reset times are exact, not estimated.
- read straight from Anthropic's own cached usage data in
- API-cost equivalent for today / this session / this week - what the
same token usage would have cost on pay-as-you-go API pricing, computed
from the token counts logged in
~/.claude/projects/**/*.jsonlusing current per-model Anthropic pricing (updated 2026-09). Each reply is counted once: Claude Code writes a transcript line per content block, all repeating the reply's usage, so lines are deduplicated by message id + request id. Cache reads use each model's own rate (Opus 5.5 and Fable 5.1 aren't the usual 0.1x), and 1-hour cache writes are 2x input. T3 Code prices every cache write at the 5-minute 1.25x rate, so it shows a little less for the same usage.
- Session (5h) and Week (7d) limit utilization and when each resets,
for every provider the server serves (Claude, Claude 2, Codex)
- Refreshes every 30s, or on demand via the "Refresh" menu item.
- The independent server serves these numbers to every client over the same API. The tray shows connection/source freshness and retains visibly stale values when offline.
- Tray icon = two small meters (green/orange/red, grey when stale) for the
label's two numbers. Tray text = one provider's windows (
Subscription plans (Pro/Max) don't expose a dollar limit or a metered balance - only a percentage of your session/weekly allowance. The cost figure here is a separate, independent estimate of value, not what you're being billed.
CGO_ENABLED=0 go build -o aiusage-server ./cmd/aiusage-server # Go only; no GTK
go build -o aiusagebar . # Linux tray clientRun go test -race ./... for the Go checks, and
python3 scripts/check-split.py ./aiusage-server ./aiusagebar for isolated
pairing and restart checks with fake provider sources.
The server needs Go 1.22+ only. The tray additionally needs a C compiler (gcc)
and GTK/AppIndicator development headers:
sudo apt install build-essential libgtk-3-dev libayatana-appindicator3-devThe watch and phone apps are separate Gradle builds (Android SDK, JDK 17+), installed by sideloading. Both pair with the server (see Pairing a device):
cd wearos && ./gradlew :wear:assembleRelease # wear/build/outputs/apk/release/
cd android && ./gradlew :app:assembleRelease # app/build/outputs/apk/release/Or skip the build: each widget-v* release on the
Releases page has the
phone APK, and from 0.12.0 the watch APK too. Both are signed with a local
debug key, so a release built on a different machine than the one you
installed from won't install over it: uninstall first (and pair again).
The iPhone app is an Xcode project generated with XcodeGen, built on a Mac (Xcode 26+, iOS 17+) and installed from Xcode with your own signing team:
cd ios && xcodegen && open AIusageWidget.xcodeprojOn the Home Screen, choose the Clear appearance for WidgetKit's Liquid Glass widget background; tinted and Lock Screen appearances work too. Pick Claude, Claude 2 or Codex under the app's "•••": the dash follows it, the pool and the threads' figures pool that pick's family, and the orbit and the threads always show every account.
Start the server on the machine where the provider CLIs are signed in:
./aiusage-server -serve :8765Pair the Linux tray just like another device. On the server machine, mint a code:
./aiusage-server -pairThen configure the tray using that code (replace the example):
./aiusagebar -server http://127.0.0.1:8765 -pair 123456
./aiusagebarFor a server on another machine, use its private LAN IP or public HTTPS URL.
The tray stores its own device token and chosen provider in
$XDG_CONFIG_HOME/aiusagebar/client.json (default ~/.config/aiusagebar/client.json,
mode 0600). It never reads provider credentials, transcripts, or server state.
Changing -server clears the old token; pair with the new server before using it.
The menu lists every provider the server serves: Claude (or Claude 1 and
Claude 2, each with its account email, when the server runs a CLIProxyAPI hub
with two subscriptions) and Codex, each with its 5h and weekly meters, reset
times and any source error. Claude 2 only appears once the server has delivered
limits for it.
-provider pool|claude|claude2|codex chooses what the top-bar label shows, and
the menu's "Top bar shows" (or a click on a section heading) changes it. pool,
the default for new configs, shows both Claude accounts as Claude 42 · 17%:
each number is that account's tighter limit (the higher of its 5h and weekly
use), and the icon's two bars match the two numbers. A single provider shows
C1 5h 42% · 7d 61%. Saved configs naming claude or codex keep working.
-check fetches one JSON snapshot without opening the tray (an array of both
Claude accounts for pool), useful for diagnostics.
-config /path/to/client.json selects a different client configuration.
The standalone server supports the existing phone/watch API and flags:
./aiusage-server -serve :8765
./aiusage-server -serve :8765 -token s3cr3t # optional shared secretThe server advertises _aiusage._tcp over mDNS through avahi-publish-service
when available. -mdns=false disables LAN discovery.
The server retains $XDG_STATE_HOME/aiusagebar/state.json and the local
aiusagebar.sock control socket, so existing mobile tokens, alert history and
pairing behavior survive the split. The tray's token is a separate device entry.
To upgrade from the combined binary, stop that process, start aiusage-server
with its previous server flags, then pair and start the new aiusagebar client.
The tray no longer accepts -serve, -headless, server refresh settings, or
server pairing-code creation; those responsibilities belong to aiusage-server.
Install the official Codex CLI on the server machine and sign in with your ChatGPT
account using codex login. The bridge uses that CLI's existing login via
account/rateLimits/read,
without opening a thread or making a model request. API-key-only accounts do
not supply subscription quota windows through this method.
./aiusage-server -serve :8765 -codex /path/to/codex # optional; PATH or ~/.local/bin/codex by defaultThe desktop menu switches between Claude and Codex. On Android, the widget's tap card and the app show both side by side; pick which one the widgets draw with its "Show on widgets" button. On iOS, the app and the card show both too; pick the widgets' provider under the app's "•••". Each phone's ring and dash widgets follow that selection. On the watch, choose the provider on the Setup page; its complications and notifications follow that selection, while the orbit always shows every account. Pairing works for both providers with the same device token.
Codex is sampled at most once a minute, including requests from multiple
clients. If the CLI is missing, signed out or offline, the bridge reports
source_error and retains the last successful limits and their original
cache_fetched_at. Clients dim stale data and retry. An absent quota window
is shown as unknown. Codex cost estimates are unavailable (cost_available: false); the numeric cost fields are compatibility placeholders.
| Endpoint | What it does |
|---|---|
GET /usage |
current snapshot for ?provider=claude (default), claude2 (second hub account, see Several Claude accounts) or codex, refreshed first if it's stale (see Freshness). Sends an ETag; send it back as If-None-Match to get 304 when nothing has changed |
GET /events?since=<id> |
alert events for the selected provider (default claude; append &provider=claude2 or &provider=codex), including threshold crossings and window resets newer than <id>, plus the new high_water mark |
GET /pool?family=claude|codex |
the pooled limit across a family's accounts, with recorded history and a projection (see Pooled limits). Same auth as /usage; ETag/If-None-Match work too |
POST /pair {"code":"123456"} |
redeems a pairing code for a per-device token (plus public_url, if set) |
GET /healthz |
liveness, no auth, no usage data. Says whether a token is needed |
GET /share |
off unless -share is set. The selected provider's limits only (?provider=codex, claude2), never the account email, no token, readable from web pages (see Show it on a website) |
{
"schema": 1, "provider": "claude", "cost_available": true,
"five_hour": {"utilization": 53, "remaining": 47, "resets_at": "2026-09-21T18:20:00Z", "resets_in_sec": 16631},
"seven_day": {"utilization": 41, "remaining": 59, "resets_at": "2026-09-27T00:00:00Z", "resets_in_sec": 469031},
"cost_today": 251.02, "cost_session": 25.06, "cost_week": 351.44,
"generated_at": "2026-09-21T13:43:29Z", "cache_fetched_at": "2026-09-21T13:43:12Z", "host": "pop-os"
}The ETag deliberately ignores generated_at and resets_in_sec: they change
on every poll but carry no news, and a 304 over a Bluetooth proxy is nearly
free next to a full body. A 304 means "your copy is still current now", so
use cache_fetched_at for the age of the source data when it is present,
and otherwise use the last successful fetch time. ETags include the
source fetch time so a fresh quota read updates that age even if usage has
not changed. ETags also include the provider, source errors and window lengths.
five_hour and seven_day retain their schema-1 names for both providers.
For Codex these carry the primary and secondary windows, respectively, and
include window_minutes: use that duration for labels, pace and countdown
fractions. Neither window has a fixed duration; an hourly primary and daily
secondary are also supported. Reset times always come from resets_at. Optional source_error
means the upstream source is unavailable; any attached limits are stale.
Older bridges that omit provider and cost_available are treated as Claude.
An optional account (the subscription's email) is sent in hub mode; clients
that don't know it ignore it.
Claude limits come from Claude Code's cache in ~/.claude.json, which only moves
when Claude Code fetches usage, normally during a session. So asking is
refreshing: when a client calls GET /usage and that cache is more than a
minute old, the bridge first runs the official CLI's local /usage command
(claude -p /usage; no model call, a couple of seconds), which makes Claude
Code fetch current numbers, and then answers. That's at most one CLI run a
minute however many clients ask, and concurrent requests share it. A client's
refresh button and its background poll are the same request; there's no
separate refresh endpoint.
cache_fetched_at is when Claude Code last fetched the limits, so the true age
of the numbers. The bridge never talks to Anthropic itself.
./aiusage-server -serve :8765 -refresh-min-age 5m # refresh less eagerly
./aiusage-server -serve :8765 -refresh-min-age 0 # never; serve the cache as Claude Code leaves it
./aiusage-server -serve :8765 -claude /opt/bin/claude # if claude isn't on PATH or in ~/.local/binGET /pool?family=claude|codex (default claude; anything else is 400;
optional days=1..14 sets how much history to return, default 7) answers "how
much can I still use across all my accounts?". Claude's family is every hub
account, Codex's is a pool of one. Numbers are in account-percent: one account
is 100, so two accounts have capacity_pct 200. The wire types are in
internal/protocol/pool.go.
{
"family": "claude", "accounts": 2, "capacity_pct": 200,
"remaining_pct": 131.0, "five_hour_remaining_pct": 103.0, "effective_remaining_pct": 103.0,
"burn_pct_per_hour": 1.63,
"next_reset": {"at": "2026-11-01T02:48:00Z", "gain_pct": 65.4},
"first_empty_at": "2026-11-06T05:29:57Z", "pool_empty_at": null,
"members": [{"provider": "claude", "account": "a@x.com", "five_hour": {"...": "as /usage"}, "seven_day": {"...": "as /usage"}, "source_error": ""}],
"history": [{"t": 1793393280, "remaining_pct": 131.0, "five_hour_remaining_pct": 103.0, "effective_remaining_pct": 103.0}],
"projection": [{"t": 1793393280, "remaining_pct": 131.0, "reset": false}],
"generated_at": "2026-10-31T02:48:00Z", "source_error": ""
}remaining_pctis the weekly pool (Σ100 - seven_day.utilization);five_hour_remaining_pctthe same over the five-hour windows;effective_remaining_pctsums, per account, the smaller of the two, which is what is usable right now. A window an account doesn't report stands in for the other one.burn_pct_per_houris how fastremaining_pctdropped over the last 3 hours of history (resets inside the window don't offset it);nulluntil there's a minute of history.projectionplays the pool forward 7 days: one point per hour plus, at each weekly reset, two points with the samet(value just before, then the value after withreset: true). It drains at the burn rate split evenly across the accounts that still have weekly capacity, never below 0, and an account jumps back to 100 at itsseven_day.resets_at(and every window length after: 7 days, or Codex'swindow_minutes). With no burn it is flat apart from the step-ups.first_empty_at/pool_empty_atcome from that projection (nullif not within 7 days;pool_empty_atis "now" if every account is already at 0).next_resetis the soonest reset and what it adds back.- No account with data (Claude without a signed-in hub account, say) gives
accounts: 0, empty arrays and asource_error; it is not an HTTP error.
History is sampled by the poll loop and on each /pool request whenever the
pool's numbers change (at least 5 minutes apart, plus an hourly sample even if
nothing changed), kept for 14 days and stored as JSON lines in
$XDG_STATE_HOME/aiusagebar/pool-history-<family>.jsonl (mode 0600), so it
survives restarts. The ETag covers the underlying snapshots and a 15-minute
time bucket, because the projection is anchored at "now".
New history samples also include five_hour_remaining (remaining percent,
0–100) and five_hour_resets_at (Unix seconds), maps keyed by provider such
as claude, claude2 or codex. Only accounts reporting a five-hour window
are included, and idle windows may have no reset. Older samples omit these
maps; clients can fall back to the pooled five-hour sum.
With more than one Claude subscription pooled behind a local
CLIProxyAPI hub, Claude Code no
longer refreshes ~/.claude.json for any one of them. Point the server at the
hub and it reads each account's limits from there instead:
./aiusage-server -serve :8765 -cliproxy-url http://127.0.0.1:8317 -cliproxy-key-file ~/.cli-proxy-api/keys.envThe key file is env-style and must hold a CPA_MGMT_KEY=... line (the hub's
management key; it is re-read on every poll and never logged). In this mode:
- The hub's enabled Claude accounts, sorted by email, become providers
claude(first) andclaude2(second). A missing account is asource_error. - The hub is asked at most every 5 minutes (15s timeout): its management
auth-filesroute lists accounts, and itsapi-callroute fetches Anthropic's usage for each one on the account's own token. The hub calls Anthropic; the server only talks to the hub. Theclaude -p /usagerefresher is not used. If the hub fails, the last limits are kept andsource_errorsays so. - Transcripts are pooled across accounts, so the cost estimate stays on
claude(windowed by that account's resets);claude2hascost_available: false. Alerts are armed separately for each provider.
Claude's resets_at is rounded to the minute: Anthropic's value wobbles by a second
between fetches, which would otherwise look like the window rolling over. Codex reset times
are preserved to the second.
The bridge is the only thing that decides an alert; clients just render what
they're told. Crossing 80%, 95% or 100% (exhausted) on either window raises an
event, and so does a window resetting. One crossing produces exactly one event: it re-arms only
when the window rolls over or utilization falls -hysteresis points back below
the line, and the arming state is persisted, so restarting the bridge doesn't
replay history.
./aiusage-server -serve :8765 -thresholds 80,95,100 -hysteresis 5Cost is deliberately not alertable - it's an independent estimate of value, not a bill, and a "you've spent $X" notification would imply a charge that isn't happening.
Typing a 32-character token on a watch is not a plan. Instead:
./aiusage-server -pair # asks the running bridge for a code, prints itThe server prints a 6-digit
code, good for one use and two minutes; the device posts it to /pair and gets
back its own token. Ten wrong codes in a row burn every pending code, so the
6-digit space can't be walked.
Codes are minted only over a Unix socket ($XDG_RUNTIME_DIR/aiusagebar.sock,
mode 0600), never over HTTP. A tunnel or reverse proxy on this machine delivers
internet traffic from 127.0.0.1, so a "loopback only" HTTP endpoint would let
anyone who can reach the tunnel mint a code. The socket is reachable only by
your own user. Pairing one device closes the bridge: once any device is
paired, unauthenticated requests get a 401, and each device's token can be
revoked on its own by deleting it from the state file.
State - device tokens, the event ring, alert arming - lives in
$XDG_STATE_HOME/aiusagebar/state.json (mode 0600), usually
~/.local/state/aiusagebar/state.json.
The bridge itself is plain HTTP with no transport security. On the LAN, keep it to a trusted network. To reach it from anywhere, put TLS in front with a tunnel:
Either tunnel works; the bridge doesn't care which one is in front of it. Run it
with -public-url set to the tunnel's address either way.
Free, with a stable https://<machine>.<tailnet>.ts.net address and TLS
included. Funnel exposes only the port you give it, not the rest of the machine
or your tailnet, and clients don't need Tailscale installed.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale funnel --bg 8765 # prints the public URL; persists across reboots
./aiusage-server -serve :8765 -public-url https://<machine>.<tailnet>.ts.netsudo tailscale funnel reset turns it off. Funnel forwards every path, which
is fine: /pair/new doesn't exist on TCP, and everything else needs a token.
A Cloudflare named tunnel
gives the bridge a stable https:// hostname without opening a port on your
router. Cloudflare terminates TLS; cloudflared on this machine forwards to the
bridge on loopback. It needs a Cloudflare account and a domain on it. The free
"quick tunnel" (trycloudflare.com) won't do: its hostname changes every
restart, and the watch needs one that stays put.
The quick way, which does everything below (login, tunnel, DNS, a config that only forwards the bridge's routes, a user-level systemd service, and server service):
scripts/setup-tunnel.sh usage.example.comOr by hand (usage.example.com is your hostname):
cloudflared tunnel login # opens a browser, picks the domain
cloudflared tunnel create aiusage # prints the tunnel UUID
cloudflared tunnel route dns aiusage usage.example.com~/.cloudflared/config.yml:
tunnel: <UUID>
credentials-file: /home/<you>/.cloudflared/<UUID>.json
ingress:
# Only the bridge's public routes cross the tunnel; everything else 404s.
- hostname: usage.example.com
path: ^/(usage|events|pair|share|healthz)$
service: http://127.0.0.1:8765
- service: http_status:404Then run both:
cloudflared tunnel run aiusage
./aiusage-server -serve :8765 -public-url https://usage.example.com-public-url does two things:
- Every request needs a credential from the start. Without it, an unpaired
bridge answers anyone; with it, the internet can reach the port, so
/usageand/eventsreturn401until a device has paired. - It's handed to devices when they pair. A watch pairs once on home Wi-Fi,
where it finds the bridge over mDNS, and the
/pairresponse tells it the public URL to use from then on. Nobody types a hostname on a watch.
Send the token as Authorization: Bearer …, not ?token=: query strings end up
in proxy logs. Keep -serve :8765 (all interfaces) if you want LAN pairing; bind
127.0.0.1:8765 instead to make the tunnel the only way in, at the cost of
pairing through the public URL.
cloudflared service install runs the tunnel at boot (see Cloudflare's docs; it
reads /etc/cloudflared/config.yml).
Every HTTPS certificate is published in Certificate Transparency logs, and scanners watch them, so a public hostname is found within minutes however random it is. The token is what protects the bridge. It's invisible in use, because a client pairs once and sends the token from then on.
-share opens one read-only route for the public: your remaining session and
weekly allowance, for a personal site, a status page, a README badge service.
./aiusage-server -serve :8765 -public-url https://usage.example.com -share https://you.example
./aiusage-server -serve :8765 -public-url https://usage.example.com -share '*' # any sitecurl https://usage.example.com/share{
"schema": 1,
"five_hour": {"utilization": 53, "remaining": 47, "resets_at": "2026-09-21T18:20:00Z", "resets_in_sec": 16631},
"seven_day": {"utilization": 41, "remaining": 59, "resets_at": "2026-09-27T00:00:00Z", "resets_in_sec": 469031},
"cache_fetched_at": "2026-09-21T13:43:12Z",
"generated_at": "2026-09-21T13:43:29Z"
}- It needs no token, even on a bridge that is closed by
-public-url, and it is the only route that doesn't. Cost and hostname are left out. - Browsers can read it only from the origins you list;
*allows any. Origins match exactly (https://you.example, no path). - It serves the last 30-second sample and never runs the CLI refresh, so
page views can't make your machine spawn anything. The numbers move
whenever Claude Code fetches its limits, which is often while you work.
When
resets_atis in the past, that window has reset: show it as full. - It's sent with
Cache-Control: public, max-age=30, so a CDN in front can soak up traffic.
In the page:
const u = await (await fetch('https://usage.example.com/share')).json()
const left = (w) => (new Date(w.resets_at) < new Date() ? 100 : w.remaining)
console.log(`${left(u.five_hour)}% of this session left, ${left(u.seven_day)}% of the week`)Anything can read the bridge, not just the watch. Here's the whole integration:
- Find it. On the LAN, browse mDNS for
_aiusage._tcp, or use the host's address on port 8765. From anywhere, use the public URL. - Pair once. Run
aiusage-server -pairon the server machine to get a 6-digit code, then:Store the token. If you paired on the LAN, switch tocurl -X POST https://<public-url>/pair -d '{"code":"123456","name":"my-thing"}' # → {"token":"…","device_id":"…","host":"pop-os","public_url":"https://…"}
public_urlfor later requests. - Read. Send
Authorization: Bearer <token>on every request, never?token=. PollGET /usagewithIf-None-Match: <last ETag>: a304means nothing changed. For notifications, pollGET /events?since=<high_water>and deduplicate bydedupe_key. The bridge is the only thing that decides an alert, so don't re-derive thresholds in your client. - Handle
401as "pair again"; your device was revoked or the state was reset. UseGET /healthz(no auth) to check the bridge is up.
Rules of thumb:
- Date your data from when you fetched it, not from
generated_at; or better, fromcache_fetched_atwhen it's there. - Don't add a refresh button that does anything but
GET /usage: that request already refreshes. - Take reset times only from
resets_at. - Never label the cost numbers as a bill.
Each client gets its own token. Revoke one by deleting its entry from
state.json and restarting the bridge.
PolyForm Noncommercial 1.0.0. It covers everything here,
including the Wear OS app in wearos/, the Android widgets in
android/ and the iOS widgets in ios/. You may use, change and share it for
any noncommercial purpose: personal use, research, hobby projects, and
charitable, educational or public-interest organisations. Commercial use needs
permission from the copyright holder.
Install the server and enable its user service:
mkdir -p ~/.local/bin ~/.config/systemd/user
install -m 755 aiusage-server ~/.local/bin/aiusage-server
cp deploy/aiusage-server.service ~/.config/systemd/user/
# Before starting, edit ExecStart for any public-tunnel/provider-path flags.
systemctl --user daemon-reload
systemctl --user enable --now aiusage-server.serviceSet -public-url before starting the service if a public tunnel forwards to it.
Keep deployment URLs and tokens in your local configuration, outside this repository.
journalctl --user -u aiusage-server.service shows server logs; restarting the
tray has no effect on that service. To keep the server running after logout,
enable user lingering with loginctl enable-linger "$USER" if desired.
After pairing the tray, install its autostart entry:
mkdir -p ~/.config/autostart
cp aiusagebar.desktop ~/.config/autostart/Edit the desktop entry's Exec= to the installed tray binary's absolute path.
It should launch only aiusagebar, with no old server flags.










