An OpenRouter-backed Discord bot — the spiritual successor to
roger-bot, my first-ever programming project. Same
character, rebuilt from scratch on hosted models and modern tooling.
Roger is a single-guild, owner-gated Discord assistant with four separate "brains":
- Admin — an owner-only server concierge, reached through
/rogerin the guild. Ask in plain language ("a read-only podcast channel under Media that DJs can post in") and it creates channels/roles and sets permissions through a small, hand-rolled tool loop, with short per-channel conversation memory so follow-ups have context. No agent framework. - Ambient — a deadpan chat persona (via
/chat, any @mention, or any DM). No tools, no authority. - Curated — a daily check of Scout's picks. It posts one
source-grounded story when there is something worth sharing, and skips quiet days. Public posting
stays off until
CURATED_CHANNEL_IDis set. The owner can preview a draft through/roger. - Giga Brain — an owner-only, read-only strategic-analysis mode, reachable by
/gigabrain. It reviews live server state and reasons about it — never acts. Optionally also checks in on its own on a configurable interval, DMing the owner unprompted suggestions.
Design details — the routing table, the tool loop, the store schema, and the security invariants the
source cites as (§N) — are in ARCHITECTURE.md.
Security is structural, not prompt-deep:
- No privileged gateway intents.
message_content,members, andpresencestay off. Roger only sees content in DMs, @mentions, and its own messages — exactly what it needs. - Owner-gated. Admin actions require
user.id == OWNER_ID, checked before a single token is spent. Everyone else gets a canned reply and an audit row. - Least privilege. Never requests Administrator. Roles it creates always have zero permissions; access is granted through channel overwrites. No delete, kick, ban, or purge tools exist — Roger creates and adjusts, never destroys, and every change to existing state is owner-confirmed.
- Budgeted. Per-brain daily token caps and a hard cap on tool calls per request.
- Untrusted item output is bounded. Curated drafts use bounded Scout article evidence, require supporting excerpts, accept only HTTP(S) item links, and suppress Discord mentions on delivery.
- No secrets in git — ever, not even encrypted. Secrets live in a
sops+age-encryptedroger.envon the host; the repo carries only.sops.yamlandroger.env.example. - Signed, scanned supply chain. CI audits deps (
pip-audit) and scans the image (Trivy, build → scan → push); releases are signed keyless with cosign and the deploy host verifies the signature before it runs — a bad or unsigned image fails the deploy closed.
Python 3.12 · discord.py · the OpenAI SDK pointed at
OpenRouter · pydantic · aiosqlite. Runs as a non-root, read-only-rootfs
container. OPENROUTER_BASE_URL is config, so pointing Roger at a local inference host is an env
change rather than a rewrite. That was tested and shelved on cost and hardware grounds, so hosted is
the only supported setup today: see
ADR-0009.
cp roger.env.example roger.env # fill in tokens, owner/guild IDs, model chainsEach MODEL_* var is a comma-separated priority list (primary first, the rest are OpenRouter
fallbacks). Every model in MODEL_ADMIN must support tool calling.
CI/CD is pull-based: pushing to main runs the tests, builds the image, and publishes it to
GHCR (ghcr.io/r055le/roger:main); the host polls that tag and redeploys itself. Secrets are
injected at runtime by sops exec-env and never baked into the image. Full runbook (host
bootstrap, age key, systemd timer) in deploy/.
Run it directly on any Docker host:
sops -e -i roger.env # encrypt at rest (age key — see .sops.yaml)
mkdir -p data
sops exec-env roger.env 'docker compose up -d' # pulls the published imageRoger exposes Prometheus metrics on port 9108 inside the container by default; set
METRICS_PORT=0 to disable the server. Compose publishes it as
127.0.0.1:${METRICS_HOST_PORT:-9108} by default. Set METRICS_BIND_ADDRESS only when a scraper on a
trusted private interface needs remote access. The endpoint has no authentication or TLS and must
not be exposed to an untrusted network.
Event counters — LLM requests, errors, and budget rejections — are incremented in process; state gauges — token/dollar spend, caps, and audit tallies — are refreshed from SQLite so they survive restarts. Key series:
| Metric | Type | Labels |
|---|---|---|
roger_tokens_today / roger_tokens_cap |
gauge | brain |
roger_cost_usd_today / roger_cost_usd_cap |
gauge | brain |
roger_llm_requests_total / roger_llm_errors_total |
counter | brain (type) |
roger_llm_budget_exceeded_total |
counter | brain, reason |
roger_audit_events |
gauge | tool, status |
roger_build_info |
gauge | — (version) |
A ready-to-merge Prometheus scrape job and an importable Grafana dashboard live in
deploy/observability/ — the bridge to the
sre-observability-lab.
python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'
pytest
ruff check .Feature-complete across the planned phases:
- Admin — owner-gated via
/rogerin the guild, with short per-channel conversation memory; a hand-rolled tool loop withlist_structure,create_channel,create_role, and confirm-gatedset_permissions/edit_channel/post_message/move_channel; per-request tool and daily token budgets; a full SQLite audit trail. - Ambient — deadpan chat via
/chat, any @mention, or any DM, rate-limited per user + globally, with a short own-thread memory. No tools, ever. - Curated — defaults to checking Scout's output at 07:00 local time and may post one useful
story with source detail and a light take. Set
CURATED_CHECK_INTERVAL_MINUTESto enable an ongoing stream, bounded byCURATED_MAX_POSTS_PER_DAY,CURATED_MIN_SPACING_MINUTES,CURATED_MAX_OBSERVATIONS_PER_DAY, and the existing model budgets. There is no posting quota. Quiet or rejected input is remembered; changed evidence or model policy allows reconsideration. Transient failures get one retry after an hour; budget deferrals get one on the next local day./statusshows remaining allowances, spacing, uncertain deliveries, and the last decisions. The owner can preview the next decision with/roger preview the curated post; that spends from the curated model budget but does not post or change Curated state. Ask forpreview_curatedwithdeveloping_stories=trueto inspect the bounded story-memory path. It follows confirmed sent reports across source updates, records explicit publish/update/combine/hold/skip decisions, and stays disabled untilCURATED_DEVELOPING_STORIES=true. Story drafts and the one allowed revision have a fixed 4,096 token output ceiling; legacy Curated drafts and reviews keep the 900-token ceiling, and every call spends from the same Curated daily token and dollar budgets. SetCURATED_CHANNEL_IDonly after reviewing preview quality. See ADR-0012 for the Scout boundary. Scout currently fetches only newly reported items; Scout #21 tracks same-page refresh evidence.
Runs as a non-root, read-only-rootfs container. More than 300 tests cover the guard rules, the tool loop (including channel creation with access presets and the confirm-gated edit, post, and reorder tools), the rate limiter, and the Scout ingestion path.
MIT — see LICENSE.