This is a self-hosted Python 3.11+ monitor for the subscription rate limits and reset times exposed by the local Codex app-server. It sends a Telegram GIF only after an early reset is confirmed by two equivalent polls.
The Platform Usage/Costs Admin API is not a source for Codex subscription
limits. The monitor does not use an Admin API key, private HTTP endpoint, or
OAuth token. It starts the installed codex CLI as the authenticated service
user and asks its local app-server for account/rateLimits/read. The existing
Codex login/auth source must therefore be available under the configured
CODEX_HOME on the host running the service. Credentials are never placed in
source, fixtures, SQLite, logs, or examples.
Each successful response is normalized into <limit_id>:primary and
<limit_id>:secondary windows. The monitor stores normalized snapshots so it
can compare the last advertised reset time with the next sample. Raw JSON,
auth material, and unknown provider fields are discarded.
- The first sample establishes a baseline and cannot alert.
- A scheduled/ordinary reset is ignored when it happens at or after the prior advertised reset, within the configured skew, or when an early reset cannot be proven.
- A manual reset is ignored when
rateLimitResetCredits.availableCountdecreases.--ignore-next-resetand the restricted Telegram command are the fallback when credit data is unavailable. - An early reset requires a material used-percent drop and remaining-percent rise, a forward reset-window change, known reset-credit data, and two consecutive observations of the same window and previous advertised reset. The provider may adjust the newly advertised reset time between those observations. Primary and secondary anomalies in the same incident are grouped into one alert.
- A pending candidate is cancelled by an ordinary, manual, unknown, or materially different observation. Repeated polls deduplicate one incident; a later reset with a new baseline advertised reset or window identity is a new incident.
The normalized snapshot remains in SQLite after an ordinary or manual reset so
the next comparison has a correct baseline. Those resets do not create rows in
reset_events and do not send Telegram alerts.
Runtime dependencies are Python standard library modules only. A virtualenv is optional for local use and is used by the deployment script.
python3.11 -m venv .venv
. .venv/bin/activate
python -m pip install --no-deps -e .
cp .env.example .env.private
chmod 600 .env.privateEdit .env.private with a text editor. It contains placeholders only until you
replace them locally; never commit it. The file is not auto-loaded, so run the
monitor with an environment file explicitly, for example:
set -a
. ./.env.private
set +a
python -m quota_monitor --status
python -m quota_monitor --onceBefore --once, verify that the same user can run the installed Codex CLI and
that its authenticated login is under CODEX_HOME (or the CLI's normal default
home). Do not put a Codex OAuth token or Platform Admin key in the monitor env
file. The CODEX_BIN default is codex; CODEX_HOME and the provider timeout
are configurable.
Useful commands:
python -m quota_monitor --help
python -m quota_monitor --status
python -m quota_monitor --ignore-next-reset
python -m quota_monitor --once
python -m unittest discover -s tests -v--ignore-next-reset writes a durable one-shot marker and does not contact
Codex. It is consumed only when a material reset observation occurs.
Create a bot with BotFather. In standalone same-user mode, keep the private
environment file mode 0600. In systemd mode, use
/etc/quota-monitor/quota-monitor.env with owner root, group
quota-monitor, and mode 0640; mode 0600 would prevent the service user from
reading it. The default target is the documented placeholder
@codexresets; verify that this username is the actual channel destination, or
set TELEGRAM_CHAT_ID to the real channel/group ID. Do not infer a Bot API chat
ID from a screenshot or another peer identifier.
The bot must be added to the target channel/group with permission to post
animations. If command handling is enabled, set
TELEGRAM_ALLOWED_USER_IDS to the numeric IDs of trusted operators. An empty
allow-list is fail-closed, so arbitrary channel members cannot mark a reset
ignored. /ignore-next-reset and /status are fetched with optional
getUpdates polling; they are disabled by default.
TELEGRAM_ANIMATION_PATH can point to a private valid GIF. Without it, the
monitor decodes the bundled two-frame placeholder only when an alert is sent
and writes it to the state directory. Telegram request failures are logged as
redacted error codes, leave the event unsent, and are retried on later polls.
The supported layout uses a dedicated quota-monitor service user and keeps
both the SQLite state and the Codex auth home under /var/lib/quota-monitor.
This lets systemd grant the service user access to the login without running
the monitor as root.
-
Install Python 3.11+, the Codex CLI, and its documented app-server support on the VPS. Confirm the CLI version supports
app-server --stdioandaccount/rateLimits/read. -
Create or use a dedicated service account. Copy or establish the Codex login for that account using the Codex-supported login flow, with
CODEX_HOME=/var/lib/quota-monitor/codex-home. Do this interactively as the service user; do not pass a token on a command line or put it in a unit file. If the installed Codex auth model cannot safely be used by a dedicated service user, keep the auth home readable/writable only by the chosen service account and adjust the unit'sUser/Groupdeliberately; do not fall back to root credentials. -
Copy this repository to
/opt/quota-monitorand run the installer as root:sudo ./deploy/install.sh
The installer enforces the fixed paths used by the unit (
/opt/quota-monitor,/etc/quota-monitor,/var/lib/quota-monitor, and thequota-monitorservice account); non-default path overrides are rejected. It never takes secrets as command-line arguments or prints environment values. -
Create the private env file with an editor, then lock it down. The service group needs read access; the Bot token is still never passed as an argument:
sudo install -o root -g quota-monitor -m 0640 /dev/null /etc/quota-monitor/quota-monitor.env sudoedit /etc/quota-monitor/quota-monitor.env sudo systemctl restart quota-monitor
Use values equivalent to
.env.example, including:CODEX_HOME=/var/lib/quota-monitor/codex-homeandQUOTA_MONITOR_STATE_DIR=/var/lib/quota-monitor/state. -
Confirm the unit and state without exposing its environment:
sudo systemctl enable --now quota-monitor sudo systemctl status quota-monitor --no-pager sudo journalctl -u quota-monitor -n 100 --no-pager sudo -u quota-monitor sh -c 'set -a; . /etc/quota-monitor/quota-monitor.env; set +a; exec /opt/quota-monitor/.venv/bin/python -m quota_monitor --status' sudo -u quota-monitor sh -c 'set -a; . /etc/quota-monitor/quota-monitor.env; set +a; exec /opt/quota-monitor/.venv/bin/python -m quota_monitor --once' sudo -u quota-monitor sh -c 'set -a; . /etc/quota-monitor/quota-monitor.env; set +a; exec /opt/quota-monitor/.venv/bin/python -m quota_monitor --ignore-next-reset'
The unit is in `deploy/systemd/quota-monitor.service`. It uses
`ProtectSystem=strict`, `ProtectHome=read-only`, `PrivateTmp`, no new
privileges, a dedicated user, and a restricted writable state path. Review the
Codex CLI's auth refresh behavior before enabling stricter filesystem settings;
the service user's `CODEX_HOME` must remain writable if the CLI refreshes it.
## State and safe rotation
SQLite contains normalized samples, durable baseline/candidate/ignore state,
and only normalized unexpected-reset event fields. It must not contain a Bot
token, Admin key, OAuth token, or raw provider response. Stop the service (or
use SQLite's online backup facility) before copying the database. Back up the
database and deployment configuration separately; the bundled GIF can be
recreated and the Codex auth home is a separate sensitive credential store.
To rotate the Telegram token, use BotFather, edit the private environment file
with `sudoedit`, verify mode/ownership, and restart the unit. Do not include the
new token in shell history, `systemctl` arguments, journal output, issue
reports, or backups that do not need it. If the token is ever exposed, revoke
it before updating the file.
## Limitations and protocol compatibility
The provider intentionally isolates all app-server protocol assumptions in
`src/quota_monitor/provider.py`. It ignores JSONL notifications, matches JSON-
RPC response IDs, accepts the camelCase fields used by current app-server
responses plus documented snake-case equivalents, and raises a typed error for
timeouts, malformed output, missing windows, or an unavailable binary. If a
future Codex CLI changes the method or response shape, update that module and
its tests; do not add undocumented private HTTP calls or substitute the Admin
API. No network, Codex CLI, or Telegram service is required by the test suite.