Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ bin/
node_modules/
*.log
host.json
auth.json
accounts/
.env
.env.*
!.env.example
Expand Down
129 changes: 111 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ Open:
- Full data: `http://127.0.0.1:8787/api/usage`
- ESP32 payload: `http://127.0.0.1:8787/api/device`

When multiple accounts are registered, the dashboard shows the selected account
identity (full email by default or explicit `-Label`), profile ID, and `n/N` position. Use its previous/next buttons to change the
displayed account; refreshes preserve the selection by stable profile ID.

Once verified, stop it with `Ctrl+C`, then listen on the LAN:

```powershell
Expand All @@ -294,6 +298,51 @@ powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 -Bind 0.0.0.0
If Windows Firewall prompts, allow access only on **Private networks**. Do not expose
port 8787 to the public internet or an untrusted network.

### 👥 Add isolated accounts

The host can poll up to four explicitly registered accounts. Register each account in
its own `CODEX_HOME` instead of copying the active global Codex login or sharing one
`auth.json` between account workers:

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\add-account.ps1 `
-Name personal -Label "PERSONAL" -PlanLabel "PRO 20X"
```

`-Name` is the required 1–32-character stable ASCII registry slug and must not be
an email address.
Optional `-Label` is a 1–32-character printable ASCII privacy-safe alias that overrides
the display identity. If it is omitted, the host uses the complete email returned by
`account/read`, for example `user@example.com`. Optional `-PlanLabel`
overrides only that account's plan label. By default the profile is stored under
`%LOCALAPPDATA%\CodexUsageMeter\accounts\<Name>\`, with metadata in `profile.json`
and credentials in its isolated `codex-home\`.

The registration flow runs the official `codex login` scoped to that new account
home. Complete it with the intended account; the script does **not** copy the global
`CODEX_HOME`, the ChatGPT desktop application's authentication files, or a different
account's credentials. Repeat the command with another unique `-Name` to add an
account. Use a short alias such as `PERSONAL`, `WORK`, or `TEAM` when you do not want
the default full email displayed on the device. Authentication tokens, cookies, and
auth-file contents are never used as labels or sent to the device. When the account registry contains
no explicit profile directory, the host preserves backward compatibility by reading
the normal default `CODEX_HOME` as one account. A present but invalid explicit profile
fails closed and is reported in diagnostics instead of silently using global credentials.

The service watches the account registry and isolated account homes. A completed
registration or a later login change is loaded without merging credentials into the
service's own state. The daemon keeps one app-server process per account, polls the
accounts serially under one global request schedule, and applies failure backoff per
account. Initial or changed profiles are warmed serially; in steady state, each global
scheduling tick polls at most one due account in round-robin order. Adding accounts
therefore keeps the steady-state total poll rate at at most one account per tick;
instead, each account refreshes less often (roughly every N ticks for N accounts).
Only startup, registration, or credential changes add a bounded serial warm-up.
OpenAI has not published a
multi-account polling cadence that is guaranteed to avoid rate limits or abuse
safeguards; use only accounts you are authorized to access and choose a conservative
interval.

### 📡 Choose Wi-Fi, BLE, or both

The host script supports four modes:
Expand Down Expand Up @@ -378,7 +427,9 @@ powershell -ExecutionPolicy Bypass -File .\scripts\start-host.ps1 `
Labels are limited to 20 ASCII characters, must begin and end with a letter or digit,
may contain letters, digits, spaces, `+`, `_`, and `-` in between, and are normalized
to uppercase before transmission. Automatic 5x/20x detection should be added only if
app-server eventually returns a dependable entitlement field.
app-server eventually returns a dependable entitlement field. In multi-account mode,
the `-PlanLabel` passed to `add-account.ps1` belongs only to that account and takes
precedence over the host-wide fallback.

### 🔐 Host identity and transport security

Expand Down Expand Up @@ -515,6 +566,19 @@ A lightning symbol means charging, `USB` means external power is available witho
battery, and an unavailable reading—or no valid sample for more than six sampling
periods—displays `--%` rather than misreporting a communication error as 0%.

Both pages show the account identity below the page title: the complete email from
`account/read` by default, or the explicit privacy-safe `-Label` override.
With multiple accounts, the label includes its position such as `WORK | 2/3`. Short-press
GPIO18/the onboard Key3 to select the next account; long-press it to select the previous
account. With only one account, the position is omitted and both actions are ignored.

Do not use the onboard Key2 as the previous-account button. On this reference board it
mechanically pulls both GPIO0 and `CHIP_PU` low, so pressing it resets the ESP32-S3 and
may enter the download path. The optional
`CONFIG_METER_EXTERNAL_PREVIOUS_BUTTON_GPIO0` input is disabled by default and is only
for a separate external button wired directly between GPIO0 and ground; firmware does
not initialize GPIO0 as a runtime button unless that option is enabled.

After 60 seconds without touch or detected movement, brightness drops from 65% to 30% by
default. The QMI8658 uses all three acceleration axes, an adaptive gravity baseline, and
an 80 mg default threshold. Moving a dimmed device restores active brightness; the very
Expand Down Expand Up @@ -745,22 +809,48 @@ verification looks like this (the data payload version remains 1):
"plan": "pro",
"planLabel": "PRO 20X",
"preferred": {
"id": "codex",
"name": null,
"primary": {
"used": 31,
"remaining": 69,
"windowMins": 10080,
"resetsAt": 1785813196,
"resetsIn": 529996
},
"secondary": null
}
},
"extras": []
"extras": [],
"accounts": [
{
"id": "personal",
"label": "user@example.com",
"status": "ok",
"capturedAt": 1785283200,
"planLabel": "PRO 20X",
"preferred": {
"primary": {
"remaining": 69,
"windowMins": 10080,
"resetsIn": 529996
}
},
"extras": []
},
{
"id": "work",
"label": "WORK",
"status": "ok",
"capturedAt": 1785283140,
"planLabel": "PLUS",
"preferred": null,
"extras": []
}
]
}
```

The host calculates `resetsIn`, so the ESP32 can show a reset countdown without NTP.
The top-level quota fields mirror the first healthy account with renderable cached
usage for compatibility; current firmware reads the complete `accounts` array, which
is capped at four. Top-level `capturedAt` is the newest safe poll/account generation so
updates to any account and registry removals are transported monotonically. The host
calculates `resetsIn`, so the ESP32 can show a reset countdown without NTP.
By default, the ESP32 reads the LAN cache every five seconds while the Windows host
requests a new quota from its local Codex runtime every 60 seconds. The two cadences do
not conflict. The UI does not repaint a host countdown every second, avoiding needless
Expand All @@ -779,19 +869,21 @@ documented in [`docs/transport.md`](docs/transport.md).

## ⏱️ Polling cadence and rate-limit risk

Three separate cadences are involved, and only the host app-server poll touches the
local Codex runtime. The host reads immediately at startup. After success, the next read
uses the configured interval—60 seconds by default, allowed range 60 seconds to 24
hours—plus 0–5 seconds of random jitter. Under the default configuration, consecutive
failures back off for 2 / 4 / 8 / 15 minutes and remain capped at 15 minutes. The
ESP32's five-second Wi-Fi pull and the BLE helper's two-second check only read this
local cache; neither adds an OpenAI request.
Three separate cadences are involved, and only the host app-server scheduler touches
the local Codex runtimes. Initial, new, and changed profiles are read serially. In
steady state, the configured 60-second default interval is one global tick and each
tick polls at most one due account in round-robin order. Consecutive failures back off
per account for 2 / 4 / 8 / 15 minutes by default, so one failed login does not create
parallel retries or block healthy accounts. The ESP32's five-second Wi-Fi pull and the
BLE helper's two-second check only read the local list cache; neither adds an OpenAI
request.

`account/rateLimits/read` is a documented local Codex app-server RPC, not a standalone
public REST quota API for ChatGPT users. The current app-server implementation fetches
primary usage and detailed reset-credit information in parallel, so one local RPC
should not be assumed to equal exactly one backend HTTP request. OpenAI has not
published a safe periodic-polling cadence or a guarantee against rate limiting. The
published a safe periodic or multi-account polling cadence, nor a guarantee against
rate limiting or other abuse safeguards. The
OpenAI Pro usage guidance also notes that abusive automated or programmatic extraction
may trigger abuse safeguards; this project's personal quota display has no official
exception or safety guarantee.
Expand All @@ -803,8 +895,9 @@ backs off after every failed read. Clawdmeter's
[PR #29 discussion](https://github.com/HermannBjorgvin/Clawdmeter/pull/29#issuecomment-4529772728)
contains a public rate-limit report for another API-query approach, later traced to
[five-second retries amplifying the problem](https://github.com/HermannBjorgvin/Clawdmeter/pull/29#issuecomment-4529796805).
No public account-ban evidence was found. That does not reveal the threshold of the
OpenAI RPC, and neither project provides an official guarantee that a cadence is safe.
No public account-ban evidence was found. That does not reveal a safe threshold for
the OpenAI RPC or multiple accounts, and neither project provides an official safety
guarantee.

Firmware separates the UI data model from the Wi-Fi/HTTP and BLE GATT implementations.
A future USB CDC transport only needs to supply the same snapshot/link callbacks; it
Expand Down
Loading