Self-hosted Brain core: a FastAPI backend backed by SQLite plus a Telegram bot. Runs well on a Raspberry Pi via Docker Compose.
- FastAPI backend with a simple SQLite schema.
- Telegram bot for finance tracking.
- Mood/Energy daily check-in with a low-friction flow and persistent storage in Brain.
- Optional daily digest message.
- Weekly mood digest on Telegram.
brain_api/: FastAPI app + SQLite (brain_api/data/brain.db).telegram_bot/: Telegram bot that calls Brain via HTTP withX-BRAIN-TOKEN.- Brain is the source of truth; the bot never stores final data.
- Docker + Docker Compose
- Telegram bot token (BotFather)
- Shared token for Brain API auth
cp .env.example .env
# edit .env
docker compose up --buildServices:
brain-apionhttp://localhost:8000telegram-botruns in the same network and talks tobrain-api
Copy .env.example to .env and set:
Core:
BRAIN_SHARED_TOKEN(required)BRAIN_DB_PATH(default/app/data/brain.db)BRAIN_URL(defaulthttp://brain-api:8000in compose)TZ(defaultEurope/Rome)
Telegram bot:
TELEGRAM_BOT_TOKEN(required)BRAIN_TELEGRAM_KEY(optional, defaults toBRAIN_SHARED_TOKEN)BRAIN_BOT_RATE_LIMIT_SECONDS(default1.0)
Digest:
DIGEST_ENABLED(defaultfalse)DIGEST_TIME(default08:00)DIGEST_CHAT_IDS(comma-separated chat IDs)WEEKLY_DIGEST_ENABLED(defaulttrue)WEEKLY_DIGEST_TIME(default20:00)WEEKLY_DIGEST_WEEKDAY(defaultsun, accepts0-6where6is Sunday)
Mood check-in:
MOOD_TIME(default21:30, Europe/Rome)- Uses
DIGEST_CHAT_IDSfor target chats BRAIN_BOT_DB_PATH(default/app/data/bot.db) for bot outbox retry
Auth: X-BRAIN-TOKEN header on protected routes.
GET /healthPOST /finance/expensePOST /finance/expense/telegram(auth withX-TELEGRAM-KEY)GET /finance/summaryPOST /finance/bulk_importGET /finance/export?since=ISO8601|YYYY-MM-DDPOST /ingestPOST /mood/checkinGET /mood/lastGET /mood/week?days=7
Notes:
tsaccepts ISO8601 datetime orYYYY-MM-DDdate.amountsupports positive/negative; zero is rejected.
{
"ts": "2026-01-09T07:18:04.076245+00:00",
"local_date": "2026-01-09",
"slot": "evening",
"energy_level": -2,
"mood_score": -1,
"mood_text": "optional",
"did_thing": "optional",
"waste_spend": true
}Commands:
/start,/help/spesa <importo> <nota opzionale>/categoria <nome>/oggi/mese/moodstarts a check-in/skipcancels the current check-in/mood_lastshows the last check-in/mood_weekshows the last 7 days summary
- Energy: keyboard
-2 -1 0 +1 +2 - Mood: keyboard
-3 -2 -1 0 +1 +2 +3 - Mood text (optional, or "Salta")
- Did thing (optional, or "Salta")
- Waste spend: keyboard
Sì/No
If numeric input is invalid, the bot replies with one line and re-sends the keyboard.
- Daily prompt at
MOOD_TIME(default 21:30 Europe/Rome) for chats inDIGEST_CHAT_IDS. - The prompt runs only if the chat is IDLE (no open check-in).
- Open check-ins expire after ~6 hours with no message sent.
- Weekly mood digest at Sunday 20:00 Europe/Rome (see
WEEKLY_DIGEST_*).
- Pull with
GET /finance/export?since=..., then push withPOST /finance/bulk_import. - Deduplication uses a unique key on
(ts, amount_cents, note, category, source); replaying is safe.
docker compose up --buildcurl http://localhost:8000/health- Use Telegram
/spesa 10 caffè curl -H "X-BRAIN-TOKEN: $BRAIN_SHARED_TOKEN" http://localhost:8000/finance/summary
Run a quick syntax check for the API modules:
make py-compile- or
./scripts/py_compile_check.sh
Install dev deps and run pytest:
pip install -r requirements-dev.txtpytest -q
- No mood check-ins in DB: finish the flow; check bot logs for outbox queue.
- To query the DB inside the container:
docker compose exec -T brain-api python - <<'PY'
import sqlite3
conn = sqlite3.connect("/app/data/brain.db")
rows = conn.execute("SELECT id, ts_utc, local_date, slot, energy_level, mood_score, mood_text, did_thing, waste_spend, created_at FROM mood_checkins ORDER BY id DESC LIMIT 10;").fetchall()
for r in rows:
print(r)
PY- Use a 64-bit Raspberry Pi OS with Docker installed.
- Persist
brain_api/datafor SQLite durability. restart: unless-stoppedis already in compose.