Inspiration

I built Oikos Ledger for an earlier hackathon, a personal finance app with recommendations, analytics and a smart voice assistant. I had a few more features I wanted to add, but most of my time went into fixing parser bugs and cleaning up the insights agent instead. As luck would have it, on June 30 this hackathon was announced, and I thought, why not. I could extend the app I already had and push it further.

That earlier version worked well, but like every finance app I've used, it started over every time you opened it. It could tell you what you spent this month, but it never remembered what it had already told you, or whether something it noticed once was actually a pattern.

That bothered me. A good financial advisor doesn't warn you about overspending the first time they see one bad weekend. They wait until it happens a few times before they say anything with confidence.

CockroachDB's managed MCP server let an agent read and write memory straight against a real database, and its vector search meant memory could be found by meaning, not exact wording. So I migrated the app to CockroachDB and built real memory on top of it. That's Oikos Recall.

What it does

Every time you check your balance or compare two accounts, the app logs what it saw as a single event. On its own, that's nothing. But a background job runs regularly, looks across recent events, and when the same pattern shows up three separate times, it gets promoted from tentative to confirmed. If you ask it later what it's noticed then it can tell you the difference, "Axis has run higher than HDFC in Shopping for 25 checks straight, I'm confident about that one. Utilities I've only seen twice, take that with a grain of salt."

Two pages carry these insights. Macro ties inflation and GDP data to your own balance, shows what idle cash is actually losing to inflation, and lets you try out what investing it instead would look like. Compare tracks two accounts side by side and remembers which one tends to spend more in which category. Both work by voice, and both can explain their own memory out loud if you ask.

How we built it

Oikos Recall runs on Next.js and CockroachDB, with two AWS Lambda functions doing the heavy lifting. One handles ingestion, parsing bank statements and generating embeddings. The other runs on a nightly schedule and does the actual remembering, reading recent events, counting how often a pattern repeats, and deciding whether it's earned confirmed status yet.

Memory itself lives in two tables. Episodic events are an append only log, one row per thing observed. Semantic facts are the conclusions drawn from that log once enough of it agrees. Both get read and written through CockroachDB's managed MCP server, and matching a new question against existing memory happens through CockroachDB's own vector search, using its <=> (Cosine similarity distance) operator to rank facts by similarity right inside the database instead of pulling everything into the app and sorting it there.

Caching turned out to matter more than I expected, especially for voice. A live memory lookup can take a few seconds, which is fine for text but noticeable when someone is mid conversation. I ended up with marker based caches everywhere memory gets read, each one checking a cheap, changes-when-the-data-changes value before deciding whether to skip the expensive round trip. It sounds simple, but I hit a real bug from it: two phrasings of the same question could disagree on whether memory had been used, because the cache and the memory itself weren't tracking freshness the same way.

The voice interface started as four separate implementations, one per page, because I built them one at a time and kept copying the last one. Once I noticed the same bugs kept showing up in different places, I pulled the shared logic into one hook and cut about 2,000 lines in the process.

Challenges we ran into

The nastiest bug was self inflicted. My fix for the memory search fetched each fact's raw embedding so I could compare it in code. That worked until a user had more than one confirmed fact, at which point CockroachDB rejected the query outright, it was too large. It turns out that there was no safe number of facts above one for that approach. The real fix was moving the comparison into CockroachDB itself, using its <=> operator to rank by similarity server side instead of hauling vectors into the app.

The other one I never fully solved. CockroachDB's free tier scales to zero when idle, and roughly one in eight sessions would hit a multi second stall touching the database cold. I tested a few theories, query plan caching, a slow first connection, and none of them held up cleanly. I ended up documenting it as a known limitation instead of chasing a root cause I couldn't pin down, and added a warm up request on page load to at least move the cost off the user's first real question.

Accomplishments that we're proud of

Watching a fact actually flip from tentative to confirmed, live, in front of me, is the moment this stopped feeling like a demo and started feeling like a real system. I'd built the schema, the thresholds, the distillation job, all on paper reasoning. Seeing it happen against real data, with a query I ran myself, was different. It meant the memory wasn't just described in the architecture, it was actually there.

I'm also glad I didn't just work around the MCP query size bug. My first instinct was to shrink the payload and move on. Instead I moved similarity ranking into CockroachDB itself, which turned out to be a more honest use of the database than what I'd built originally. The bug forced a better answer than the one I would have shipped if everything had just worked the first time.

And I'm proud of catching my own mistakes instead of only catching the ones a user would notice. A few of the real bugs this project had, the ones I actually found, were phrasing that quietly lied about confidence, or a date calculation that was almost right, or a query with no date filter at all. None of those would have crashed anything. They would have just been wrong in ways that are easy to miss and cause distrust in the app.

What we learned

Bank statement parsers are a bigger problem than they look. Every bank has its own narration format, and trying to fix one from a single sample statement is mostly guessing. I only found the real gaps in the Axis parser once I had more than one real statement to check it against. One sample tells you almost nothing about how a bank actually writes its transactions

The other lesson came from the demo recording itself, which took me close to three hours. I hadn't taken caching seriously enough until I was sitting there watching a voice response stall for several seconds mid recording. It mattered for two separate reasons I hadn't fully separated before that, latency, since a slow answer is obviously bad live, and cost, since every uncached call was a repeated LLM or embedding charge for a question I had effectively already answered

What's next for Oikos Recall - Your finances, remembered, not just recorded

The threshold of three occurrences before something counts as confirmed is arbitrary. It works, but it isn't grounded in anything statistical, it's just a number that felt reasonable. I'd rather replace it with real significance testing suited to what's actually being measured. For something like which account tends to spend more in a category, that's a repeated directional outcome, so a binomial test against random chance fits better than an arbitrary count, three in a row could become however many checks it actually takes to be unlikely by coincidence, rather than a fixed number that doesn't adjust for how surprising the result actually is. For a balance trend staying flat or drifting, the better fit is testing whether the slope over time is significantly different from zero, which is closer to how a t-test is normally used.

Beyond that, real three or more account comparison is the most obvious next feature, the schema and UI are both intentionally capped at two right now, but the underlying idea doesn't have to be. I'd also like to extend memory to more fact types than balance trend and category spend, so the system starts noticing things I haven't specifically built it to look for yet.

Built With

Share this project:

Updates

Submission history