Local-first finance automation for a creator or small business team.
This repo is built for one specific operating model:
Telegramis the control surfaceNotionis the finance ledgerSQLiteis local bot state onlyCodex CLIis the optional AI layerLocal PDF renderinggenerates invoices from code
There is no hosted backend requirement.
This system helps you:
- log won deals
- reserve invoice numbers
- finalize invoice details later
- mark payments received
- upload receipts and draft expenses
- approve sensitive ledger writes
- generate weekly summaries
- ask for finance insights from the ledger
- not a Google Sheets system
- not a Telegram webhook service
- not a hosted SaaS
- not an OpenAI API integration
- not a replacement for your local Notion workspace
Telegram
-> FinanceTelegramBot
-> FinanceMessageParser
-> ReceiptOcrService
-> FinanceService
-> NotionFinanceRepository
-> InvoiceRenderer
-> CodexCliClient (optional)
-> LocalStore (SQLite)
Boot entrypoint.
It:
- loads
.env - validates runtime config
- opens SQLite
- connects to Notion
- wires the bot, scheduler, and parser together
- starts the long-polling Telegram bot
Telegram application layer.
It:
- polls Telegram updates
- handles slash commands
- routes text messages, PDFs, and photos
- creates approval prompts
- waits for follow-up text after bare file uploads
- deletes bot-side approval cards after approval/rejection
Core business logic.
It:
- records deals
- creates revenue rows
- reserves invoice rows
- finalizes invoices
- marks invoices paid
- builds summaries
- answers finance questions
- rebuilds derived client/service summaries
This is the main rules engine of the app.
Deterministic text parser.
It:
- parses expense text
- parses won-deal text
- parses
/invoice_details - parses
/mark_paid - detects backfill dates
Receipt and file interpretation.
It:
- extracts text from PDFs/images
- parses structured expense captions
- merges OCR with explicit caption data
- creates expense drafts for approval
Local invoice generation logic.
It:
- chooses the next invoice number
- prepares invoice render payloads
- applies invoice branding fields from
.env - calls the PDF renderer script
Actual PDF renderer.
It:
- lays out the invoice page
- wraps long description text safely
- positions totals and payment details
- outputs the final PDF bytes
Notion repository layer.
It:
- verifies the expected data sources exist
- writes expenses, deals, revenue, and invoices
- updates existing rows
- rebuilds derived
ClientsandServices
Telegram Bot API client.
It:
- sends messages
- downloads files
- polls updates
- deletes bot messages
- registers the Telegram command menu
Optional AI bridge.
It:
- runs local
codex - supports finance Q&A
- can be disabled
Local SQLite state store.
It stores:
- processed Telegram update IDs
- pending approvals
- pending attachments
- scheduler checkpoints
- invoice override/sequence helpers
Important:
- this is not your business ledger
- this is only local runtime state
Duplicate-process protection.
It prevents:
- two polling bots running at once
- Telegram
getUpdatesconflicts
Reminder scheduler.
It:
- checks daily/weekly summary windows
- sends summaries to the configured Telegram chat
- uses SQLite checkpoints so summaries do not spam repeatedly
Create one parent page and put these data sources under it:
Expense Ledger 2026Deals Pipeline 2026Revenue Ledger 2026Invoices 2026Clients 2026Services 2026Backfill Batches 2026
Source of truth for costs.
Each row should represent one real expense.
Source of truth for commercial work.
Won deals create revenue and reserve invoice numbers.
Tracks booked revenue and collection state.
Tracks:
- reserved
- draft
- sent
- paid
- cancelled
Derived summary table.
Used for analysis, not raw operational entry.
Derived service summary table.
Used for service grouping, price observations, and common deliverables.
Used to tag historical imports and backfilled records.
Example:
Won deal: [Acme Studio, Spring Launch] [2000] SGD on [March 25th, 2026]. Notes: [1 x Instagram Reel or TikTok Video + 1x IGS repost]
Result:
- deal row created
- revenue row created
- invoice number reserved
- invoice row created with status
reserved
Example:
/invoice_details INV-001 | Acme Studio Pte Ltd | 123 Example Street ; Suite 8 ; Example City | 1 x Instagram Reel or TikTok Video + 1x IGS repost | 2000 | April 20th, 2026 | 30days
Result:
- existing reserved invoice updated
- bill-to name/address added
- PDF generated locally
- invoice row updated to draft state
Example:
/mark_paid INV-001 2000 2026-04-21
Result:
- invoice becomes paid
- revenue row reflects payment
spent 240 on meta ads
Send a photo or PDF with caption:
Expense: 2026-03-10, Example Electronics, SGD 4242, Editing Workstation (Equipment), paid by card
If you upload a PDF/image without a caption:
- bot waits 5 seconds
- expects a follow-up text message
- combines the file with the follow-up details
- errors if nothing arrives
/help/whoami/status
/insights/insights <question>/summary daily/summary weekly/rebuild_insights
/approve_deal DEAL_ID/draft_invoice DEAL_ID/invoice_details INVOICE_OR_CLIENT | [BILL_TO_NAME] | [BILLING_ADDRESS_LINE_1 ; LINE_2 ; LINE_3] | DESCRIPTION | AMOUNT | ISSUE_DATE | DUE_DATE/mark_paid INVOICE_ID AMOUNT [YYYY-MM-DD]
/backfill YYYY-MM-DD <expense or deal text>
Open Telegram and message @BotFather.
Run:
/newbot
Then:
- choose the bot name
- choose the bot username
- copy the bot token
In @BotFather:
- run
/setprivacy - choose your bot
- set privacy mode to
Disable
Why:
- with privacy enabled, the bot cannot read normal group messages properly
Add the bot to the shared finance group.
Make sure:
- it is allowed to send messages
- it can see group messages
In Notion:
- create an internal integration
- copy the integration token
- share your finance workspace page with that integration
Under one parent page, create:
Expense Ledger 2026Deals Pipeline 2026Revenue Ledger 2026Invoices 2026Clients 2026Services 2026Backfill Batches 2026
Then collect:
- parent page ID
- each data source ID
cp .env.example .envThen fill the real values.
TELEGRAM_BOT_TOKENTELEGRAM_ALLOWED_CHAT_IDTELEGRAM_NOTIFICATION_CHAT_IDTELEGRAM_ALLOWED_USER_IDSTELEGRAM_APPROVER_USER_IDSNOTION_API_TOKENNOTION_ROOT_PAGE_IDNOTION_EXPENSES_DATA_SOURCE_IDNOTION_DEALS_DATA_SOURCE_IDNOTION_REVENUE_DATA_SOURCE_IDNOTION_INVOICES_DATA_SOURCE_IDNOTION_CLIENTS_DATA_SOURCE_IDNOTION_SERVICES_DATA_SOURCE_IDNOTION_BACKFILL_BATCHES_DATA_SOURCE_ID
AI_PROVIDERCODEX_CLI_ENABLEDCODEX_BINCODEX_MODELAI_CONFIRMATION_THRESHOLD
These are only needed if you want the generated PDF invoice to show your business information cleanly:
INVOICE_NUMBER_PREFIXINVOICE_SEQUENCE_STARTINVOICE_PAYMENT_TERMS_DAYSBUSINESS_NAMEBUSINESS_ADDRESSBUSINESS_EMAILBUSINESS_PHONEPAYMENT_DETAILS
Answer to your earlier question:
- yes, the
BUSINESS_NAME / ADDRESS / EMAIL / PHONE / PAYMENT_DETAILSsection is still needed if you want proper invoice PDFs - no, it is not needed just to run deal logging or expense logging
codex login statusThis is only relevant if you want the optional AI finance-analysis path.
npm run init:workbookExpected result:
Notion ledger connectivity verified.
npm startIn private chat with the bot, send:
/whoami
In the finance group, send:
/whoami
Use those values to fill:
TELEGRAM_ALLOWED_CHAT_IDTELEGRAM_NOTIFICATION_CHAT_IDTELEGRAM_ALLOWED_USER_IDSTELEGRAM_APPROVER_USER_IDS
Won deal: [Acme Studio, Spring Launch] [2000] SGD on [March 25th, 2026]. Notes: [1 x Instagram Reel or TikTok Video + 1x IGS repost]
Approve it.
/invoice_details INV-001 | Acme Studio Pte Ltd | 123 Example Street ; Suite 8 ; Example City | 1 x Instagram Reel or TikTok Video + 1x IGS repost | 2000 | April 20th, 2026 | 30days
/mark_paid INV-001 2000 2026-04-21
Upload a receipt with:
Expense: 2026-03-10, Example Electronics, SGD 4242, Editing Workstation (Equipment), paid by card
Approve it.
npm start
npm run dev
npm run init:workbook
npm run rebuild:insights
npm run summary:daily
npm run summary:weekly
npm testInstall the launch agent:
zsh scripts/install-launchd.shThis gives you:
- restart on crash
- survives terminal closure
- auto-start on login
It does not run while the Mac is asleep.
Earlier iterations still carried an optional Google Sheets / Drive path.
That was not your live runtime anymore.
The current repo has been cleaned so the real system is now:
- Telegram
- Notion
- SQLite
- local PDF invoices
- optional Codex CLI
No Google integration is required for the current system.
If you are using Codex, Claude Code, or another coding agent:
src/index.tssrc/bot.tssrc/services/finance.tssrc/integrations/notion.tssrc/storage/database.ts.env.example
- Telegram is input/output
FinanceServiceowns business rulesNotionFinanceRepositoryowns ledger writesLocalStoreowns local runtime stateInvoiceRendererowns invoice PDF generation
- do not put secrets or live IDs into committed files
- keep bot transport logic in
src/bot.ts - keep ledger rules in
src/services/finance.ts - keep storage logic in
src/integrations/notion.tsandsrc/storage/database.ts - run
npm test
Safe to commit:
- source code
- tests
.env.example- launchd template
- scripts
- README
Do not commit:
.envdata/logs/- other local workspace symlinks
- generated runtime artifacts
Before the latest push:
- publish-safety scrub completed
- repo surface rescanned
npm testpassed
So the checked-in repo is aligned to the current runtime and does not depend on Google anymore.