This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
December 19, last year. I am in the supermarket line, holding a very large watermelon.
In two days, twelve people come to our place for Yalda, the longest night of the year.
I open my bank app. I close it. I open it again, as if the number might change the second time.
It does not.
Payday is January 1. I do the math. Then I stop doing the math.
This is That Day. Every family has one.
🧱 The bot is less dramatic. It calls That Day "Wall Day": the day the budget runs out.
TL;DR: Ledgerly is a Telegram bot for lazy families. Send it receipt photos and family dates. It tells you when the money will run out, while you can still change it.
Sped up: I tell the bot the family events, ask for /forecast, then ask about a gift. The chart shows the likely path, the range and the budget line.
A note before the story: to keep my family private, I changed the names, the birthdays and the money. The demo bot uses a sample year of spending I made for the demo. Money is in Turkish lira (₺). The bot, the screenshots and every test number are real.
My friend Ryan has typed every receipt into a spreadsheet, by hand, for years. Ryan has never had That Day.
So I tried Ryan's method. I lasted four days.
Some days I had more than 30 receipts. Skip three days and you are looking at a pile of a hundred. That is when you quietly give up.
Ryan's method works. It just needs a Ryan. He is the tortoise in the old story: slow and steady, every day. I am only slow. So I wanted a bot to be steady for me.
I looked back at December. The supermarket was normal. The calendar was not.
Four birthdays, one of them my nephew Arman's. Yalda Night at our place, for twelve people. We stay up late with watermelon, pomegranates and poems. One more family dinner.
Gifts, cakes, and two big grocery runs in one week. By That Day, the money was already at the cake shop.
I could not cut any of it. You do not cancel a nephew's birthday.
My wife knew every one of these dates in November.
My bank app knows I paid at a supermarket. It does not know it was a watermelon for Yalda. And it only looks back.
Families spend by the calendar. I did not need to type more. I needed to see the future, and I was too lazy to type even the past.
So I asked a lazy person's question: why not give this job to an AI?
The first version was not even an app. It was a folder of photos and one instruction file for Claude Code.
Then I wanted it on my phone, at the checkout, with the receipt still warm. And I thought: other lazy people might want this too. So for this challenge, I started again from zero, with a new repo.
Turkish receipts write 1.234,56. Read it the wrong way, and a ₺1,234 birthday cake costs ₺1.23. On a long receipt, it looks believable. It is very wrong.
Now I send the photo to a Telegram bot and put my phone away. That is my whole job.
Gemma reads it. Then plain code adds up the items, because of that cake. Only then is the receipt filed.
Code checked the sum. Gemma only read it.
When it cannot read the date, it just asks.
No clear date, no guessing. "Yesterday" is a fine answer.
Same photo twice? The bot says: "You already sent me this one 🦥"
December has not come yet. I am too lazy to wait, so I set the demo bot's clock to December 1, payday.
In October, my wife gave me the December list early. I passed it to the bot, the way I would tell a friend:
"Arman's birthday is Dec 14. We're hosting Yalda for 12 people."
Then I ask for /forecast. It gives me the reply at the top of this post.
My wife reads it over my shoulder. "Yalda," she says. "Of course it is Yalda."
The bot says Wall Day comes around December 19. Last year I found out on that day, in the supermarket line. This year I ask first:
"Can I afford a ₺5,000 gift for Arman?"
A ₺5,000 gift for Arman on Dec 14 moves Wall Day from Dec 19 to Dec 17. Likely spending becomes ₺47,400 of your ₺30,000 budget.
So Arman's gift gets a little smaller. The watermelon stays the same size.
The demo bot only answers me, because spending data is private. Your own copy is one button and three API keys away (Telegram, Gemini, TabPFN):
A Telegram bot for people who are too lazy to track their spending.
Send it a photo of a receipt. Gemma reads it, and plain code checks that the items add up before anything is filed. Tell it about family events in plain chat: "Arman's birthday is Dec 14." TabPFN reads the ledger and the calendar and tells you Wall Day: the day the budget runs out, while you can still change it.
The demo copy on Dec 1, sped up. A made-up household: every amount is made up.
Each person runs their own bot and their own database. One bot = one household.
/newbot).
MIT license. 165 tests, plus 5 live checks with real Gemma and TabPFN. The README shows where every measured number in this post came from.
Each tool has one job. Gemma 4 on Google AI Studio reads. Plain Python checks. TabPFN, through the Prior Labs API, looks ahead. Render runs the bot, and Sentry watches it.
On the way, I learned five things that work outside this project too.
1. Let the model read. Let code do the math.
Gemma turns photos into rows, and chat into small commands like "add this event". Language models are great readers and shaky accountants. So the sum check is plain Python:
if abs(sum(item.amount for item in items) - total) > 0.01:
status = "⚠️ needs check" # after one retry
That is the whole fix for the ₺1.23 cake. Gemma never predicts money.
2. With a tiny table, use a model that learns at prediction time.
I have about two months of real receipts, around 60 days. Most models need far more data and a training run.
TabPFN was never trained on my receipts. It was pretrained on a huge number of made-up tables. Now it reads my small table and predicts in one go. It is a bit like an LLM that learns from examples in a prompt.
One row is one day:
| Day | Days to payday | Birthday in next 3 days | Guests | Spent |
|---|---|---|---|---|
| Nov 26 | 5 | no | 0 | ₺600 |
| Dec 13 | 19 | yes | 0 | ? |
| Dec 21 | 11 | no | 12 | ? |
Past rows have the answer. Future rows have a question mark. TabPFN fills in the question marks.
model.fit(past_days, spent_per_day) # fit() only stores the table. No weights change.
3. Only use what you will know on the day you predict.
"Spent in the last 7 days" is a useful column. It is also a trap. On December 1, I cannot know what I will spend from December 13 to 19.
| Column | Known on Dec 1 for Dec 20? |
|---|---|
| Days to payday | Yes |
| 12 guests for Yalda tomorrow | Yes, I told the bot |
| Spent in the last 7 days | No. So I freeze it at the Dec 1 value |
A backtest hides past days and asks the model to guess them. Skip this rule, and the backtest looks great while the real forecast fails.
4. A range turns a guess into a decision.
TabPFN gives three numbers for each day: low, likely and high.
low, likely, high = model.predict(next_days, output_type="quantiles", quantiles=[0.1, 0.5, 0.9])
Wall Day is the first day where money already spent plus the likely days passes the budget. For the demo family, that is ₺30,000. The high numbers give "maybe as early as".
"Can I afford it?" adds ₺5,000 on Arman's birthday to the same daily forecast and finds Wall Day again. One number is a guess. Two forecasts side by side are a choice.
I also gave TabPFN an opponent: a typical day, the middle value of my past days. I hid two separate two-week stretches of my real spending. Then both guessed the total.
| TabPFN | A typical day | |
|---|---|---|
| Error on the two-week total (lower is better) | 19.3% | 24.5% |
| Days inside the low-to-high range | 92.9% | no range |
On average, TabPFN was closer on the total, and the total is what Wall Day needs. It won one test by a lot and lost the other by a little. Day by day, the typical day was slightly better. My real data has no family events, so quiet weeks are easy for a typical day. Two tests is a small test.
Always give your model an opponent. Otherwise you cannot tell if it helps.
5. Trace one real turn before you guess where the time goes.
Every Telegram message becomes one Sentry trace, with steps for Gemma, the tool call and, when it runs, TabPFN.
My first /forecast on Render took 37 seconds. TabPFN's step took 34 of them. The trace showed the wait came before TabPFN's first API call. It was Python libraries on the free server's small CPU.
So now the libraries load when the bot starts. The forecast is computed in the background after every new receipt or event. On an awake bot, TabPFN's step takes 3 to 9 seconds, and the answer is ready before you ask.
After the fix: TabPFN in the background, on the demo family's year.
Here is the Arman question, after the fix.
Gemma took 1.59 seconds and 1,149 + 37 tokens to understand the question. The forecast tool took 0.07 seconds, because the forecast was already waiting.
And the cost: $0. Gemma, TabPFN, Render and Sentry all run on free plans.
No prompts and no images go to Sentry.
Render's free plan puts the bot to sleep after 15 quiet minutes. This morning, after 23 quiet minutes, a test /forecast reached the sleeping bot. The first reply came after 43 seconds. The forecast came after 77. Sentry shows where the time went: right after a wake-up, TabPFN's step took 29 seconds again. I have not fixed this yet. Next, I will keep the last forecast in Postgres, so a wake-up does not wait for TabPFN.
Telegram resends a message when the bot is slow. So the bot says "got it" at once, does the work after, and skips repeats. After a nap, it also says sorry for the wait. On a plan that sleeps, answer first and work after.
One gotcha: leave out plan: free in render.yaml, and Render picks a paid plan. The same render.yaml behind the Deploy button passes Render's Blueprint check. My copy was deployed from it through Render's API in about 4 minutes.
My copy on Render: one free web service and one free Postgres.
Receipts are private. They show where you shop, what you eat and whose birthday is next week.
GEMMA_PROVIDER=ollama on a machine you control, and Gemma reads your receipts there.
With open weights, you choose where your receipts are read.
Typing one receipt by hand takes me about a minute. Sending a photo takes about five seconds. The bot needs about ten more. On a busy day with 30 receipts, that is half an hour of typing, or under three minutes of photos. Ryan: still typing.
I wrote the story, the spec and the first receipt rules. Claude Code wrote most of the code, ran the tests and deployed the bot through Render's API. A review pass found a real bug. The chat agent moved last year's Arman birthday and Yalda dinner to this year. I tested the bot in Telegram and recorded the demo.
render.yaml and a Deploy button give each family its own copy, with its own data. My I am still lazier than Ryan. I do not type a single receipt.
But it is only the first of December, and I already know when That Day is coming.
It may still come. It will not be a surprise.
Ryan still types every receipt by hand. He says it is relaxing.
Which month is your family's That Day? Tell me in the comments.