Cheapskate!
October 15, 2026  --  14 minutes
· · ·

Cheapskate is a tool I’ve made over the last few months to help manage investments.

You visit once a month with statements from all your bank accounts, and Cheapskate parses the individual investments from each one to track your holdings over time. More importantly, you assign an asset class to each, and define a target portfolio composition, so the tool can help you diversify according to your own unique goals.

I’m proud of it, and so I wanted to share a little here about how it came to be, what it is today, and where it’s going next!

Origin Story

I’m not really a money person, so in the event I find myself with any, my modus operandi just becomes “don’t fuck it up”, right. In that vein, it seems like the big common-sense approach is “diversification” – spread your eggs among a few different baskets so you don’t lose literally half your worth in a 2008-style event. Something simple, like the Boglehead 3-fund portfolio, 60%/20%/20%. Yeah, I can do that!

Turns out, that’s easier in theory than in practice. What happens when you have four different accounts – two 401(k)s, a Roth IRA, and an after-tax one? And so does your wife? Is it better to hold bonds in a tax-advantaged account? What if her 401(k) only really has “target date” funds which are a blend of US stocks, international stocks, and bonds? And most importantly – even if you do all the math to get the allocation perfect right now, how’s that going to look next year? You ready to do it all again?

I think this is the point some people might pay someone else to do it. But not this guy!

I just in general dislike outsourcing problems like this, and besides, this concept isn’t difficult, just the math is. Software is great at doing hard math. I’m a software developer. I think we can make something happen here?

My original concept was quite different from where we ended up, and quite simpler – just use Plaid or something to connect to a user’s accounts and pull holdings for each, then aggregate with a few pie charts to help them make rebalancing decisions. Easy! But, apparently the “pull holdings” part is… special? And really expensive? Like, the “contact sales” kind of expensive; I saw reports of minimums being in the hundreds of dollars per month, of course an outlandish sum for an open-source app I wanted to offer for free.

So, I had to do something different, and the current Cheapskate was born!

Architecture

The architecture Cheapskate ended up with is super simple and (not coincidentally) is also my favorite format for software :)

It’s a “backend-free” web app, which means it is just a static bundle thrown onto Bunny CDN (though any web host will work fine here), plus a database that the frontend talks with directly via e.g. PostgREST (Firebase and Supabase are the usual picks here, and Cheapskate uses the latter one).

I’ve always felt this is the ideal shape for a one-person software project to take:

There is some functionality already that can’t fit into this simplistic model. For example, both the PDF-parsing and account-deletion bits require secret credentials (an OpenRouter API key and the Supabase service key respectively) that can’t be published, and so Cheapskate uses a couple of Edge Functions for those. Future functionality might lean on more of them.

The system has been very pleasant and functional to develop so far!

Data Model

The data model was a lot of fun to iterate on, and I really like where it ended up, which is an event-log style that works well for historical queries.

It has the entity tables you’d expect (an accounts table with one row per account, an assets table with one row per asset, etc), which hold metadata. Updates to the meat and potatoes, though, like asset prices or holdings in an account, never result in a value being edited in the database; they insert a new row in an updates table, like an append-only log, with “effective date” timestamp.

From this, we can reconstruct the state of an account at any point in time just by filtering the updates table for that effective date. This is key for the “portfolio” and “net worth” views, where we have to determine an account’s value at the end of every month. A bit reminiscent of real-time data’s unifying abstraction perhaps?

It also means we get statement backfills and a complete audit log essentially for free!

It isn’t truly append-only (Cheapskate does support deleting an update as a means of undoing a mistake) but the benefits are the same, and it makes it easy to reason about how new kinds of updates should work too.

AI Stuff

The most interesting part of Cheapskate, to me, is how it leverages LLMs, turning a task that would’ve been difficult-to-impossible a few years ago into something that can be done on a mid-spec laptop today.

The LLM integration is what enables the app to parse any statement, from any bank, into a list of investment holdings. For this, the “upload statement” flow uses the structured-output mode that’s provided by any OpenAI-compatible LLM API (and has actually existed for a pretty long time, over 2 years now). It’s a great feature for incorporating isolated bits of AI magic in more “normal” applications, and I wish it got more love!

This did take some experimentation to get right. For example:

I was able to sort out the latter issue above with a two-step approach – in one prompt, include the PDF and just ask the LLM to extract the information as regular text, and then a follow-up prompt to ask it to render that same information as JSON in structured-output mode – which works well (and doesn’t incur much latency thanks to the prompt cache).

# first prompt, parsing PDF => long prompt eval
prompt eval time = 13554.50 ms / 11182 tokens ( 1.21 ms/tok, 824.97 tok/sec)
       eval time = 28608.49 ms /   850 tokens (33.66 ms/tok,  29.71 tok/sec)
      total time = 42162.99 ms / 12032 tokens
   graphs reused = 846
stop processing: n_tokens = 12031, truncated = 0
# second prompt, outputting JSON => cache is reused
prompt eval time =  1476.09 ms /  868 tokens ( 1.70 ms/tok, 588.04 tok/sec)
       eval time = 16285.29 ms /  473 tokens (34.43 ms/tok,  29.04 tok/sec)
      total time = 17761.38 ms / 1341 tokens
   graphs reused = 1316
stop processing: n_tokens = 12518, truncated = 0

In the end, we have an approach where a model as small as 4B params can accurately process a 10-page statement (a task that takes under 60 seconds on my base M5 Macbook), which I’m really happy with!

Sharing and Security

Cheapskate does include family sharing, but it’s very basic, just “good enough for now”.

It was necessary because I’m married / have combined finances with my wife, and so I wanted to make sure anyone in this situation could use the app effectively. With this model, anyone in a family has full CRUD permissions for all of the family’s accounts, assets, and goals.

Implementation-wise, it’s woven into the RLS policies for every table. Instead of “each user can CRUD only their own rows” (as is the most common case with RLS), I’ve gone with a set of “each family can CRUD only its own rows” policies. When a user attempts to run a query against a table, Postgres runs a quick check to see what family that user is a part of, and the RLS is applied from there.

This works well! The implementation scope was small and simple, and it makes it very easy to invite someone else to your family and be sure that they can do everything they need to.

There are substantial downsides to this approach, though. For one, it’s not very flexible; it isn’t possible to (for example) add someone as a “limited” member of a family, with visibility of, or write access to, only certain accounts. I believe implementing this would be very difficult with this sharing architecture.

In addition, it is entirely “security by obscurity”! Just knowing a family’s UUID is enough to join it; an existing member doesn’t need to approve a request to join, and I don’t think it’d be easy to allow e.g. kicking someone out of your family. My logic here is that it’s impossible to guess a UUID (and we very explicitly deny SELECT on the families table so folks can’t scan it), so that acting as a sort of very-long password for a family’s data is an alright place to start.

Should You Use Cheapskate?

You can!

It’s reliable and functional; in fact I’ve been using it myself for almost a year. The data model has had time to bake and I don’t expect any more big changes to it or the UI.

That said, there are still quite a few caveats:

For what it’s worth, I do have a plan to address these shortcomings! But that is longer-term.

And importantly, this is definitely still alpha software – there could be some big change in the future (to fix the above? 👀), a boundary across which existing data can’t be migrated. If you do decide to use Cheapskate, please know that in this event, I will give ample notice (at least a month) and a feature to download all your data before it goes poof!

The Future

I am excited to continue developing Cheapskate! This is just v1 / the first iteration I feel good enough about to share.

Ultimately, in line with my goal of this being a kind of pseudo-robo-advisor, I’d like to take the “portfolio rebalancing” aspect one small step further than it is today, and enable Cheapskate to make suggestions around what assets you could buy or sell in order to achieve a portfolio composition that’s more in line with your goals.

To make these suggestions actionable / not annoying, we’ll need to pull in some external data – namely, ticker prices for publicly-traded assets, probably daily. Then, a suggestion won’t just be “you need to sell some DDOG”, it’ll be “you need to sell some DDOG, and it’s at its highest price today of the last 6 months”, which might make deciding to rebalance easier.

(and as of publishing, it kinda is, good job datadog)

I’ve been putting this off so far since it’ll require some background service, or cron-triggered edge function, to actually update the prices, and likely some kind of integration with a finance API to get that data, which, I don’t know how expensive that’ll be. Still, it’s a “soon” thing, and will (in my opinion) greatly improve the usefulness of the site.

I’m also really interested in seeing how I can make the local-AI angle of Cheapskate have less friction. Today, the technical barrier to entry is sadly pretty high even aside from the hardware requirements:

I want for this to be easier to get right, guide users into the pit of success, such that we could normalize local AI and have more people build apps like Cheapskate that rely on it.

Maybe someday soon!

Made with Jekyll   |   Last updated 20 Oct 2026