OpenChart
Claude·Codex와 함께하는 무료 데스크톱 차트 도구. 알림 설정·차트 움직임 분석·데이터 제공자 선택
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 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.