A privacy-oriented Telegram bot for managing personal subscription services. Runs on Cloudflare Workers, stores data in Cloudflare KV, encrypts sensitive payloads at the application layer, and hashes Telegram user IDs before using them in storage keys.
- Runtime: Cloudflare Workers (
nodejs_compat) - Language: TypeScript (strict)
- Telegram SDK: grammY + @grammyjs/conversations
- Storage: Cloudflare KV
- Validation: Zod
- Testing: Vitest
- Crypto: Web Crypto API (AES-GCM, HKDF, HMAC-SHA-256)
- Reports: SVG + resvg PNG rendering
src/
├── bot/ # Telegram bot setup, commands, conversations, callbacks, keyboards, middleware, KV session storage
├── handlers/ # Worker fetch/scheduled/health handlers
├── services/ # Subscription, reminder, report, export, privacy, Telegram API logic
├── repositories/ # KV storage access layer and config readers
├── crypto/ # Encryption, hashing, key derivation, master key parsing
├── models/ # TypeScript type definitions
├── schemas/ # Zod validation schemas
├── utils/ # Parsers, formatting, date/math helpers, report rendering
└── types/ # Shared Env and grammY context types
- Add subscriptions interactively or with one-line commands.
- Track fixed cycles (
weekly,monthly,quarterly,yearly), manualcustomcycles, and interval cycles such as30d,4w,6m,2y,every 30 days, and每30天. - Mark subscriptions as trial or non-auto-renewing so reports and reminder wording match the real billing state.
- Pause and resume subscriptions from the inline list manager. Paused subscriptions are excluded from reminders, date advancement, and spending reports.
- View compact lists, paginated inline list management, subscription details, JSON export, a PNG spending overview, and text reports.
- Send scheduled renewal reminders through Cloudflare Cron Triggers.
# Install dependencies
pnpm install
# Run dev server
pnpm dev
# Push Telegram slash-command menu
pnpm command:push
# Run tests
pnpm test:run
# Type check
pnpm types:check
# Lint
pnpm lint
# Format
pnpm formatBefore merging a code change, run:
pnpm types:check
pnpm test:run
pnpm lint| Variable | Required | Format / Notes |
|---|---|---|
BOT_TOKEN |
Yes | From @BotFather |
TELEGRAM_WEBHOOK_SECRET |
Yes | High-entropy random string |
ENCRYPTION_KEY |
Yes | Base64url-encoded 32-byte value |
USER_HASH_SECRET |
Yes | High-entropy random string |
ADMIN_USER_ID |
No | Telegram user ID marked as admin |
APP_ENV |
No | development (default), production, test |
REMINDER_DAYS_AHEAD |
No | Number of days ahead to send renewal reminders (default: 3) |
XCURRENCY_API_KEY |
No | XCurrency commercial data API key for admin-triggered exchange-rate sync |
Secrets belong in .dev.vars locally and in Wrangler secrets for production:
wrangler secret put BOT_TOKEN
wrangler secret put TELEGRAM_WEBHOOK_SECRET
wrangler secret put ENCRYPTION_KEY
wrangler secret put USER_HASH_SECRET
wrangler secret put XCURRENCY_API_KEY/report generates one PNG overview with upcoming 30-day due spending, monthly subscription run rate, future 12-month projected spending, upcoming line items, and distribution charts. /report_text generates a Telegram text version with upcoming 30-day line items and 12-month projected line items.
Known currencies are converted to the user's default report currency from /settings using exchange rates stored in KV. Exchange rates are maintained with USD as the base (1 USD = N currency), then converted from the source currency to USD and from USD to the selected default currency.
The bot supports two exchange-rate sources:
- XCurrency live rates stored at
config:exchange-rates:xcurrency:v1 - Manually maintained rates stored at
config:exchange-rates:v1
When both exist and are valid, XCurrency rates are used. If XCurrency rates are missing or invalid, reports fall back to the manual config.
Admins can sync XCurrency rates with:
/admin_sync_exchange_rates
The command requires XCURRENCY_API_KEY. If the key is missing, the command skips without writing KV. The XCurrency endpoint returns USD-quoted rates (1 currency = N USD), and the bot stores their inverse so the existing report format remains 1 USD = N currency.
Seed or update the manual fallback key config:exchange-rates:v1 with JSON like:
{ "base": "USD", "rates": { "USD": 1, "CNY": 7.2, "EUR": 0.923 } }For local wrangler dev storage:
pnpm wrangler kv key put config:exchange-rates:v1 '{"base":"USD","rates":{"USD":1,"CNY":7.2,"EUR":0.923}}' --binding SUBSCRIPTION_KV --local
pnpm wrangler kv key get config:exchange-rates:v1 --binding SUBSCRIPTION_KV --localMissing currencies are shown where possible but are not included in converted default-currency totals. Paused subscriptions, trial subscriptions, non-auto-renewing subscriptions, custom cycles, and entries without price/currency are excluded from calculated spending totals.
Subscriptions support fixed cycles (weekly, monthly, quarterly, yearly),
custom cycles that do not auto-advance, and interval cycles in days, weeks, months, or years. One-line commands accept examples such as 30d, 4w, 6m, 2y, every 30 days, every 4 weeks, every 6 months, 每30天, 每4周, 每6个月, and 每2年.
The master encryption key must be exactly 32 bytes (256 bits), base64url-encoded:
node -e "console.log(Buffer.from(crypto.randomBytes(32)).toString('base64url'))"This key is used for application-level encryption. KV-backed session data derives a per-session AES-GCM key from it with HKDF. Never commit it to version control.
See docs/architecture.md for system design.
See docs/privacy.md for data handling and encryption details.
See docs/commands.md for the full command reference.
See docs/scaffold-review.md for the latest security and correctness audit.