DEV Community

Cover image for Four Birthdays, Two Family Dinners, and One Extremely Expensive December
AsiMir
AsiMir

Posted on AI-assisted

Four Birthdays, Two Family Dinners, and One Extremely Expensive December

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

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's /forecast reply on December 1: likely spending ₺42,400, budget ₺30,000, Wall Day around Dec 19, biggest reason Yalda Night
🧱 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.

The chat at 1.5× speed: the events, /forecast with its chart, and the gift question
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.

What I Built

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.

Where the money really went

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.

The lazy first version

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.

Ledgerly

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.

A filed receipt in Telegram: six items, total ₺1,800,
Code checked the sum. Gemma only read it.

When it cannot read the date, it just asks.

The bot asks
No clear date, no guessing. "Yesterday" is a fine answer.

Same photo twice? The bot says: "You already sent me this one 🦥"

Seeing That Day coming

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.

Demo

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):

Deploy to Render

Code

GitHub logo asi-mir / ledgerly

Telegram bot for lazy families: Gemma reads receipts, TabPFN predicts the day the budget runs out. Deploy your own copy on Render.

Ledgerly 🦥

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.

Deploy to Render

Ledgerly in Telegram: two family events, /forecast with Wall Day and a chart, then a gift question

The demo copy on Dec 1, sped up. A made-up household: every amount is made up.

Your own copy in 3 steps

Each person runs their own bot and their own database. One bot = one household.

  1. Get three keys and your Telegram ID.

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.

How I Built It

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.

Gemma reads, code checks, TabPFN looks ahead

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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.

Where did the 34 seconds go?

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 loading 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.

Sentry trace after the fix: the background TabPFN step takes 3.12 seconds for the demo family's 365 past days and 31 future days
After the fix: TabPFN in the background, on the demo family's year.

Here is the Arman question, after the fix.

Sentry trace of the gift question: 2.11 seconds in total, with the Gemma call and the forecast tool call
One question, 2.11 seconds.

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.

The bot naps

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.

Render dashboard: the ledgerly web service, deployed, and the ledgerly-db Postgres database, available, both in Frankfurt
My copy on Render: one free web service and one free Postgres.

Why Does Open Innovation Matter?

Receipts are private. They show where you shop, what you eat and whose birthday is next week.

  • Your copy, your data. Each family deploys its own bot and database. There is no central Ledgerly server with everyone's spending.
  • Private when you need it. Gemma's weights are open: you can download the model and run it yourself. The free AI Studio tier may use your data to improve Google's products. Set GEMMA_PROVIDER=ollama on a machine you control, and Gemma reads your receipts there.
  • Only numbers go to TabPFN. One row per day: calendar counts and money totals. No shops, no items, no names.

With open weights, you choose where your receipts are read.

The Lazy Payoff

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.

What it can't do (yet)

  • The event features are tested only on the demo family. My real data has no events.
  • One household, about two months of data. Telegram only.
  • The free Render database expires after 30 days. After a nap, the first answer takes about 40 seconds, and a forecast over a minute.

My Agent Session

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.

Prize Categories

  • Gemma: Reads every receipt photo and every chat message, and turns them into rows or small commands. In the trace, it understood a question in 1.59 seconds.
  • TabPFN: Low, likely and high spending inside a Gemma agent. Error on the two-week total: 19.3% vs 24.5% for a typical day.
  • Render: The agent's runtime. One free web service runs the whole agent: the webhook, the Gemma and TabPFN calls, and the background forecast. A free Postgres holds the ledger. One render.yaml and a Deploy button give each family its own copy, with its own data. My render.yaml passes Render's Blueprint check. My copy was live in about 4 minutes, through Render's API.
  • Sentry Agent Tracing: One trace per message: Gemma, tool call, TabPFN. It showed most of the 34 seconds came before TabPFN's first API call.

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.

Top comments (0)