- Python 46.5%
- HTML 23.8%
- CSS 23.1%
- JavaScript 6.3%
- Dockerfile 0.3%
"What can I cancel to save the most?" was recommending the phone plan — the
biggest line item, but an essential bill, not a cut candidate. Now the model is
told to reason about what each entry IS and how cancellable it realistically is:
housing, utilities, phone/internet, and insurance are treated as essential bills
("shop for a cheaper plan", never "cancel"), and cut suggestions focus on
discretionary subscriptions ranked by savings, weighted toward unused/duplicated/
low-value ones. Essential-category entries are also flagged inline in the context
snapshot so the model has the signal directly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SpwvqyELkyDN4LaPfpZeH
|
||
|---|---|---|
| app | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
Stub
A subscription & bill statement for bunko.me — tracks every recurring charge, keeps the running monthly/annual burn, flags free trials before they convert, and pings you (ntfy + Discord) before each charge lands.
Lives at bunko.me/stub, SSO-gated.
What it does
- Statement dashboard — monthly burn (everything normalized to $/mo), annual total, what's left to post this month, the next charge, and a category breakdown.
- Renewal radar — a daily scan (08:00 CT) pushes to ntfy + Discord
NOTIFY_DAYSdays before each charge, plus the morning of. Idempotent: one push per charge. - Trial watch — trials ending soon are pushed before they convert to paid.
- Cost normalization — a $120/yr and an $11/mo plan are compared on equal footing.
- One-click cancel — store each service's manage/cancel link; it rides along on the row and in the push.
- What-if — tick rows in the ledger to model dropping them; the burn recomputes live.
- Variable bills — utilities/usage bills flagged with
~, counted as estimates.
Design
The "STUB" identity: a manila bill statement — postmark cancellation stamp over the next due date (the signature), perforated tear lines, OCR/mono numerals, condensed bureaucratic display type (Saira Condensed). Deliberately not a generic dashboard.
Stack
FastAPI + SQLite + Jinja2, one in-process asyncio loop for the daily radar scan. No external services beyond ntfy/Discord webhooks.
Configure
Copy .env.example to .env and fill in:
| var | meaning |
|---|---|
NTFY_URL |
base ntfy URL (e.g. https://ntfy.bunko.me) |
NTFY_TOPIC |
topic to publish to |
NTFY_TOKEN |
bearer token (if your ntfy requires auth) |
DISCORD_WEBHOOK |
Discord webhook URL (optional) |
Compose-level knobs: NOTIFY_HOUR (default 8), NOTIFY_DAYS (default 3),
STUB_TZ (default America/Chicago).
Use the Test radar button in the header to confirm both channels are wired.
Deploy
cd ~/apps/stub
docker compose up -d --build
Then add the Caddy route (see the proxy repo) and restart caddy.
Routes
GET /— statement dashboardGET /new,POST /new— add an entryGET/POST /{id}/edit— editPOST /{id}/status— active / trial / paused / cancelledPOST /{id}/paid— advance to the next cyclePOST /{id}/delete— removePOST /seed— load a sample statement (empty-state only)POST /api/test-notify— fire a test pushPOST /api/run-checks— run the radar scan nowGET /health— healthcheck