Part 1 of 7 · Expense approver series ~5 min read

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.

System architecture: three sources, three pieces inside AWS At the top, three external boxes in a row. Far left, "Claims in" — the three ways a staff member submits an expense claim: a short web form, a forwarded receipt email, and a chat upload, each carrying an amount, a date, and a receipt image. Centre, "Policy and voice" — a Drive folder with a policy doc covering per-category limits, allowed categories, receipt rules, and who approves what, plus a voice doc with the message templates the approver sees. Far right, "Approvers" — the managers and finance leads who decide each claim; the approval request lands in their email or chat. Each connects via an arrow to the AWS account container below. Claims have an outgoing arrow into AWS. Policy and voice feed in to ground every decision. Approvers receive an approval card with the claim, the amount, the category, whether it is in or out of policy, a link to the receipt, and Approve, Reject, and Ask buttons. Inside the AWS account are three components in a row, mirroring the layout above. On the left, the Claim intake — receives each claim, reads the receipt via Textract, sorts the category via Bedrock, and writes a clean claim record. In the middle, the Checker — compares the amount against the per-category limit in the policy doc; picks one of four outcomes: clear, confirm, review, or reject. On the right, the Routing — sends the approval request to the right person, respects quiet hours, and tracks the decision per claim. Internal arrows flow left to right. A note at the bottom reads: a human approves every payment — the system never moves money on its own. Claims in form, email, chat Policy and voice limits, rules, templates Approvers where decisions land claims in grounds approval with reason AWS account Claim intake read receipt, sort, write claim record Checker picks one of four: clear, confirm, review, reject Routing email or chat, respects quiet hours claim outcome A human approves every payment — the system never moves money on its own.
Fig 1. Three sources outside, three pieces inside AWS. Claims flow in from a web form, a forwarded email, and a chat upload. The Checker compares each against policy and picks one of four outcomes. Routing sends the right request to the right person, who makes the call.

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