- Python 50.4%
- HTML 24.6%
- CSS 19.6%
- JavaScript 5.2%
- Dockerfile 0.2%
| app | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
Stub
Undeployed 2026-09-11. Not running on the VPS. Code and Forgejo
bunko/stubare kept; the container, Caddy/stubroute, dashboard tile, and n8n Stub Inbox workflow were removed. SQLite data remains in./data.
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.
Inbox pipeline (receipts → review → statement)
n8n watches your billing inbox, forwards matching emails to Stub's internal API, AI extracts recurring charges, and you review them at bunko.me/stub/inbox before anything is saved. A ntfy ping links straight to the review screen.
- Set
STUB_INBOX_API_KEYin~/apps/stub/.env(and mirror it in~/apps/n8n/.env). - Import
~/apps/n8n/stub-inbox.jsonin the n8n UI and attach your IMAP credentials. - Restart both stacks:
docker compose up -dinstubandn8n.
Test without email: POST to the workflow's test webhook:
curl -sS -X POST 'https://n8n.bunko.me/webhook/stub-inbox-test' \
-H 'Content-Type: application/json' \
-d '{"text":"Your Netflix subscription was charged $15.49 on June 14. Next billing: July 14, 2026.","source_ref":"Netflix receipt (test)"}'
Internal API (used by n8n on the Docker web network):
curl -sS -X POST 'http://stub:8000/api/inbox' \
-H 'X-Api-Key: $STUB_INBOX_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"text":"...","source":"email","source_ref":"Subject · sender","kind":"auto","notify":true}'
kind: auto (default), receipt (single billing email), statement (multi-line
statement), or data (CSV/JSON — skips AI).
Routes
GET /— statement dashboardGET /inbox— pending receipt/email draftsGET /inbox/{id}— review one inbox itemPOST /inbox/{id}/accept— add selected candidates to the statementPOST /inbox/{id}/dismiss— discardPOST /api/inbox— ingest (API key; n8n only)GET /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