As an AI enthusiast I came across Hermann's original Clawdmeter and immediately wanted one on my desk. A small dashboard showing your live Claude Code usage — perfect.
Unfortunately, I ordered the wrong board. Hermann's project runs on the Waveshare ESP32-S3-Touch-AMOLED-2.16, and what I had was the ESP32-C6 version. Not the same thing — different CPU (RISC-V vs Xtensa), no PSRAM (just 512KB SRAM total), different display IC (SH8601 instead of CO5300), different pinout, different buttons.
Rather than send it back, I sat down with Claude Code and ported the whole firmware. What now works on the C6:
- SH8601 driver with the vendor init sequence (without this the panel powers on but stays dark).
- Per-strip QSPI push that doesn't confuse the panel (the CS-toggle bug is real —
draw16bitRGBBitmap()per strip blanks the screen on this revision). - Splash animation redesigned from a 460KB CPU-upscaled canvas to a 20×20 LVGL image with 24× zoom — 800 bytes total.
- Snapshot/screenshot command disabled (doesn't fit without PSRAM).
- Buttons re-mapped: BOOT (left), KEY2 (right), KEY1 (PWR via AXP).
- IMU axis transform because the QMI8658 is physically mounted at a different orientation on this board.
- Touch coordinates rotate with the screen so swipes keep working in any orientation.
Once the C6 port was working I kept adding to it. Saw AiMetr's multi-provider Electron dashboard and borrowed the same idea on the daemon side — so the kastje now isn't just a Claude meter anymore, it can rotate through Claude, OpenAI / Codex and DeepSeek on the same Usage screen, picking up rate-limit and spend data from each one's own API. Everything below documents the result.
The daemon can now poll Claude, OpenAI / Codex and DeepSeek in parallel. When more than one is enabled, the Usage screen rotates through them every ~10 seconds — a small provider tag (CLAUDE / OPENAI / DEEPSEEK) appears under the title so you know what you're looking at.
Each provider returns the same normalised shape (session %, secondary %, reset time, status, cost), but the underlying data path differs:
| Provider | Endpoint | What 's' / 'w' mean |
|---|---|---|
| Claude | POST /v1/messages (rate-limit headers) + GET /api/oauth/usage |
5-hour session % + 7-day weekly % |
| OpenAI | GET /v1/usage?date=YYYY-MM-DD (fallback to /v1/models headers) |
Today's tokens vs 10M daily org limit |
| DeepSeek | GET /user/balance |
Spent credits vs original credit |
Per-provider failures use exponential back-off ([10, 30, 60, 120, 300]s) so one broken API doesn't slow the others down. Disabled providers are simply skipped.
Configure via ~/.clawdmeter.json (copy from daemon/config.example.json):
{
"claude": { "enabled": true },
"openai": { "enabled": true, "api_key": "sk-..." },
"deepseek": { "enabled": true, "api_key": "..." },
"poll_interval": 60
}API keys can also come from OPENAI_API_KEY / DEEPSEEK_API_KEY env vars on the LaunchAgent plist. Claude still uses its OAuth token from the macOS Keychain — no API key needed.
The Details, Pomodoro and Splash screens stay Claude-only (Anthropic's extra_usage data is what powers them and there's no equivalent on the other providers).
Pattern ported from AiMetr — same provider abstraction, scoped to the three we use.
On the Usage screen, swipe right-to-left → you land on a new Details page that shows your month-to-date API spend (exactly what console.anthropic.com → Settings → Billing calls "Extra usage"). Includes your budget and a progress bar.
Data comes from Anthropic's OAuth usage endpoint (/api/oauth/usage) — same Bearer token Claude Code already has, no separate admin key needed. The daemon fetches it and pushes it to the device over BLE.
Swipe back left-to-right to return to the Usage screen.
Use the right side button to start a focus session:
| Button action | What happens |
|---|---|
| 1 tap | Shift+Tab (Claude Code mode toggle, same as before) |
| 2 taps in quick succession | 25-minute focus session |
| 3 taps | 60-minute session |
| 4 taps | 90-minute session |
Fullscreen arc countdown with MM:SS in the centre. When the timer expires: the panel flashes to maximum brightness, "Done!" appears, and the device automatically types /clear + Enter into Claude Code so you land in a fresh conversation. PWR button during the timer = cancel.
The splash Clawd watches your Extra-usage % and shifts mood as you burn through the budget:
| Spend | Clawd does |
|---|---|
| < 50% | Normal rate-driven animations (idle / working / dancing depending on activity) |
| 50–79% | Focus mode — work coding, work think, idle look around |
| 80–94% | Worried — expression surprise, expression wink, idle blink |
| 95%+ | Panic — frantic dance (djmix, bounce dj, …) |
Threshold transitions switch immediately rather than waiting for the next 20-second rotation, so the moment you cross 80% Clawd notices.
If the daemon ever reports st="limited" (Anthropic returned a rate-limit hit), the splash drops to the expression sleep animation — Clawd is knocked out until the status returns to allowed. A subtle "you've been throttled" cue rather than a popup.
The middle (PWR) button has three different actions depending on how you press it:
| Press pattern | What happens |
|---|---|
| Single short press | Cycle screens (Usage ↔ Details ↔ Bluetooth). On splash, advance to the next animation. On Pomodoro, cancel the timer. |
| Double-tap (2× quick) | Blackout the panel — backlight goes to 0. Wakes on any screen tap or on the next BLE data poll. Use it as "do not disturb" for the desk. |
| Long press (~1.5 s) | Theme switcher — flips between dark and light Anthropic palette. The device reboots once (~2 s) so every widget picks up the new colours cleanly. Setting is persisted in NVS. |
| Hold ~6 seconds | AXP2101 hardware power-off (unchanged, this is the PMU's own behaviour). |
All three features only work when the daemon is running and you're signed in to Claude — see below.
The daemon on your Mac reads your Claude Code OAuth token (from ~/.claude/.credentials.json or the macOS Keychain entry Claude Code-credentials) and polls the Anthropic API every 60 seconds for rate-limit and spend data. That data goes over BLE to the device.
First-time setup:
-
Make sure you're signed in to Claude Code on your Mac:
claude login
(Or via the Claude Code app — its installer walks you through the OAuth flow.)
-
Install the daemon:
cd Clawdmeter ./install-mac.shThis creates a Python venv with
bleakandhttpx, installs a LaunchAgent that auto-starts, and prompts for Bluetooth permission for Terminal. -
Pair the device:
- System Settings → Bluetooth → click Connect next to "Claude Controller".
- The daemon discovers it within ~30 seconds and starts sending data.
-
Verify it's working:
tail -F ~/Library/Logs/claude-usage-daemon.out.logOnce a minute you should see a line like:
Sending: {"v":2,"p":[{"id":"claude","ok":true,"s":12,"sr":155,"w":35,"wr":3015,"st":"allowed","eu":12.34,"em":50.0,"cu":"EUR"}]}With more providers enabled the
parray grows — one entry per provider. Per entry:s/sr= primary % + reset (minutes),w/wr= secondary if applicable,st= status,cost= USD spent (OpenAI/DeepSeek),eu/em/cu= Claude only.
Switching accounts? Sign out and back in via Claude Code (claude logout / claude login). The daemon picks up the new token on the next poll.
Reset cached BLE address (e.g. after flashing firmware to a different board):
rm ~/.config/claude-usage-monitor/ble-addressA small ESP32 dashboard I made for my desk to keep an eye on Claude Code usage.
It runs on a Waveshare ESP32-S3-Touch-AMOLED-2.16 and pairs with my laptop over Bluetooth, the splash screen plays pixel-art Clawd animations that get busier when your usage rate climbs. The two side buttons send Space and Shift+Tab over BLE HID for Claude Code's voice mode and mode-toggle shortcuts.
| Usage meter | Clawd animation screen |
|---|---|
![]() |
![]() |
The Clawd animations come from claudepix, @amaanbuilds's library of pixel-art Clawd sprites, check it out, it's lovely.
The device boots into the splash and stays there until you press the middle (PWR) button, which cycles between Usage and Bluetooth. Tap the screen anywhere (except the Reset zone on the Bluetooth screen) to flip back to the splash; tap again to dismiss it.
| Splash | Usage | Bluetooth |
|---|---|---|
![]() |
![]() |
![]() |
| Splash; touch-toggle anytime | Session and weekly utilization | Connection status and bond reset |
While the splash is up, the middle button cycles animations instead of screens. The firmware also auto-rotates every 20 s within the current usage-rate group, so a long stretch on the splash isn't just one Clawd on loop.
Two supported boards (both 480×480, same chassis, same peripherals):
- Waveshare ESP32-S3-Touch-AMOLED-2.16 — ESP32-S3R8 (dual-core Xtensa, 8MB PSRAM), CO5300 AMOLED, CST9220 touch, AXP2101 PMU, QMI8658 IMU
- Waveshare ESP32-C6-Touch-AMOLED-2.16 — ESP32-C6 (single-core RISC-V, no PSRAM, 8MB flash), SH8601 AMOLED, CST9217 touch, AXP2101 PMU, QMI8658 IMU. Cheaper, smaller power envelope; same firmware features.
Plus:
- USB-C cable for flashing firmware and charging
- 3.7V Li-Po battery (MX1.25 2-pin connector, optional)
firmware/platformio.ini defines two environments:
| Env | Board | Notes |
|---|---|---|
waveshare_amoled_216_s3 |
S3 2.16 | Original target. PSRAM-backed splash, screenshot QA cmd. |
waveshare_amoled_216_c6 |
C6 2.16 | SH8601 driver, vendor init, no-PSRAM splash, screenshot off. |
Build a specific board: pio run -d firmware -e waveshare_amoled_216_s3 (or _c6). The flash helper auto-picks the S3 env; for C6 use pio run -e waveshare_amoled_216_c6 -t upload --upload-port /dev/cu.usbmodem<XXXX> (find the port with ls /dev/cu.usbmodem*).
- Linux (tested on Ubuntu) or macOS
- PlatformIO CLI
- Linux:
curl,bluetoothctl,busctl(BlueZ Bluetooth stack) - macOS:
python3(the installer sets up a venv withbleakandhttpx) - Claude Code with an active subscription
The macOS host pieces — Python daemon, LaunchAgent, and flash helper — were ported by Chris Davidson (@lorddavidson). Thanks Chris!
./flash-mac.sh # auto-detects /dev/cu.usbmodem*
./flash-mac.sh /dev/cu.usbmodem1101 # or pass an explicit USB serial portAfter flashing, open System Settings → Bluetooth and click Connect next to "Clawdmeter". The daemon will discover it on its next scan (~30 s).
The daemon reads your Claude OAuth token from the macOS Keychain (service Claude Code-credentials), polls usage every 60 s, and pushes it to the display over BLE.
./install-mac.shThe installer creates a Python venv in daemon/.venv/, installs bleak and httpx, renders a LaunchAgent into ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist, and loads it. The first run is launched interactively so macOS prompts for Bluetooth permission.
Useful commands:
launchctl list | grep claude-usage # check it's running
tail -F ~/Library/Logs/claude-usage-daemon.out.log # live logs
launchctl unload ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist # stop
launchctl load -w ~/Library/LaunchAgents/com.user.claude-usage-daemon.plist # startcd firmware
pio run -t upload --upload-port /dev/ttyACM0After flashing, the device advertises as "Claudemeter". Pair it once:
# Scan for the device
bluetoothctl scan le
# When "Claude Controller" appears, pair and trust it
bluetoothctl pair F4:12:FA:C0:8F:E5 # use your device's MAC
bluetoothctl trust F4:12:FA:C0:8F:E5The MAC address is shown on the Bluetooth screen — press the middle (PWR) button to cycle to it.
The daemon polls your Claude usage every 60 seconds and sends it to the display over BLE.
./install.sh
systemctl --user start claude-usage-daemonCheck status: systemctl --user status claude-usage-daemon
View logs: journalctl --user -u claude-usage-daemon -f
- The daemon reads your Claude Code OAuth token from
~/.claude/.credentials.json. - It makes a minimal API call to
api.anthropic.com/v1/messages— one token of Haiku, basically free. - The usage numbers come straight out of the response headers (
anthropic-ratelimit-unified-5h-utilizationand friends). - The daemon connects to the ESP32 over BLE and writes a JSON payload to the GATT RX characteristic.
- The firmware parses it and updates the LVGL dashboard.
- The firmware also tracks the rate of change of session % over a 5-minute window and picks splash animations from the matching mood group.
- The two side buttons are independent of all of this — they send Space and Shift+Tab as BLE HID keyboard input to the paired host directly.
The board has three side buttons. Left and right do the same thing on every screen; the middle button is screen-aware.
| Button | S3 GPIO | C6 GPIO | Function |
|---|---|---|---|
| Left | GPIO 0 (BOOT) | GPIO 9 (BOOT) | Hold to send Space (Claude Code voice-mode push-to-talk) |
| Middle (PWR) | AXP2101 PKEY | AXP2101 PKEY | Cycle screens (Usage ↔ Bluetooth); on splash, cycle animations |
| Right | GPIO 18 | GPIO 10 (KEY2) | Press to send Shift+Tab (Claude Code mode toggle) |
Space and Shift+Tab go out as standard BLE HID keyboard reports, so they trigger in whatever window has focus on the paired host — not just Claude Code.
The C6 port keeps all features of the S3 build, but a few things behave differently because of the smaller chip and different display IC:
- No PSRAM. Total SRAM is ~512KB. The 480×480 splash canvas was replaced with a 20×20 RGB565 image that LVGL scales 24× at draw time (
lv_image_set_scale, antialias off). Saves ~460KB. - SH8601 vendor init. The C6 board uses the SH8601 display controller; the bare
Arduino_SH8601lib init leaves the panel dark.main.cppruns the Waveshare vendor init sequence (sh8601_vendor_init()) right aftergfx->begin(). - Streamed per-strip push. Per-strip
draw16bitRGBBitmapcalls blanked the SH8601 panel (CS toggles between calls confuse this panel revision). The flush path was rewritten tostartWrite → writeAddrWindow → writePixels → endWriteso each LVGL strip is one clean QSPI transaction. - No screenshot serial command. The host-side
./screenshot.shQA flow needs a 460KB framebuffer that doesn't fit in C6 SRAM — disabled on this build. The command returnsSCREENSHOT_UNAVAILABLE_NO_PSRAM. - USB Serial/JTAG only. No native USB OTG — the C6 build sets
ARDUINO_USB_MODE=1soSerialresolves toHWCDCSerial(without this SensorLib won't compile). - Bigger partition table. Firmware is ~1.4MB so the C6 env uses
huge_app.csv(3MB app slot, no OTA) on the 8MB flash.
The QMI8658 IMU is mounted at a different physical orientation on the C6 board. If the screen rotates the wrong way when you tilt the device, edit firmware/src/imu.cpp and change CLAWD_IMU_AXES to one of the 8 presets (0–7) listed in the comment block, or set -DCLAWD_IMU_AXES=<n> in platformio.ini. One of them will line up.
The device advertises a custom GATT service alongside the standard HID keyboard service:
| UUID | |
|---|---|
| Data Service | 4c41555a-4465-7669-6365-000000000001 |
| RX Characteristic (write) | 4c41555a-4465-7669-6365-000000000002 |
| TX Characteristic (notify) | 4c41555a-4465-7669-6365-000000000003 |
| HID Service | 00001812-0000-1000-8000-00805f9b34fb |
JSON payload format (written to RX):
{ "s": 45, "sr": 120, "w": 28, "wr": 7200, "st": "allowed", "ok": true }Fields: s = session %, sr = session reset (minutes), w = weekly %, wr = weekly reset (minutes), st = status, ok = success flag.
The firmware/src/font_*.c files are pre-compiled LVGL bitmap fonts.
npm install -g lv_font_convGenerate each one (one at a time — lv_font_conv doesn't like loop-driven invocations) with --no-compress (required for LVGL 9):
# Tiempos Text (titles, 56px)
lv_font_conv --font assets/TiemposText-400-Regular.otf -r 0x20-0x7E \
--size 56 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_tiempos_56.c --lv-include "lvgl.h"
# Styrene B (large numbers 48, panel labels 28, small text 24, minimal 20)
for size in 48 28 24 20; do
lv_font_conv --font assets/StyreneB-Regular.otf -r 0x20-0x7E \
--size $size --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_styrene_${size}.c --lv-include "lvgl.h"
done
# DejaVu Sans Mono (32px, with spinner Unicode chars)
lv_font_conv --font assets/DejaVuSansMono.ttf \
-r 0x20-0x7E,0xB7,0x2026,0x2722,0x2733,0x2736,0x273B,0x273D \
--size 32 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_mono_32.c --lv-include "lvgl.h"Important: lv_font_conv v1.5.3 outputs LVGL 8 format. Each generated file must be patched for LVGL 9 compatibility:
- Remove
#if LVGL_VERSION_MAJOR >= 8guards aroundfont_dscand the font struct - Remove the
.cachefield fromfont_dsc - Add
.release_glyph = NULL,.kerning = 0,.static_bitmap = 0to the font struct - Add
.fallback = NULL,.user_data = NULLto the font struct
Without these patches, fonts compile but render as invisible.
The UI uses a small set of Lucide icons (bluetooth + battery states) converted to RGB565 / RGB565A8 C arrays for LVGL.
node tools/png_to_lvgl.js assets/icon_bluetooth_48.png icon_bluetooth_data ICON_BLUETOOTH_WIDTH ICON_BLUETOOTH_HEIGHTDefault tint is white (0xFFFFFF); Lucide PNGs ship as black-on-transparent and would render invisible against the dark UI without it. Pass --no-tint for pre-coloured artwork like the logo. Battery icons use RGB565A8 (alpha plane) so they blend cleanly over the splash; the rest are baked RGB565 over the panel colour. Paste the converter output into firmware/src/icons.h.
The animations come from claudepix.vercel.app,
a library of Clawd sprites. tools/scrape_claudepix.js evaluates the
site's JavaScript in a Node VM to pull out frame data and palettes, then
tools/convert_to_c.js turns everything into RGB565 C arrays and writes
firmware/src/splash_animations.h.
To re-pull (e.g. when the source library updates):
node tools/scrape_claudepix.js
node tools/convert_to_c.js
pio run -d firmware -t uploadSee tools/README.md for details.
- Pixel-art Clawd animation by @amaanbuilds, sourced from claudepix.vercel.app. Frame data and palettes scraped + converted by the tooling in
tools/. - Lucide icon set (lucide.dev, MIT) for bluetooth and battery UI glyphs.
- Anthropic brand fonts (Tiempos Text, Styrene B) — see licensing warning below.
The software in this repository uses and adheres to the Anthropic brand guidelines and uses the same proprietary fonts that Anthropic has a license for but this software uses without permission as well as using assets from Anthropic such as the copyrighted Clawd mascot so even though the code in this repo is non-proprietary I will not license it myself under a copyleft license since this repo includes proprietary fonts and copyrighted assets. Please be aware of this if you fork or copy the code from this repo. You have been warned!




