C

Coupaso

B2C
live

Receipt photos turned into verified, payable cashback.

Lead full-stack engineer2024 – 2026Core of a 4-dev teamAuribises TechnologiesCashback / fintech
Next.js 14
Firebase Functions
TypeScript
BigQuery
OpenAI
Google Vision
GCP
Client product. Source is private.

Overview

A cashback app where people upload photos of their shopping receipts, an AI pipeline reads and checks the items, and an admin team approves the rewards and pays them out. I owned the admin dashboard and the whole cloud-functions backend.

On a four-person team, I owned the Next.js admin and the Firebase Functions backend end to end: the receipt AI pipeline, the BigQuery warehouse, the move to TypeScript, and the payout flow. Payments and referral logic were a teammate's.

How it works

1

Receipt photo

Uploaded by the user, saved as WebP

2

Google Vision OCR

Raw document text and labels

3

Model extraction

Items pulled from the text only

4

Code checks

Fraud and suitability rules

5

Review queue

Admin approves with the evidence

A receipt photo goes through OCR, then the model pulls out the items, then a set of plain code checks, before it lands in a human review queue.

Engineering challenges

01Reading receipts people actually upload

Problem. People snap receipts however they happen to grab them. Crumpled, half in shadow, shot at an angle, sometimes in another language. We were paying real cashback off those photos, so rough extraction wasn't an option. If an amount got misread, or someone quietly edited a total or sent the same receipt twice, that was money out the door.

Approach. I split the job in two instead of handing the whole image to one model and hoping for the best. Google Vision does the OCR and lifts the raw text off the photo. Only that text goes to a cheaper language model to pull out the line items, which kept it quick and cut the cost a lot. The model never gets the last word either. Its output runs through ordinary code checks first, and the review screen puts the warning signs right in front of the admin: what the user typed versus what we read off the receipt, and whether the receipt date even falls inside the claim window.

Outcome. By the time a receipt reaches a person, the data and the red flags are already sitting next to it, so the team spends its time on the odd ones instead of retyping every receipt.

02Getting the data somewhere we could actually query

Problem. Everything sat in Firestore. That's great for the app and frustrating the moment anyone wants analytics that cross collections or look back over time. The reports the team kept asking for just weren't questions Firestore answers well, and the easy fix of one export script per collection was going to turn into a mess fast.

Approach. So I wrote a single sync layer rather than a script per collection. One generic function takes the row type and every collection runs through it. Writes go in as parameterised MERGE queries keyed on the document id, which means running a sync twice never doubles a row or wipes what's already there. It all runs on a task queue with retries, and every run leaves its own audit record, so when something breaks I can see exactly which run and why instead of guessing.

Outcome. The team ended up with a warehouse that stays in step with the app across every core collection, and any sync that misbehaves is traceable.

03Keeping the AI cheap and easy to tweak

Problem. The extraction prompt needed changing constantly as new shops and odd receipt layouts turned up. Pushing a deploy for every little edit was slow, and it locked out the non-engineers who actually understood the receipts. Left alone, the AI bill would have climbed quickly too.

Approach. I pulled the prompt out of the code and into config, then put together a small admin playground so anyone could edit and test it without waiting on a release. On cost, I added a quick cheap check up front and converted images to WebP before the heavier work ran, so we weren't paying to deeply analyse receipts that were never going to qualify anyway.

Outcome. Changing the prompt became a config edit instead of a deploy, and the AI cost stayed flat even as more receipts came through.

What I'd do differently

  • I'd give the extraction a confidence score so the clearly good receipts can approve themselves and only the shaky ones land in the queue. Right now a person still looks at all of them.
  • The warehouse has no schema-drift detection yet. If someone renames a Firestore field it quietly needs a manual fix, and I'd rather it caught the mismatch and complained loudly.