尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Alfred is a personal finance assistant for Telegram and the website. A signed-in user can log expenses and income, ask for summaries, set budgets, track savings goals, and correct or delete records. Both interfaces use the same ledger.
Telegram access comes from the Telegram ID saved on that user's account. The ID can be edited in Settings until the first private message arrives from it. After that message, the ID is locked.
Website sign-in emails a one-time code through Resend. A password is optional and can be added later in Settings.
Telegram sends an update to POST /api/telegram/webhook. The route checks the webhook secret, validates the payload, and confirms the sender's numeric Telegram user id. Only then does it call DeepSeek or Google Sheets. DeepSeek proposes a structured action. The server validates that action, calculates totals itself, and writes to the spreadsheet. The bot replies only after the required write has been checked.
A message cannot choose a spreadsheet or another person's records.
Copy .env.example to .env. Do not commit .env. None of these variables use a NEXT_PUBLIC_ prefix.
| Variable | Purpose |
|---|---|
TELEGRAM_BOT_TOKEN | Bot token from BotFather |
TELEGRAM_WEBHOOK_SECRET | 16–256 characters: letters, numbers, _, -. Generate with openssl rand -hex 32 |
DEEPSEEK_API_KEY | DeepSeek API key |
DEEPSEEK_MODEL | deepseek-flash or deepseek-v4-pro |
GOOGLE_SERVICE_ACCOUNT_EMAIL | Service account email |
GOOGLE_PRIVATE_KEY | PEM private key. In Vercel, store newlines as \n |
GOOGLE_SHEETS_SPREADSHEET_ID | Id from the spreadsheet URL |
AUTHORIZED_TELEGRAM_USER_IDS | Optional fallback only when Supabase is not configured |
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL used by the browser |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Public anon key. Never put the service-role key here |
SUPABASE_SERVICE_ROLE_KEY | Server-only key for seeding accounts and creating sign-in codes |
RESEND_API_KEY | Resend API key used to email sign-in codes |
RESEND_FROM_EMAIL | Verified From address, such as Alfred <[email protected]> |
CRON_SECRET | Bearer token for GET /api/cron/reminders |
APP_TIMEZONE | Default Africa/Accra |
DEFAULT_CURRENCY | Default GHS |
CONFIRM_TRANSACTIONS | auto or always |
auto records an unambiguous expense or income immediately. Ambiguous messages, corrections, and deletions still ask for confirmation. always asks before every new transaction too.
The model ids were checked against the DeepSeek models list on 11 Oct 2026. deepseek-flash and deepseek-v4-pro are the current ids. A live JSON-mode call to deepseek-flash with thinking disabled succeeded. Google Sheets writes have not been run against a real spreadsheet in this environment because those credentials are not configured yet.
https://www.googleapis.com/auth/spreadsheets. The service account can only open files that were shared with it.Prepare the tabs without touching existing rows:
npm run setup:sheets
The script creates these tabs if they are missing and writes a header row only when row 1 is empty. If the headers do not match, it stops and leaves the rows alone.
transaction_id, created_at, transaction_date, user_id, type, amount, currency, category, description, payment_method, notes, status, updated_at, source_update_id
source_update_id is an extra column used to recognize a Telegram retry. Amounts are positive decimals. type is income or expense. status is active or deleted. Deleted rows stay in the sheet and are left out of totals. created_at and updated_at are UTC ISO timestamps. transaction_date is a date in the user's timezone.
budget_id, user_id, period, category, amount, currency, created_at, updated_at
period is YYYY-MM. Remaining budget is the budget amount minus active expenses in that category, currency, and month. If spending is higher, the bot says how far over it is.
goal_id, user_id, name, target_amount, current_amount, currency, target_date, status, created_at, updated_at
A goal is recorded progress. It is not a bank balance. Logging a savings-goal contribution does not also create an expense unless the user explicitly records that expense too.
update_id, user_id, status, result_reference, processed_at
Telegram may deliver the same update more than once. Alfred stores the update id and replays the previous reply instead of writing a second transaction. This is not a database transaction. Two overlapping requests can both pass the first read; the earliest processing row continues, and transaction rows also carry source_update_id so a retry can find a write that already landed. If you need strict uniqueness under concurrent writes, move this state to a database.
action_id, user_id, chat_id, action_type, payload_json, status, expires_at, created_at
Corrections and deletions, and transactions that are not explicit enough, create a pending action for 15 minutes. The user confirms with inline buttons. The action is bound to the user id and chat id. Another user cannot confirm it. The payload is validated again before it runs. Expired actions do nothing.
/id to the bot or run:npm run telegram:user-ids
/id replies with the sender's numeric id and no financial data. It works even when that id is not authorized yet. getUpdates returns nothing after a webhook is set; delete the webhook first if you use the script.
https://<your-domain>/api/telegram/webhook is public HTTPS.npm run setup:webhook -- https://<your-domain>/api/telegram/webhook
That calls setWebhook with the URL, secret_token, and allowed_updates set to message and callback_query.
The current bot username from getMe is @MadeByTonyBot. The production webhook path is always /api/telegram/webhook on the deployed HTTPS origin.
npm install
npm run dev
Useful checks:
npm test
npm run lint
npm run typecheck
npm run build
npx tsx scripts/smoke-interpret.ts
smoke-interpret.ts sends one example sentence to DeepSeek and prints the validated action. It does not write to Google Sheets.
.env.example in the project settings. Paste the private key as one line with \n for newlines.maxDuration to 60.npm run setup:sheets locally with the production spreadsheet credentials, then npm run setup:webhook with the production URL.Health check: GET /api/health returns { "status": "ok", "checks": { ... } }. The booleans say whether a variable is present. They do not return the values.
Weeks start on Monday and end on Sunday, in Africa/Accra unless APP_TIMEZONE changes. A missing period is treated as this calendar month, and the reply shows the dates. Totals are recorded cash flow, not an account balance.
Examples:
Several expenses in one message are checked first, then written one by one. If one write fails, the reply lists what was recorded and what failed.
Initial expense categories: Food, Transport, Utilities, Subscriptions, Savings, Emergencies, People, Personal, Social, Misc. Income categories: Salary, Business, Gift, Refund, Other. Unknown categories are questioned rather than invented. Lunch maps to Food. Trotro maps to Transport.
GOOGLE_SHEETS_SPREADSHEET_ID, run npm run setup:sheets, and redeploy.user_id, so they do not see each other's totals. A later user directory can map each Telegram id to its own spreadsheet id inside authorizeUser without letting a message pick the sheet.GET /api/health returns status: ok and no secrets/id returns only the numeric user id/id still returns only the numeric id.deleted and drops out of the next totalX-Telegram-Bot-Api-Secret-Token header does not match TELEGRAM_WEBHOOK_SECRET. Register the webhook again after changing the secret./id, save it on the website, then message again. After the first message, the ID can no longer be edited.source_update_id before entering it again by hand.getUpdates is empty. A webhook is already set. Use the webhook route, or delete the webhook before using npm run telegram:user-ids.