Step down. Don't crash.
A Discord bot for the Conscious Biohacker who wants to reduce caffeine intake from yerba mate without triggering withdrawal symptoms — the headaches, brain fog, fatigue, and irritability that come from quitting cold turkey.
Caffeine withdrawal is a real pharmacological event. The standard literature (and lived experience) shows that abrupt reductions of more than ~25% of daily intake can trigger 2–9 days of unpleasant symptoms. The Conscious Biohacker approach is to taper gradually — small, regular drops your adenosine receptors can adapt to — instead of quitting outright.
This bot exists because doing that taper by hand is annoying:
- You have to compute a step-down schedule that never drops too fast.
- You have to remember your daily target every morning.
- You inevitably miss a day or drink extra, and the plan needs to recover gracefully.
- You want to look back and see whether you actually stuck to the plan.
The bot automates all four. It generates a personalized tapering plan from your current intake, pings you each morning at 6 AM Pacific with that day's target, lets you log actual consumption (including extras, slips, and adjustments), and recalibrates the plan when life happens.
Yerba mate at our reference café comes in two sizes:
| Size | Caffeine |
|---|---|
| Small | 115 mg |
| Large | 150 mg |
Starting from (L large + S small) drinks per day, the algorithm steps down by:
- Swap a large for a small (–35 mg per swap), repeating until all drinks are smalls.
- Drop the count — once at all smalls, replace
N smallwith(N–1) large, then repeat the swap loop. - Hold each level for 2 days so adenosine receptor density adjusts before the next drop.
- Merge tiny steps — if a transition would change caffeine by less than 35 mg, the previous level is overwritten so no step is imperceptible (and no day feels wasted).
- Stop at 115 mg (one small) — the floor below which we declare victory.
2025-11-09 0S + 3L = 450mg
2025-11-11 1S + 2L = 415mg (-35mg)
2025-11-13 2S + 1L = 380mg (-35mg)
2025-11-15 3S + 0L = 345mg (-35mg)
2025-11-17 0S + 2L = 300mg (-45mg)
...
2025-11-25 1S + 0L = 115mg (-35mg)
About 18 days from (0S + 3L) to a single small drink. Total caffeine load drops by ~75% with every step well below the withdrawal threshold.
The bot exposes its behavior entirely through Discord slash commands. Once invited to a server, the commands appear in the autocomplete drawer.
Generates and persists a fresh tapering plan starting today, replacing any future plan days.
Displays the remaining plan from today onward as a Discord embed.
Records actual consumption. date_str accepts today (default), yesterday, or YYYY-MM-DD. Future dates are rejected. Re-logging the same date overwrites the previous entry.
Increments today's actual log by one drink. Useful when you grab a "just one more" without wanting to retype the full count.
Recalibrates the plan based on your most recent actual consumption.
| Strategy | Behavior |
|---|---|
replan (default) |
Generate a new tapering schedule starting tomorrow from your most recent actual level. |
catchup |
Hold at the current actual level for as many days as needed, then rejoin the original plan when it dips down to that level. |
Use replan if you've truly fallen behind. Use catchup if you had a one-off slip and want to preserve the original schedule.
Shows today's planned vs actual consumption with the delta.
Shows planned vs actual for the past N weeks (default 4) as a one-row-per-day table.
Every day at 06:00 US/Pacific, the bot posts an embed to PING_CHANNEL_ID with the day's target consumption. If no plan exists, it nudges you to create one.
The codebase is small (~600 LOC) and deliberately split into single-purpose modules with a strict dependency graph:
bot.py ──────┐
├──► commands.py ──► planner.py (pure)
│ │
│ └──► db.py ──► PostgreSQL
│ │
│ └──► formatters.py
│
└──► db.py ──► PostgreSQL
└──► formatters.py
| Module | Responsibility | Side effects |
|---|---|---|
bot.py |
Discord client setup, scheduler, daily ping, cog loading | Network, time |
commands.py |
Slash command cog: parses interactions, orchestrates planner + db + formatters | Network, DB |
planner.py |
Pure-function tapering algorithm | None |
db.py |
PostgreSQL connection pool + plan/actuals persistence | DB |
formatters.py |
Discord Embed builders (tables, deltas, status) |
None |
main.py |
Original standalone TSV-printing script (kept for reference / cron use) | stdout |
The dependency graph is acyclic: planner and formatters are pure and free of I/O, db knows nothing about Discord, and commands is the only place where domain logic, persistence, and presentation meet.
Two tables, both keyed by date:
CREATE TABLE plan_days (
date DATE PRIMARY KEY,
small INTEGER NOT NULL,
large INTEGER NOT NULL,
total_mg INTEGER NOT NULL
);
CREATE TABLE actuals (
date DATE PRIMARY KEY,
small INTEGER NOT NULL,
large INTEGER NOT NULL,
total_mg INTEGER NOT NULL
);The same PlanDay dataclass is reused for both plan rows and actual rows, since the shape is identical. total_mg is denormalized so the database can be queried for trends without recomputing.
save_plan / replace_plan_from_date always treat the plan as append-from-cutoff: history before the cutoff is preserved so the /history command can show what was planned even after a /adjust.
Daily reminders run on APScheduler's AsyncIOScheduler co-located with the Discord event loop, with a CronTrigger pinned to US/Pacific and a 1-hour misfire_grace_time so a brief restart doesn't skip the morning ping.
| Choice | Why |
|---|---|
| Python 3.12 | Modern type syntax, dataclass(slots=True), no FFI complications. Pinned via runtime.txt for Heroku-style deploys. |
| discord.py 2.x | First-class slash command support via app_commands. Cog pattern keeps bot.py tiny. |
| APScheduler | Async-native scheduler that lives in the same event loop as the bot — no second process, no cron container. |
| PostgreSQL via psycopg2 | Persistent across restarts (Discord bots restart often). The SimpleConnectionPool keeps connection setup cheap without an ORM. |
| No ORM | Two tables, hand-written SQL, full control over upsert semantics with ON CONFLICT DO UPDATE. SQLAlchemy would be overkill. |
| Pure-function planner | planner.py has zero side effects → trivially unit-testable, deterministic, and reusable from the standalone main.py script. |
Dataclasses with slots=True |
Memory-efficient, immutable-feeling, free __init__/__repr__. |
| Embed-based output | Discord embeds render fixed-width code blocks reliably across desktop and mobile, which the table layouts in formatters.py exploit. |
python-dotenv |
Env-var configuration only — no config files, secrets stay out of the repo. |
The project enforces MAXIMUM QUALITY ENGINEERING standards (see CLAUDE.md for the full doctrine):
- Tests: pytest + hypothesis, ≥90% branch coverage required.
- Linting: ruff (E/W/F/I/N/UP/B/C4/SIM/TCH/RUF), pylint ≥9.0.
- Types: mypy strict mode.
- Formatting: black + isort.
- Security: bandit + pip-audit.
- Complexity: radon, cyclomatic complexity ≤10 per function.
All of these run via ./scripts/check-all.sh (Gate 1 of the Stay Green workflow). Never invoke them directly — the scripts ensure local and CI behave identically.
- Visit the Discord Developer Portal.
- Create an application → add a Bot.
- Enable the
applications.commandsscope. - Generate an invite URL and invite the bot to your server.
Anywhere you can get a connection string — Supabase, Heroku Postgres, Neon, or local postgres.
Create a .env file:
DISCORD_TOKEN=your-bot-token
PING_CHANNEL_ID=your-channel-id
DATABASE_URL=postgres://user:pass@host:5432/dbname
pip install -r requirements.txt
python bot.pyFor Heroku-style deploys the included Procfile runs the bot as a worker dyno.
yerba-mate/
├── bot.py # Discord client, scheduler, daily ping
├── commands.py # Slash command cog
├── planner.py # Pure-function reduction algorithm
├── db.py # PostgreSQL persistence
├── formatters.py # Discord embed builders
├── main.py # Standalone TSV-printing script (legacy / cron)
├── requirements.txt # Runtime deps
├── pyproject.toml # Tooling config (mypy / ruff / pytest / coverage)
├── Procfile # Heroku worker entrypoint
├── runtime.txt # Python 3.12.12
├── scripts/ # check-all.sh, test.sh, lint.sh, format.sh, security.sh
├── tests/ # unit / integration / property tests
└── CLAUDE.md # Engineering doctrine
- discord.py — Discord API wrapper with slash commands.
- APScheduler — async daily ping scheduler.
- psycopg2-binary — PostgreSQL driver.
- python-dotenv —
.envloader.
MIT