An expense approver on AWS for a few dollars a month
A small business runs on a steady drip of small spends. The $12 client coffee. The $30 airport taxi. The $400 software seat somebody bought because the trial ran out on a Friday. Each one comes back as a claim with a blurry receipt, and somewhere a manager is supposed to look at it, decide if it’s fair, and approve it for payment. In practice the manager rubber-stamps the pile once a week, or the pile sits for a month and the team grumbles. This post walks through the design of a small system that reads each claim, checks it against your policy, clears the easy ones for a one-tap confirm, and sends the rest to the right person with the reason — while a human still approves every single payment.
Key takeaways
- Three ways a claim gets in: a web form, a forwarded receipt email, and a chat upload.
- Every claim ends in one of four outcomes: clear, confirm, review, or reject.
- Per-category limits live in a policy doc: meals $40/day, taxis $30/trip, software needs sign-off.
- A human approves every payment. Nothing is reimbursed automatically — ever.
- Designed on AWS for about $2.40/month at typical small-business volume.
The whole system on one page
Before any code, here’s the shape of what we’re designing.
What you set up once (the outside)
- Claims in. Three ways a staff member submits a claim. A short web form (pick a category, type an amount, snap a photo of the receipt). A forwarded receipt email (forward the vendor’s emailed receipt to a dedicated address and the system takes it from there). And a chat upload (drop a receipt photo into a team chat channel). All three land as one claim record, covered in Part 2. Every claim carries an amount, a date, a category, the claimant, and a receipt image.
- A policy folder. Two short Google Docs in a Drive folder. The policy doc covers the per-category limits in plain prose — “meals up to $40 a day, taxis up to $30 a trip, software of any amount needs a manager’s sign-off, anything over $250 goes to finance.” It also lists which categories are allowed, when a receipt is required, and who approves what. The voice doc holds the message templates — what the approval card and the reject note actually say.
- Approvers. The people who decide. A manager for the everyday in-policy claims, a finance lead for the larger ones. Each approver has a chat ID (so the request is a private message) or, if chat isn’t set up, an email address. The approval card lands with the claim amount, the category, whether it’s in or out of policy and why, a link to the receipt, and Approve, Reject, and Ask buttons.
What runs on every claim (the inside)
- The claim intake. Each new claim, however it arrived, is normalized into one record. Textract reads the receipt image and pulls out the amount, the date, and the vendor. A small Bedrock Haiku 4.5 call sorts the receipt into a category (“this looks like meals,” “this looks like a taxi”) so the claimant doesn’t have to get the category perfectly right. The cleaned claim is written to DynamoDB and the receipt is stored in S3.
- The checker. Reads the claim. Compares the amount against the per-category limit in the policy doc. Picks one of four outcomes. Clear: in policy and small — the manager gets a one-tap confirm card. Confirm: in policy but worth a quick look (close to the limit, or the category that always wants eyes) — same one-tap confirm, with the detail shown. Review: over a limit or out of policy — full approval card to the right person, with the reason spelled out. Reject candidate: clearly outside policy (a banned category, no receipt where one is required) — the system proposes a reject for a human to confirm. The checker doesn’t call a model on the limit comparison; the math is plain Python.
- Routing. Reads the voice doc, formats the approval card for the outcome and category, and sends it to the resolved approver. Chat messages go through a chat webhook; email goes through SES outbound. Both honor quiet hours so a request doesn’t land at 2am. Every send and every decision writes a row to DynamoDB so the trail is complete. A weekly digest summarizes what was approved and what’s still waiting. A monthly summary writes a board-ready paragraph: spend by category, top claimants, anything that needed a second look.
In plain words
Sam took a client to lunch and spent $36. He snaps the receipt into the web form and picks “meals.” The intake reads the receipt: $36.00, today, “Olive & Vine.” The category sorts to meals. The checker sees meals has a $40/day limit and $36 is under it, with a receipt attached — that’s a clear. His manager Dana gets a one-tap card in chat: “Sam — meals $36.00, in policy (limit $40), receipt attached. [Approve] [Reject] [Ask].” Dana taps Approve. The claim is written to the payable sheet your bookkeeper reads, and Sam gets a note that it’s approved. The whole thing took Dana three seconds.
Now Sam buys a $400 software seat. The checker sees software needs a sign-off regardless of amount — that’s a review. The request goes to the finance lead, not Dana, with the reason: “software, $400, policy requires finance sign-off.” The cost of running all this is about $2.40 a month at SMB volume. The cost of not running it is the rubber-stamped pile where the one claim that should have been questioned sailed through with the rest.
Design rules that shaped every decision
- A human approves every payment. The system reads, checks, and routes — it never moves money.
- Four outcomes, always. Clear, confirm, review, reject. There is no fifth.
- Every approval card ships with the reason — in policy or not, and why. The approver never has to dig.
- Quiet hours are respected. An approval request that lands at 2am is a worse request.
- The policy lives in Drive. Changing a limit or an approver doesn’t need a deploy.
- Every claim and every decision is logged. Audit a reimbursement next year and the trail is there.
Why this shape
Most small teams handle expenses in one of three ways: a shoebox of receipts reconciled monthly, a spreadsheet nobody enjoys filling in, or a manager’s memory of who spends what. The shoebox is slow and the team waits weeks to get paid back. The spreadsheet drifts the moment someone forgets a row. And the memory fails the day the claim is bigger than usual and nobody questions it because they’re busy.
The setup above keeps the policy in a doc the team already edits, but adds a small system that reads each claim against that policy the moment it arrives and does the easy work for you. The in-policy lunch is a one-tap confirm, not a meeting. The over-limit software buy is routed to the person who should actually see it, with the reason attached. And nothing is ever paid without a human tapping Approve — the system makes the decision fast, it doesn’t make the decision for you.
The next four posts walk through each piece in turn: how a claim gets submitted, how it gets checked against policy, how it finds its approver, and how it gets paid once approved. One diagram per post. A cost breakdown and a final engineering reference at the end.
All posts