$2.94: A Serverless Spending Tracker

Since moving to London, I've built a handful of tools to help me keep on top of my finances. They're not finished, or even particularly polished, but they're incredibly handy!

Partly as a challenge - but mostly to avoid having to update yet another EC2 instance - I decided to use some of Amazon's managed services for business logic and for storing my data.

This has the added advantage of being extremely cost-effective: my current total forecasted bill for August is $2.94.

It's worth noting that even this is an overestimate: while debugging some slow load-times, I've had aggressive caching and autoscaling rules enabled for API Gateway and DynamoDB respectively, as well as liberal logging to CloudWatch. For a project with such low scaling requirements, these represent around 40% of the overall cost of the system; they can now be cut back significantly as the problem was identified elsewhere.

My main spending card is a Monzo debit, which can be configured via their developers page to send webhooks for every new transaction made with the card.

In order to respond to these webhooks, my first managed service is API Gateway. I haven't ventured into using Swagger yet, so I've manually added a "resource" to respond to the webhooks sent by Monzo.

My second managed service, which integrates nicely with API Gateway, is Lambda. This allows me to write business logic without maintaining a runtime environment - in this case, for Python.

Adding A Transaction

The first lambda function, which simply stores a (somewhat more lightweight) "transaction" in a database, is shown below in full:

This Lambda function uses a third managed service, DynamoDB, to store data. DynamoDB is a NoSQL database which allows for auto-scaling read and write capacity based on demand.

Categorising Transactions

Four more Lambda functions interact with the Transactions table in DynamoDB:

  • categoriseTransaction();
  • getAllTransactions();
  • getCategorySummaries(); and
  • getUncategorisedTransactions()

To explain what these accomplish, the general flow of transactions is as follows:

  • Transaction added to database, uncategorised;
  • Notification sent to phone to categorise data;
  • Transaction category updated in database;
  • Transaction now counted in that category's total.

The phone notification is accomplished using IFTTT and Pushbullet: IFTTT isn't strictly necessary, but the authentication was easier to set up than using the PushBullet API.

My phone receives a notification, which - when I click it - takes me to a website for categorising my recent transactions:

This website is hosted using the fourth AWS managed service: S3. Specifically, it's using the S3 Website configuration to serve static HTML, CSS and JavaScript. When I select an option, a POST request is sent to the /transaction/<id>/category endpoint, which saves the category I've chosen in the DynamoDB table.

Viewing Transactions

All this data being saved in a DynamoDB table is useless if I can't see any of it! To view some basic statistics, I created two static websites using Elm: a functional programming language which compiles to HTML and JavaScript.

Charts

The first website, shown below, displays some basic information summarising the amount of money in total spent on categories. It uses the elm-plot library to render an SVG bar chart.

Tables

The second website, also shown here, is a searchable and sortable table of all my transactions. As well as containing somewhat complicated logic for decoding the JSON data into Elm-native data types, this website also prompted me to think about the scalability of my API.

Sending requests for the entire table of transactions, while useful for the client, is costly for the backend. While pagination would be an ideal solution, the size of the table isn't huge, and so some more careful design of the backend sufficed to improve loading times greatly.

In order to make sure my services could handle unexpected load, I enabled AutoScaling with a target of 80% utilization in DynamoDB. I also enabled an API cache with a TTL of two minutes (120s) on API Gateway to prevent repeated requests causing sudden spikes in database requests.

Future Plans

While I currently have three separate, useful tools, I'd like to combine these tools into a single website. As well as keeping all the code in one place (and ideally in one language!), it'd then be possible to register a better-looking domain and use Amazon's managed TLS termination for a secure connection to the website.

I'd also like to include OAuth2 or some other method of federated authentication.