A purchase order approver on AWS for a few dollars a month
Someone on your team needs to buy something. They email you. You are on a site visit, so the email waits. Two days later they buy it anyway on a personal card, and now you have a receipt to reimburse, no purchase order, and a budget line that quietly went over without anyone noticing until the month closed. This post walks through the design of a small system that takes the request wherever it arrives, checks it against the budget you already keep in a sheet, approves the routine small ones on the spot, and puts everything else in front of you with the numbers already gathered.

Key takeaways
- Three ways to ask: a reply-to inbox, a short form, and a Slack shortcut. All three land as the same request.
- Every request ends in one of three states: approved, waiting on a person, or returned for a missing detail.
- The budget lives in a sheet you already keep. Changing a limit is an edit, not a deploy.
- It can approve small routine spend only. Above your line, a person decides — always.
- Designed on AWS for about $3 a month at typical small-business volume.
The whole system on one page
Before any code, here is the shape of what we are designing.
- App integration
- Machine learning
- Analytics
- Front-end & mobile
- People
What you set up once (the outside)
- Purchase requests. Three ways to ask, all covered in Part 2. The first is an inbox — somebody emails a dedicated address the way they already email you. The second is a short form on your intranet, five fields long. The third is a Slack shortcut for the people who live there. All three end up as the same record: who asked, what they want, which budget line it belongs to, how much, and which vendor.
- A budget sheet. One tab you almost certainly already keep. Each row is a budget line — “Workshop consumables”, “Site fuel”, “Software” — with the amount for the period, the amount committed so far, the people allowed to spend against it, and the auto-approve threshold for that line. A second tab lists approved vendors. The sheet is the source of truth. If a line is not in the sheet, the system will not invent it; it returns the request and asks which line it belongs to.
- An approver. The person who owns the money. They get a short message per request — what, how much, which line, how much is left in that line after this, and whether this vendor has been used before — with two buttons: approve and decline, plus a box to ask a question. They are not reading a form. They are reading a number and a name.
What runs on every request (the inside)
- The intake. Three sources feed one queue. An email is messy prose; a form is already structured; Slack is somewhere in between. The intake turns all three into the same five fields, and this is the one place a model earns its keep — reading “can I get another two boxes of the blue gloves from Medline, same as last time” and coming back with a vendor, a quantity, and a best-guess budget line. If it cannot fill a field with confidence, it leaves the field empty rather than filling it in with something plausible.
- The checker. Runs once per request and asks four plain questions against the sheet, in order. Does this budget line exist? Is there room in it for this amount? Is this vendor on the approved list? Has this same request already come through today? Each answer is a yes or a no with the number that produced it, so the decision can be read back later. None of this is the model’s job — it is arithmetic against a sheet, and plain code does arithmetic more reliably than any model will.
- The router. Reads the four answers and picks one of three outcomes. All four yes and under the line’s auto-approve threshold: issue the purchase order, write it to the ledger, tell the requester. All four yes but over the threshold: send it to the approver with the budget position attached. Any no: return it to the requester saying exactly which check failed and what would fix it. Nothing is ever silently dropped.
One request, end to end
Here is the same system as a single line, which is how it actually feels to use.
- App integration
- Machine learning
- Analytics
In plain words
Your workshop lead needs gloves. She emails the requests address: “another two boxes of the blue nitriles from Medline please, same as last month, about 90 dollars”. The intake reads that into five fields: requester her, item two boxes of nitrile gloves, vendor Medline, amount 90 dollars, budget line Workshop consumables. The checker opens the sheet. Workshop consumables exists. It has 1,240 dollars left this quarter, so there is room. Medline is on the approved list. Nobody has asked for the same thing today. All four are yes, and 90 dollars is under the 250-dollar auto-approve line you set on that row. So the router issues the purchase order, emails it to Medline, replies to her with the PO number, and moves the committed column from 760 to 850. Total elapsed time: under a minute, and you never saw it.
The following week she needs a replacement bench grinder, 640 dollars. Same three checks pass, but 640 is over the line. So instead of issuing it, the router sends you one message: what it is, who asked, which line, and the sentence that actually matters — “Workshop consumables has 1,150 left this quarter; this would leave 510.” You tap approve on your phone at the traffic lights. The PO goes out. That is the whole difference between this system and an inbox: the inbox gives you a request, and this gives you a request and the consequence of saying yes to it.
Design rules that shaped every decision
- A person decides anything above the line. The system can issue small routine spend and nothing else, and the line is a number you write in the sheet.
- The sheet is the source of truth. The system reads budget lines, limits and vendors from it and never invents one that is not there.
- Every answer ships with the number behind it. “No room” always comes with how much is left and how much was asked for.
- Empty beats plausible. When the intake cannot read a field confidently, it leaves it blank and asks, rather than guessing a budget line.
- One request, one purchase order. A fingerprint on requester, vendor, amount and day makes a resend land on the same request instead of a second one.
- Nothing is silently dropped. A failed check returns to the requester saying which check failed and what would fix it.
Why this shape
Most small teams run purchase approval one of two ways. Either everything goes through one person’s inbox, which works until that person is on a roof or on a plane, and then the whole business waits on them for a 90-dollar box of gloves. Or there is no process at all, people buy what they need and hand in receipts, and the budget is a thing you discover at month end rather than something you steer. The first is a bottleneck; the second is not a system, it is a hope.
The shape above keeps the budget where the business already keeps it — a sheet — but puts a small, fast, boring layer in front of it. The layer is allowed to do exactly one consequential thing on its own: issue small, routine, in-budget, known-vendor purchase orders. Everything else it turns into a one-tap decision with the number attached. It does not need to be clever to be worth having. It needs to be quick, honest about what it does not know, and completely unwilling to spend money above a line you drew.
The next four posts walk through each piece in turn: how a purchase request arrives, how it gets checked against the budget, how an approval reaches the right person, and how the money actually gets committed. One diagram per post, a cost breakdown, and an engineering reference at the end.
All posts