A tip calculator, a stopwatch, and a lot of pesos.
TL;DR: I built a USD/MXN tip calculator with Cursor for the Build a Mini App challenge during Hacktoberfest and MLH Global Hack Week. One prompt, one commit, no corrections, and the site was live on GitHub Pages eight minutes after I first asked for a prompt. The prompt is in this post so you can run the same test. What took longer than the build was writing the prompt.
8:25 in the morning. The challenge brief says "something small but real, a to-do list, a tip calculator." I'm sitting there thinking a tip calculator is the most boring thing on earth, and then I remember I'm in Mexico every few months doing pesos-to-dollars math on a napkin.
So, Propina. Spanish for tip.
earlgreyhot1701D
/
propina
Dual-currency tip calculator for USD and MXN with a manual exchange rate, split-the-bill math, and live updates. Single HTML file, no frameworks, no build step, no API. Styled with Clew Labs brand tokens. Built with Cursor for the Build a Mini App challenge. AI Assisted. Human Approved.
Propina
Propina is a single-page tip calculator: enter a bill, choose a tip percent, and split the total among people, with a USD/MXN toggle that shows every result in both currencies. The MXN-per-USD exchange rate is a manual field (default 18.00). It is not fetched from a live API, so you set the rate yourself. Open index.html in a browser, or visit the GitHub Pages site for this repository.
Live: https://earlgreyhot1701d.github.io/propina/
Built with Cursor for the Build a Mini App challenge (Hacktoberfest / MLH Global Hack Week 2026). One prompt, one shot, eight minutes from first prompt to live site.
Clew Labs. AI Assisted. Human Approved.
Live: earlgreyhot1701d.github.io/propina
What it does
A single HTML page. You pick the bill currency, USD or MXN. You type the exchange rate yourself (default 18.00, no live feed, I'll come back to that). Bill amount, tip percent, number of people. Every result shows in both currencies, bill currency first, converted amount under it in smaller type.
| Input | What you get |
|---|---|
| Bill 1,250 MXN at 15% | Tip MXN 187.50 ($10.42) |
| Same bill | Total MXN 1,437.50 ($79.86) |
| Split by 1 | Per person MXN 1,437.50 ($79.86) |
No frameworks. No build step. No API. The exchange rate is manual on purpose, because a live rate means a key, a fetch, an error state, and a dependency I'd have to check isn't dead in six months. For a tip at a taqueria, the rate from this morning's ATM receipt is close enough.
The clock
All times Pacific, all from the chat log and the GitHub commit record.
| Time | What happened |
|---|---|
| 8:25 | Asked Claude for a prompt I could paste into Cursor |
| 8:26 | Revised it: dual currency, my brand tokens |
| 8:28 | Repo created on GitHub |
| 8:32 | First and only commit |
| 8:33 | Pushed. Pages live. |
| 8:39 | I tested the math by hand. It held. |
| 8:53 | Code review found one minor bug. Fixed and pushed. |
Eight minutes from the first ask to a URL I could send someone. I did not redirect Cursor once. Not a single "the total isn't updating, fix it." The first commit, co-authored by me and cursoragent, is the whole build. The second commit, twenty minutes later, is the review. More on that below.
The prompt did the work
Here's the thing I want to be honest about. The whole thing took eight minutes, and most of that was the spec, not the build. I had Claude write the prompt, I revised it twice, and by the time it hit Cursor there was nothing left to decide. This is the prompt, verbatim:
Build a tip calculator as a single index.html file with inline CSS and JavaScript. No frameworks, no build step, no external scripts except Google Fonts.
Currency: a toggle for USD and MXN. Default USD. An editable exchange rate field (default 18.00 MXN per 1 USD, label it as a manual rate, no live API). Show every result in both currencies: bill currency first, converted currency underneath in smaller text.
Inputs: bill amount, tip percent (buttons for 10, 15, 20, plus a custom field), number of people to split with.
Outputs: tip amount, total, and per-person total, each in both currencies. Update live on every input change. Show "0.00" when inputs are empty or invalid, never NaN or a blank screen.
Use textContent, not innerHTML. Validate inputs (no negatives, people must be at least 1, rate must be greater than 0). Format money with two decimals and a thousands separator.
Style it with these tokens exactly:
--field #0A1F2E (page background)
--paper #F1E8D2 (card background)
--turquoise #1FB5B7 (buttons, active toggle, focus ring)
--sienna #E85A1F (custom tip active state and per-person highlight)
--ink #17262E (text on cards)
--paper-warm #E8E0CC (text on dark background)
--muted #647178 (captions, converted amounts)
Fonts from Google Fonts: Cinzel 700 for the title (uppercase), Source Serif 4 400 for body, JetBrains Mono 600 for labels, buttons, and numbers (uppercase labels, letter-spacing 0.12em). Add a subtle 48px grid overlay on the page background using rgba(31,181,183,0.035). Single centered card, max width 480px, mobile-friendly.
Title: "Propina". Footer line in mono: "Clew Labs. AI Assisted. Human Approved."
Add a README.md with one paragraph on what it does, that the exchange rate is manual, and how to open it.
Then: run git init, commit everything, create a public GitHub repo named propina with the gh CLI, push to main, and enable GitHub Pages from the main branch root. Give me the repo link and the Pages link when done.
Do not add features beyond this list.
Look at what's in there. Hex codes. Font weights. Which element gets the orange. textContent not innerHTML. Never show NaN. And the last line, which I think did more than any other: do not add features beyond this list.
Every one of those lines is a decision I'd otherwise be making in a chat window at 8:35 while Cursor waited. I made them at 8:26 instead. The agent didn't have to guess, so it didn't guess wrong.
I think that's the real finding. The speed wasn't Cursor being magic. The speed was that there was nothing left to argue about.
The NaN trap
One line in the prompt said "never NaN or a blank screen." Here's why that line has to be there.
In JavaScript, typeof NaN === "number". So a check like "is this a number" passes on garbage. An empty bill field, a rate of zero, a negative tip, all of them sail through and the page prints $NaN in your brand font. Cursor handled it the way I'd want: validate first with Number.isFinite, reject negatives, require at least one person and a rate above zero, and if anything fails, write formatted zeros with textContent instead of leaving the box empty.
if (!Number.isFinite(amount)) {
amount = 0;
}
That snippet is from the shipped file, not a tutorial. The money formatting is Intl.NumberFormat, two decimals, thousands separators, so 1437.5 shows as 1,437.50 without anyone writing a comma-insertion function at 8:30 in the morning.
The credits ran out before the QA did
I burned through my Cursor credits right after the build. No big deal, it's a free-tier morning, but it meant the final bug review I'd normally ask the build agent for didn't happen there.
So Claude did the review instead. It read all 424 lines of index.html and came back with three findings:
| Finding | Weight |
|---|---|
| Typing 1.5 in Number of People passed validation and split the bill by 1.5 | Minor bug, fixed |
The result boxes updated silently for screen readers; no aria-live
|
Accessibility gap, fixed |
| One CSS rule that could never match | Cosmetic, left alone |
Two lines changed, pushed as a second commit. Everything else it checked (empty inputs, negatives, zero rate, zero people, both conversion directions, the thousands separator) held. So the build history is: one commit from Cursor, one two-line fix from a reviewer that hadn't watched the build. I like that shape better than the pure one-shot story, because the second commit is where the "human approved" part lives.
Two agents, one build log
Three parties, as usual.
Cursor generated index.html and README.md, ran git, created the repo with the gh CLI, pushed, and switched on Pages. One shot. It also wrote a README paragraph with two em dashes in it, which is the only thing I'd call a miss, and that's a style preference, not a bug.
Claude (Fable 5.1) wrote the prompt from my two-sentence ask, revised it when I said "pesos," pulled the commit timestamps for the table above, took the screenshots, spot-checked the math on the live site, did the code review when Cursor's credits ran out, pushed the two-line fix, and made the README banner. It also could not download Google Fonts from its sandbox, so the banner uses stand-in fonts. Same colors, different letterforms. I'm telling you so you don't think the banner is the Cinzel the app uses. (Yes, Fable is overkill here but I threw token caution to the wind!)
Me. Picked the idea. Said "pesos." Handed over the brand guide. Tested at 8:39. Approved.
I did not write a line of code. I wrote one line of spec ("can we make it in pesos or dollars and pesos since I travel to Mexico a lot?") and the rest was direction.
What I didn't need, and would use next time
Cursor's desktop app has a browser pane. You open the running page inside the editor, click an element, and move it, and the agent picks up the change. I didn't touch it this build, because there was nothing to fix. The next time a layout comes back slightly off, that's where the fix happens, not in a chat message describing where the button should go.
What it can't do
- No live exchange rate. You type it.
- No history, no saving, no sharing a split with the table.
- No rounding options. Two decimals, that's it.
- One currency pair. If you need euros, fork it and change two strings.
- The banner in the README is not in the app's real fonts (see above).
Try it
earlgreyhot1701d.github.io/propina. Nothing is sent anywhere. There's no backend to send it to.
Or copy the prompt above into Cursor and see if you get eight minutes too. I'd like to know if the one-shot holds on someone else's machine, or if I got a good roll.
The tip calculator is just the evidence
So, was it Cursor or was it the spec? Both, but not equally. Cursor executed clean, and Iâd use it again tomorrow. The spec is what made clean execution possible, because it left the agent nothing to decide.
What I think is cool here isn't the eight minutes. Plenty of people have one-shot a tip calculator.
It's the shape of the morning. I spent two minutes on the idea, let one agent write the spec, let another agent build it, hit a credit wall, swapped in a third reviewer that hadn't seen the build, and the whole thing is documented down to the minute with a commit log that backs it up. That's the workflow I've been describing for a year, run end to end in under thirty minutes on a Thursday.
One caveat I'm keeping in view. A tip calculator is small enough that a good spec can cover every decision. The next test is whether the same prompt discipline holds on something with a real second layer, like state or a backend. Does it?
I don't know yet. That's the next post.
Quick context if you are new here. I work in the California courts, running court operations for the county. I started building with AI in July 2025 and I have been learning in public ever since. I do not write the code. I direct, the agents generate, I validate and decide. I build the Clew Suite, a set of civic tech tools for making complex systems easier to inspect. That is the lens I am writing from.
Built with AI assistance on October 15, 2026. Times are from the chat log and the GitHub commit record for that morning. Cursor moves fast, so check cursor.com/docs before you quote me on what the browser pane does.
AI Assisted. Human Approved. Powered by NLP.



Top comments (44)
The input rules are the part of this prompt that travels to the next mini app: whole numbers for people, bounds on percentages, what an empty field does. Kept as one block you paste into every build prompt, the next app starts with the 1.5 people case already closed instead of rediscovering it in review.
Agreed, and I usually do build this way. My normal process is block by block with a QA pass after each one, and the input rules live in a block that carries over. This one was an 8-minute challenge, so I skipped my own process and wrote the prompt in one go, and the 1.5 people case is what skipping cost me.
So the lesson Iâm taking isnât âstart a reusable block.â Itâs âthe block exists, use it even when the build is small.â The small ones are where Iâm most tempted not to. But look where it got me! ð
That temptation makes sense, since on a small build the block looks like the slow part. One way around it is making the block the first paste rather than a step you choose: every build prompt starts with the input rules already in it, so skipping them takes a deliberate edit instead of happening by default.
Thatâs a good flip. If the block is already in the template, skipping it means deleting something on purpose, and Iâm much less likely to do that than to not add it. Default on, opt out, instead of the other way around.
Default on is the whole trick for me too. The other half is keeping the block short enough that nobody resents pasting it, so it never feels like the slow part on an eight minute build. Once a rule earns its place in there, it stays, and the small builds get it for free.
Agreed. Short enough not to resent is the part Iâd have gotten wrong. Iâd have kept adding lines until the block was a second spec. So the rule for the block is: a line gets in when a review caught the bug, not before. Whole number for people is in. The penny rule is in. Anything Iâm only worried about isnât.
That entry rule keeps the block honest. It also gives every line a source, so when a model update seems to make one unnecessary, you know which review it came from and can rerun that case without the line before you cut it.
Yes, agreed.
For the next test, I'd keep the app small but add state and externally fixed acceptance checks: invalid exchange rates, fractional people counts, rounding across split totals, and reload behavior. The interesting metric isn't just whether the agent builds in one pass. It's whether the same workflow preserves invariants when inputs become invalid and state has to survive beyond the initial render.
This is close to what I had in mind for round two, and youâve named the checks more precisely than I had. Rounding across a split is the one I hadnât listed. 1,437.50 three ways is 479.17 each, which adds up to 1,437.51, and right now the page just lets that penny float.
âPreserves invariantsâ is the part I want to make sure Iâm reading right. I take it to mean the things that should stay true no matter what the user does: the split always adds back to the total, a bad rate never produces a number, a reload never shows a state the user didnât enter. If thatâs it, then yes, thatâs the metric, and itâs a harder one than âdid it build.â
Iâm not sure yet whether Iâd write those checks into the spec or keep them outside it as a separate list the reviewer runs. The thread above makes a case for the spec. Your phrasing, âexternally fixed,â sounds like youâd keep them out. Curious why.
I wouldn't keep the invariants out of the spec. I'd keep the acceptance checks independent of the build process. The spec should describe what must always hold, while a separate, version-controlled test suite verifies those properties without letting the agent silently change the rules it's being judged against. When a review exposes a missing invariant, update both deliberately: add it to the spec so future work inherits it, and encode it in the tests so the behavior is independently checked. Otherwise, the builder and reviewer can agree on the same incomplete assumption. For the split example, I'd also define how remainder cents are allocated, since preserving the total requires more than just rounding each share independently.
That clears it up. Spec says what must always hold. Tests check it from outside. When the review finds a gap, both get updated on purpose, and the builder doesnât get to edit the thing itâs being graded by. That last part is the one I hadnât thought through. If the same agent writes the code and the tests, it can agree with itself all day.
In my setup that probably means the build agent never touches the tests, and a second agent writes them from the spec before the build starts. Which is close to how I run reviews already, just moved earlier.
The remainder cents point is a decision I hadnât made. 1,437.50 three ways leaves a penny. Someone pays it. Right now the page doesnât say who, it just rounds each share and lets the total drift. Whoever enters the bill? Person one? Random? I donât have an opinion yet and I suspect the right answer is âthe spec has to pick one and say why.â
I'd separate two things here: the spec defines intended behavior, while acceptance checks independently verify it. When a check exposes a missing or wrong assumption, update both deliberately, but don't let the agent under review silently redefine the check to make its own implementation pass.
For the penny, I'd use a deterministic allocation rule that guarantees the shares add back to the total, rather than rounding each share independently. The exact policy matters less than making it explicit and testing it.
Thatâs how I read it. Two things, kept apart on purpose, and the builder never edits the answer key. On the penny, Iâll pick a rule and write it down. Probably âfirst person pays it,â since someone has to and itâs easy to test.
The 1.5 people bug is my favorite part. "People must be at least 1" was in the spec, and the agent did exactly that: 1.5 is at least 1. The spec never said whole number, so the build was right and the spec was wrong, and only a reviewer reading it cold caught it.
We run a small team of agents and keep landing on the same rule: the thing that checks can't be the thing that built it.
Did Claude get the original prompt for the review, or just the file? Curious whether knowing the spec would have made it miss the same gap.
The 1.5 people thing made me laugh. The spec said âat least 1â and nobody pictured a decimal in a people field at 8:26 in the morning. That oneâs on me, not the build.
Claude had the prompt. It wrote the prompt, and it had the whole chat when it did the review. It still caught the gap, I think because it was checking what the page does with bad input, not whether the code matched the spec. Matching the spec would have passed.
How does your team handle it when the spec is the thing thatâs wrong? I donât have a process for that yet.
That detail is the whole answer for me: the review caught it by poking the page with bad input, not by diffing against the spec. A spec can be wrong. Behaviour can't lie about itself.
My take on your question: treat the spec as a hypothesis, not a contract. When the checker finds something the spec never imagined, the fix goes into the spec first and the code second, so every later prompt inherits it. "At least 1" becomes "whole number, 1 to 20". Nothing stops a wrong spec, but one that grows from every caught bug gets wrong less often.
"Spec as hypothesis, not contract" is doing a lot of work in one line. It turns the review from a check on the code into a test of the spec, and it means a caught bug is the spec learning something, not the spec failing.
One thing I'd add: a changelog line when the spec changes. "At least 1" became "whole number, 1 to 20" because of a review on Oct 15. Otherwise in three months I'll wonder who made up the 20.
Thanks for this. Next time I'll just prompt "Write the spec for this project. Make no mistakes." and see how that goes. ð
The changelog line is a great add. A spec with no dates is just vibes with headers, and "who made up the 20" is exactly the question it answers in three months. Also "make no mistakes" is the most optimistic prompt in software. Report back if it works.
ðĪĢð
On your question about apps with state or a backend: that's where I've seen the spec stop covering things, because the bugs are about what nobody thought to write down.
In an expense-tracker app I built with AI tools, the bugs that got furthest all passed a happy-path check. An AI endpoint that accepted any signed-in caller with no daily limit. A Pro feature with no Pro check (Copilot's PR review caught that one). The model adding up totals itself instead of the code doing it. Dark mode missing a few screens with hardcoded colours. None of those would show up in an eight-minute build of a calculator, and none of them crashed.
Matthew's idea of updating the spec with every bug the reviewer finds is close to what I'm starting: one standing list of bug types I check after each AI session.
I made a 5-minute video walking through five mistakes like these, with the check for each (my channel, Coldboot): youtu.be/ewSZTFC-SpY
Four bugs that all passed the happy path and none of them crashed. Thatâs the category an eight-minute calculator build canât surface, and itâs a useful list to have.
Two of yours overlap with a pre-deploy checklist I run (authorization and rate limiting), and the model doing the arithmetic is something I try to keep out of my builds: code writes the numbers, the model writes around them. The dark mode one I would not have thought to check, and the Pro feature with no Pro check is the kind of thing that looks fine in every screenshot.
A standing list of bug types per session is close to where this thread has been heading, with the spec getting the rule and the list getting the check. Iâll watch the video.
The line I would keep from that prompt is the last one: do not add features beyond this list. It turns the spec from a wish into a boundary, and it is probably why there was nothing to correct afterwards. The NaN rule does something similar. It states what failure should look like, which most briefs never mention.
I see the same pattern in client work. When the problem is written down as one thing, with the inputs, the outputs, the edge cases and an explicit list of what is out of scope, the build becomes the short part. When any of those are left open, the decisions get made in the chat window at the worst moment, as you put it.
The code review that still found a bug twenty minutes later is a useful detail too. The spec removed the guessing, but it did not remove the checking. What was the bug, and could the prompt have named it in advance?
âThe spec removed the guessing but not the checkingâ is a good way to put it.
The bug was a decimal in the people field. The prompt said âpeople must be at least 1,â and 1.5 is at least 1, so the build was right and the spec was short a word. Could the prompt have named it? Yes, âwhole numberâ would have closed it. But I only know that because the review found it, which is your point: the spec can name every edge case Iâve already met, and the check is for the ones I havenât.
On the last line, I think youâre right that it did a lot of the work. It also made the review easier, because there was nothing extra to look at. Everything in the file was something the prompt asked for.
The part about writing the prompt taking longer than building the app is what caught my attention. Itâs a good reminder that AI can speed up implementation, but knowing exactly what you want it to build is still the hard part.
Thanks. And the thread above added a wrinkle to that: knowing what you want is the hard part, and even then the prompt said less than I meant in two places. So itâs not just deciding, itâs writing the decision down completely enough that the agent canât fill a gap I didnât know was there.
The line Iâd watch is the 18.00 default. The rest is logic: test it once and it stays right. A default exchange rate is a fact about the world, and it gets a bit more wrong every week nobody touches it. An âas ofâ date next to the field would cover it.
Biased here: we shipped a constant with one letter too many this month and found it four days later.
Thatâs a distinction I hadnât made. The math is logic and stays right once itâs tested. 18.00 is a fact about October 2026 and gets a little more wrong every week. An âas ofâ date next to the field is a small change and it tells the person typing the rate that the default is a starting point, not an answer.
The constant with one letter too many is a good example of the same thing. The build was fast, the number looked right, and nothing checked it because it wasnât logic. Thanks for the share.
Nice small project, and the manual exchange rate field is a smart call since it avoids an API key and a whole class of things that can break. The title question is interesting too. It sounds like most of the time went into writing the prompt, so did you iterate on the spec before sending it, or did it come together in one pass?
Yes, I did iterate on the prompt but only 1 time. Originally the tip app was only going to be in USD but I changed my mind and added MXN since I frequently go to Mexico I knew adding pesos would be a good add. It's interesting, I did spend more effort on the prompt than I did with Cursor.
The penny stops being a rounding question once the shares stop being the source of truth. Keep the bill in integer cents, divide, then hand out the remainder one cent at a time in a fixed order. After that sum(shares) == total holds exactly, for any input, and it fits in one assertion.
A spec line like âformat money with two decimalsâ is already where this goes wrong. It describes the display and leaves the arithmetic open, so the agent picks something reasonable and the drift only shows up on bills nobody tests.
We do this for Portuguese transfer tax, where the split ends up on a deed, so the cent has an owner before anyone asks who pays it.
Thatâs the part I was missing. I was thinking of the penny as a rounding problem when itâs a âwhat are we counting inâ problem. Count in cents, divide, hand out the remainder in a fixed order, and the shares canât drift because thereâs nothing to round.
And youâre right that âformat money with two decimalsâ is the line that let it in. It told the agent what to show, not what to compute, and itâs in the prompt I published. Same shape as the 1.5 people bug: the spec said less than I meant and the agent filled the gap sensibly.
The deed example is a good reminder that this isnât fussy. Someone owns the cent whether the spec says so or not.
the line about showing 0.00 when inputs are empty, never NaN, is the real spec. that is an acceptance test written in plain words, which is why one shot worked.
we do the same for agent builds now. every prompt ends with the edge cases as checkable lines, and the review pass runs against that list instead of vibes. bugs found in review dropped a lot once the list existed.
what was the minor bug the 8:53 review caught, and did the prompt have a line covering it?
The bug was a decimal in the people field. The prompt did have a line: âpeople must be at least 1.â It just didnât say whole number, so 1.5 passed. Which fits your framing. The NaN line was a complete test in plain words. The people line was half of one.
Edge cases as checkable lines at the end of the prompt, with the review run against that list, is close to where this thread has landed. Iâm interested that bugs in review dropped once the list existed. That matches what Iâd expect, but itâs good to hear it from someone measuring it.