Skip to content

Zain Butt

Full-stack product engineer · London, UK

Selected work

ProofLens dashboard — "Preserve the proof behind the file", with integrity, device-linked and trusted-time checks
Year
2026
Role
Design and engineering
Built with
TypeScriptWebCryptoRFC 3161Node

Proof-Lens

Media provenance you can verify without trusting the person who sent it.

The problem

Everything here circles one question: how do you know a record is true? This is that question with the most at stake, because the record is a photograph and the answer has to hold up in front of someone who has no reason to trust you.

A photograph proves nothing on its own. Any image can be edited, re-encoded, or generated outright, and the metadata that might tell you so is trivially rewritten. The usual answer is to trust the platform that hosts it — which just moves the question rather than answering it.

The approach

Proof-Lens binds a capture to the device that made it and to the moment it happened, then packages both into evidence that survives leaving the app.

  • Device signatures. The capture is signed at source using hardware-backed keys, so the signature attests to the device rather than to a server.
  • RFC 3161 timestamping. An independent timestamping authority countersigns the hash, establishing that the content existed no later than a given moment.
  • Offline-verifiable bundles. Everything needed to check the claim travels with the file. Verification needs no network call and no account.

What was hard

The interesting constraint is that verification has to work for someone who does not trust you, does not have your app, and may be offline. That rules out any design where the proof is a lookup against a service you control.

What I'd change

The signature proves which key signed a capture. It does not prove that the key was in a phone rather than an emulator, and hardware attestation is exactly the part that varies most across devices — it is strongest where you need it least. The honest framing is the one already on the page: it verifies provenance, not reality. Someone can still point a genuine, attested camera at a convincing fake.

The RFC 3161 dependency is the part I would change first. Anchoring against a single timestamping authority means every bundle ever issued inherits that authority's lifespan. Countersigning with two, from different jurisdictions, costs almost nothing at capture time and removes a single point of failure that only becomes visible years later, when it is far too late to re-issue anything.

The bundle format was cut to what a verifier needs offline, which meant leaving out most of the capture context. I would revisit that trade — the difference between "this file is unaltered" and "this file is unaltered and here is what was around it" is most of the difference between evidence and a checksum.

Melody Terminal running AAPL — command bar, company overview, filings, news, chart and watchlist panels
Year
2026
Role
Design and engineering
Built with
TypeScriptSEC EDGARNodePlaywright

Melody

A command-first research terminal for public companies.

The problem

The same question as Proof-Lens, pointed at a different record: a filing is only worth reading if you can see where it came from and how old it is. Most tools show you the number and hide both.

Research tools for public companies are built around dashboards. Dashboards are good at showing you what someone else decided was important, and bad at answering a specific question quickly. If you already know what you want, a dashboard is in your way.

The approach

Melody inverts it. You type what you want and the answer streams back.

  • Primary sources. SEC filings direct from EDGAR, not a summarised feed.
  • Delayed market data, clearly labelled as delayed — the honest presentation matters more than the freshness.
  • Command-first. Keyboard is the primary interface; the UI exists to display results, not to be navigated.

What was hard

Filing data is messy in ways that resist generic parsing — the same concept is tagged differently across filers and across years. Most of the work is in the normalisation layer that sits between EDGAR and anything a user sees.

What I'd change

Normalisation is where nearly all of the work went and where the remaining problems live. Filers tag the same concept differently, and the same filer changes how it does so between years, so a mapping that looks complete against recent filings quietly degrades the further back you go. What I would change is not the mapping but the reporting around it: the interface should say when a figure came through a fallback path rather than a direct match, in the same way it already says when data is delayed.

The command-first claim deserves testing rather than asserting. Once several panels are open, reaching for the pointer is genuinely faster for some things, and the interesting question is which — that is a usage question I never instrumented and should have.

Labelling market data as delayed costs the tool any use case that depends on being current, which is most of the ones people expect from something that looks like a terminal. I would still make the same call. A research tool that is quietly stale is worse than one that is loudly late.

About

Zain Butt

I build product end to end — mobile, web, and the backend underneath. Computer Science graduate in London, UK, freelancing now, looking for a graduate or product engineering role. More on LinkedIn.

I build product end to end — mobile, web, and the backend underneath.

Recent work: a tool that proves a file has not been altered since it was captured, a keyboard-first terminal for reading company filings from primary sources, and a ledger that settles group expenses without a spreadsheet. Alongside those, freelance builds for paying clients — an operations platform for a cross-border logistics team, and a booking site for a private tutor.

Computer Science graduate, London, UK. Open to roles and freelance work.

Replay — "The shape of your time.", with a year scrubber marking the months a trip falls in
Year
2025
Role
Design and engineering
Built with
TypeScriptElectronSQLite

Replay

A local-first timeline that turns a camera roll back into trips and days.

The problem

A camera roll is chronological and nothing else. The structure you actually remember — the trip, the day, the afternoon something happened — exists in your head and nowhere in the file system.

The approach

Replay reads a photo and video library on Windows and reconstructs that structure locally. Nothing is uploaded; the library never leaves the machine.

  • Clusters captures into trips, days and moments from time and location.
  • Local-first by design — no account, no sync, no cloud dependency.

What was hard

Clustering is easy to make work on your own library and hard to make work on someone else's. Threshold-tuning that felt correct on a dense London camera roll fell apart on a sparse one.

What I'd change

The clustering is tuned, not learned, and tuned against my own library — which is dense, urban and heavily photographed. On a sparse library the same thresholds produce either one enormous bundle or a hundred singletons. The fix is not better constants; it is deriving the thresholds from the library's own density before clustering anything, so the parameters are a property of the collection rather than of mine.

Local-first was the right constraint and it cost more than I expected. No sync means no second device, and the moment someone has a phone and a laptop the model quietly stops matching how people actually keep photographs. I would keep the default and add optional, explicit sync rather than pretending the need is not there.

I never measured it against a genuinely large library. Electron and SQLite are fine at the sizes I tested, and "fine at the sizes I tested" is exactly the sentence that precedes a performance problem.

IOU review screen — a £66 food bill split four ways with service charge
Year
2025
Role
Design and engineering
Built with
TypeScriptExpoExpressSupabase

IOU

Group-trip expenses and rounds, settled without the spreadsheet.

The problem

Splitting costs on a group trip is a solved problem that keeps being solved badly. Most tools handle the arithmetic and ignore the social part: nobody wants to be the person chasing four friends for £11.

The approach

A shared ledger for trips and rounds, with the settlement maths done continuously rather than at the end.

  • Full stack across mobile and backend — Expo client, Express API, Supabase.
  • Rounds modelled as first-class objects, not just another expense category.

What was hard

Offline and concurrent edits. Two people adding expenses on bad signal in the same pub is the normal case, not the edge case.

What I'd change

Rounds as a first-class object is the decision I am least sure about. It models the social reality well and it doubles the surface area of everything that touches the ledger — settlement, permissions, the edit paths. If I rebuilt it I would start with expenses that can be grouped and see whether rounds ever needed to be their own thing, rather than assuming they did.

The offline story is the part that would keep me up. Two people adding expenses on bad signal in the same pub is the normal case, and last-write-wins is only adequate because the amounts are small enough that nobody audits. That is a reason it has not bitten, not a reason it is correct.

The stated problem was social rather than arithmetic, and I did not really solve the social half. The app makes the maths continuous and unambiguous, which removes the argument about how much — but somebody still has to be the one who asks. Making settlement feel like something the group does, rather than something one person chases, is the actual product problem and it is still open.

Accuracy for three models on the same four age classes — HOG+SVM 0.7106, HOG+MLP 0.7294, ResNet18 0.7871
Year
2025
Role
Research and implementation
Built with
PythonPyTorchscikit-learnResNet18

Age Group Detection

Three approaches to the same classification problem, measured honestly.

This one has no interface and was never meant to. It ran from a notebook — a demonstration rather than a product, included because the method is the point.

The problem

Four-class age-group classification, which is less a modelling problem than a comparison problem: the interesting question is not whether a deep model beats a classical one, but by how much, at what cost, and on which failure cases.

The approach

Three pipelines built to be compared like for like.

  • HOG + SVM — the classical baseline.
  • MLP — a shallow learned representation.
  • ResNet18 — transfer learning from a pretrained backbone.

Identical splits, identical preprocessing, identical metrics.

The result

ModelAccuracyMacro F1InferenceSize
HOG + SVM0.71060.70080.02 ms0.10 MB
HOG + MLP0.72940.71810.02 ms10.41 MB
ResNet180.78710.779619.91 ms42.72 MB

ResNet18 wins by roughly eight points over the classical baseline, and pays for it with a thousandfold increase in inference time and four hundred times the model size. Whether that trade is worth making depends entirely on where the thing has to run — which is the answer the comparison existed to produce.

What was hard

Class imbalance across the age bands, and resisting the temptation to report the accuracy figure that flattered the model I'd spent the most time on.

What I'd change

The comparison is sound and the reporting around it is thin. Accuracy and macro F1 across four classes hide the thing that matters most — which classes each model confuses, and whether all three fail on the same faces. A confusion matrix per model would have said more than any of the headline figures, and it is the first thing I would add.

The repository reports no per-class breakdown and no treatment of the class imbalance beyond acknowledging it. That is the real gap. Macro F1 is chosen precisely because it does not let a majority class carry the score, but choosing a fairer metric is not the same as addressing the imbalance, and I would not want the metric to be mistaken for a fix.

The largest omission is bias. Age classification from faces is known to perform unevenly across skin tone and gender, and I did not measure it. Any conclusion here about which model is better is a conclusion about aggregate accuracy on one dataset, which is a much narrower claim than it looks — and on this particular task, the narrower claim is the only one worth making.