Your financial fairy π§
A personal finance Telegram bot that logs cashflow via natural language and stores everything in Google Sheets.
- Natural language input β just message the bot like you're texting a friend
- Bilingual β auto-detects Indonesian and English per message
- Backdate support β log past transactions with relative ("kemarin") or absolute ("30 maret") dates
- Pocket management β categorize money across accounts, wallets, or e-wallets
- Google Sheets as dashboard β your data lives in a spreadsheet you can see, edit, and export
- No balance tracking β cashflow-first: income, expenses, and transfers only
- Bun β₯ 1.0
- A Telegram bot token β create one via @BotFather
- A Vercel AI Gateway API key
- A Google Cloud project with Sheets API + service account (see Google Sheets Setup below)
- A public HTTPS URL β use cloudflared for local dev
git clone https://github.com/your-username/pixance.git
cd pixance
bun install
cp .env.example .envFill in .env:
| Variable | Required | Description |
|---|---|---|
BOT_TOKEN |
β | Telegram bot token from @BotFather |
OWNER_CHAT_ID |
β | Your Telegram user ID β get it from @userinfobot |
AI_GATEWAY_API_KEY |
β | Vercel AI Gateway API key |
AI_MODEL |
β | Model to use (default: anthropic/claude-sonnet-4-5) |
GOOGLE_SERVICE_ACCOUNT_JSON |
β | Full service account JSON as a string (see below) |
SPREADSHEET_ID |
β | Google Sheets ID from the URL (see below) |
WEBHOOK_URL |
β | Public HTTPS URL Telegram will POST updates to |
PORT |
β | Server port (default: 3000) |
OWNER_CHAT_IDlocks the bot to a single user. To find your ID, message @userinfobot β it replies with your numeric user ID.
Two separate things to get. Here's exactly how.
The easy one. Create a new Google Sheet, then grab the ID from the URL:
https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/edit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
this is your SPREADSHEET_ID
Copy the string between /d/ and /edit. That's it.
This is the bot's identity β how it authenticates to the Sheets API without a browser login.
Step 1 β Create a Google Cloud project
- Go to console.cloud.google.com
- Click the project dropdown (top left) β New Project
- Name it anything, e.g.
finance-botβ Create
Step 2 β Enable the Sheets API
- With your project selected, go to APIs & Services β Library
- Search "Google Sheets API" β click it β Enable
Step 3 β Create a service account
- Go to APIs & Services β Credentials
- Click + Create Credentials β Service Account
- Name it anything, e.g.
finance-bot-saβ Create and Continue - Skip the optional role and user access steps β Done
Step 4 β Generate the JSON key
- Click your new service account from the credentials list
- Go to the Keys tab
- Add Key β Create new key β JSON β Create
- A
.jsonfile downloads automatically β this is yourGOOGLE_SERVICE_ACCOUNT_JSON
The file looks like:
{
"type": "service_account",
"project_id": "finance-bot-xxxxx",
"private_key_id": "abc123...",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\n...",
"client_email": "finance-bot-sa@finance-bot-xxxxx.iam.gserviceaccount.com",
"client_id": "...",
...
}Step 5 β Share the spreadsheet with the service account
This is the step people most often forget.
The service account is like a separate Google user β it needs explicit access to your sheet.
- Open your Google Sheet
- Click Share (top right)
- Paste the
client_emailfrom the JSON file - Set permission to Editor
- Send (ignore the "can't notify" warning if it appears)
Step 6 β Put it in your .env
The entire JSON content goes in as a single-line string:
# Option A β paste the whole JSON as a string (wrap in single quotes)
GOOGLE_SERVICE_ACCOUNT_JSON='{"type":"service_account","project_id":"..."}'
# Option B β on Railway/cloud platforms, paste the raw JSON directly into the env var field
# Most deployment platforms handle multiline values fine in their UIβ
Google Cloud project created
β
Google Sheets API enabled
β
Service account created
β
JSON key downloaded
β
Spreadsheet created
β
Spreadsheet shared with service account email (Editor)
β
SPREADSHEET_ID copied from URL
β
GOOGLE_SERVICE_ACCOUNT_JSON set in .env
If the bot throws a 403 error on first run, the missing share in Step 5 is almost always why.
Telegram requires a public HTTPS URL to deliver updates. Use cloudflared β no account needed.
# Install (macOS)
brew install cloudflare/cloudflare/cloudflared
# Expose your local port
cloudflared tunnel --url http://localhost:3000Copy the printed *.trycloudflare.com URL and set it as WEBHOOK_URL in .env.
Note: The URL changes every time you run cloudflared. Update
WEBHOOK_URLand restart the bot each dev session.
bun dev # with hot reload
bun start # without hot reload| Command | Description |
|---|---|
/start |
Show help and active pockets |
/report [today|week|month] [lang] |
Financial summary for the given period (lang: id or en, default: today) |
/history [lang] |
Last 10 transactions |
/pockets |
List active pockets with balances and total |
/pockets all |
List all pockets including archived |
/addpocket <name> |
Create a new pocket |
/renamepocket <old> <new> |
Rename a pocket (use quotes for names with spaces) |
/archivepocket <name> |
Archive a pocket |
/restorepocket <name> |
Restore an archived pocket |
/delete <n> |
Delete the nth most recent transaction |
Just send a message β the bot figures out the rest:
kopi 25rb pake gopay β expense: Food & Drinks, 25k, Gopay
gajian 8jt ke BCA β income: Salary, 8M, BCA
transfer BCA ke Gopay 1jt β transfer: 1M from BCA to Gopay
kasih uang 200rb ke Budi β expense: Gift Given, 200k
pinjemin 500rb ke Andi β expense: Loan Given, 500k
kopi 25rb kemarin β expense logged with yesterday's date
makan siang 50rb 2 hari lalu β expense logged 2 days ago
beli bensin 30 maret β expense logged on March 30
hapus kopi 25rb tadi β finds and deletes matching transaction
# Build and start
docker compose up -d --build
# Update after git pull
git pull && docker compose up -d --build
# View logs
docker compose logs -f
# Stop
docker compose downbun test # run tests
bun lint # lint with Biome
bun format # format with Biome
bun check # lint + format + fix