Back to the Archive

LibraryThe daily build10 min read

Quote Seal: sealing what a quote says, not the file it arrived in

A remodeling contractor has twice had a client edit an emailed quote and argue the edited number was the agreed price. Every free tool for this hashes the file, which is the wrong thing to hash. We built the one that hashes the numbers.

A contractor’s quote seal encloses itemized prices and an agreed total, while an altered replacement number is crossed out.

A remodeling contractor with two employees posted a question to r/smallbusiness three days ago. Twice this year, a client had taken a quote he emailed, changed a number in it, and then argued that the changed number was the price they had agreed on. Both times it was a Word file. Both times, in his words, it "turned into a headache I didn't need."

His own fix was to start converting everything to PDF before it left his outbox, "so at least it's not sitting there begging to be edited." His own assessment of that fix is the most useful sentence in the thread: "Feels like it helps but I'm sure someone determined could still mess with it."

He is right. And what he actually asked for was wording. "Is there something in how you word the quote or lock it down that actually holds up when a customer tries to play games?" He wanted a sentence to add to a template. He wanted, as he put it, to "stop bleeding hours arguing over numbers I know I typed."

The answers he got, and why none of them fit

The thread's replies sorted into four piles, and it is worth walking them because the gap between them is where today's build lives.

The best-received answer was procedural: PDFs plus a signed acceptance, plus a quote number, an issue date, a validity period, and a note that only the latest version sent from your email is valid. That is genuinely good practice. It is also a description of paperwork discipline, not a detection mechanism. It makes a dispute easier to argue after it starts. It does nothing to end one in ten seconds.

The second pile was "Docusign," with someone helpfully adding that Docuseal is a free alternative. E-signature is a real product category and it solves a real problem, but it solves a different one. A signature proves that a specific person assented to a specific document at a specific time. That is heavier machinery than this contractor needs, and it puts an account, a login, and a signing ceremony between a handyman and a two line estimate for replacing a subfloor. Most of his quotes will never be signed at all. They will be texted, printed, or read aloud over a phone.

The third pile was "don't work with anyone who does that." Emotionally satisfying, and useless. He has already done the work when the argument starts.

The fourth pile was the honest one, and it was buried. Someone pointed out that PDFs are also trivially editable, which is true and quietly demolishes the top answer. Someone else pointed out that the original is still sitting in his sent folder, so the evidence already exists. That second one is right about the evidence and wrong about the fight. He has the evidence. He is still losing hours. The dispute is not being adjudicated by a judge who will subpoena his mail server. It is being adjudicated by two people on a phone call, and "I have it in my sent folder" is a claim the other party can simply keep disagreeing with, at length, for free.

Nobody proposed a fingerprint of the content, printed on the document itself, that either party can independently re-check.

The thing that is actually wrong with the obvious approach

There is prior art here, and reading it first is what produced the build.

Several free tools already do file tamper detection. Drop a file in, get a SHA-256 digest out, compare it later. There is even an open standard for it, letsseal, which describes itself as the open standard for proving any file is real, unaltered and sealed. The underlying hash function is a well specified, decades-old primitive: NIST's FIPS 180-4.

Every one of those tools hashes a file. For this problem, that is the wrong thing to hash, in two separate ways.

It breaks on things that are not cheating. Open the PDF and re-save it. Print it and scan it. Let a mail gateway re-encode the attachment. Let the client open it in Preview, or Google Docs, or a phone. The hash changes. And critically, it changes in exactly the same way, and by exactly the same amount, as it changes when the client edits the tile line from 2,150 to 1,150. A detector that fires identically on "she printed it" and "she changed a price" cannot distinguish them. So the operator sees a mismatch, investigates, finds nothing, and within about three false alarms stops looking. A tamper detector nobody trusts is worse than no tamper detector, because it costs the same hours it was supposed to save.

And in the real dispute there is frequently no file at all. This is the part the existing tools miss completely. The client is holding a printout. Or they forwarded the quote to their spouse, who retyped the relevant lines into a new email. Or they are reading a number off a phone screen on a job site. There is nothing to drop into a file hasher. The argument is happening in a medium where the file does not exist any more.

So the substitution that makes this work: seal the semantics, not the bytes. Normalize the quote down to its line items and its money, throw away everything that is presentation, and hash that.

The consequence is the entire product. Retyping matches. Reformatting matches. Changing the font matches. Word turning the document into a PDF matches. Writing $1,200.00 where you originally wrote 1200 matches. Printing it, faxing it, and typing it back in on the other end matches. But changing a digit breaks it. Adding a line breaks it. Deleting a line breaks it. Reordering two lines breaks it, even when the total is identical.

That is a detector that only fires on the thing the contractor actually cares about.

What we built

Quote Seal is free, runs entirely in your browser tab, requires no account, and sends nothing anywhere.

It has two modes. In Seal a quote, you paste your line items and it returns a twelve character code in three groups, along with a plain text stamp block to paste under the last line of the quote before you send it. In Check a quote, you paste the copy that came back plus the stamp, and it tells you whether the numbers still match.

Three design decisions are worth explaining, because each one came from thinking about where this tool gets used rather than how it gets built.

The code is designed to be transcribed by a human, off paper. It uses Crockford's base32 alphabet, which deliberately excludes the letters I, L, O and U, so that no character in a printed code can be confused with another or accidentally spell something. The input side is correspondingly forgiving: capitals, missing hyphens, and wrong grouping all still parse, so 57yw ccc d n4ns reads the same as 57YW-CCCD-N4NS.

There is a phonetic spelling under the code. 5-7-YANKEE-WHISKEY / CHARLIE-CHARLIE-CHARLIE-DELTA / NOVEMBER-4-NOVEMBER-SIERRA. This exists because a trades operator settles this kind of thing on a phone call standing in a driveway, not in a threaded email exchange. If the tool cannot be used out loud, it cannot be used at the moment the argument is actually happening.

When it breaks, it names the line. This is the part I would defend hardest. Reporting "DOES NOT MATCH" across a forty line quote is a dead end dressed up as an answer, because the operator's very next question is "which one," and the tool has just refused to say. So the stamp carries a short fingerprint per line, and a broken check reads:

Line 4, "Install tile, labor", does not match the sealed copy.

with both codes and both totals side by side. It also distinguishes a changed line from an added one and from a removed one, and it says which. And when it genuinely cannot tell you which line, because you pasted only the bare code and not the full stamp block, it says that too, and tells you what to paste to get a better answer instead of leaving you at a wall.

Architectural lessons from the build

Two edge cases are worth noting, because both point to foundational principles in building resilient client-side tools.

The first was a pluralization mismatch: rendering "1 lines" on a single-line quote across several UI states while pluralizing correctly elsewhere. This is a classic symptom of assembling text strings directly inside UI components rather than in testable domain logic. When UI components format their own strings without dedicated unit tests, copy defects slip through easily. The robust fix was not patching JSX strings directly, but moving string formatting and count-dependent phrasing into a pure TypeScript formatting module with unit test coverage, letting components simply render strings they are handed. Keeping presentation completely decoupled from formatting arithmetic guarantees consistent rendering.

The second was a verification false-positive around numeric edge cases. When scanning rendered pages for unintended NaN or Infinity tokens, testers often flag false defects when user-provided descriptions literally contain those substrings (for instance, a line item description mentioning "Infinity pool maintenance"). Echoing user strings back to the page can trigger naive assertions. The meaningful test is feeding malicious or extreme numeric inputs into currency amounts and verifying computed totals. Under strict testing, values like -0, 1e999, 0x10, and twenty-one digit integers all parsed or rejected cleanly without polluting totals.

What it cannot do

Every tool built in a day has an edge it does not handle, and naming them is the difference between a build log and an advertisement.

  • It is not a timestamp. Nothing here proves _when_ you sealed anything. If a client claims you sent an entirely different quote, carrying its own perfectly valid seal, this does not adjudicate that. It proves a document either does or does not match a given seal. That is all it proves.
  • It is not a signature and it is not a contract. It is evidence, not assent. If you need proof that someone agreed, you need the e-signature product, and the thread was right to name one.
  • It does not prevent editing. It makes editing detectable. Anyone can still change your quote. They just cannot do it without the code failing.
  • It only helps if the stamp actually travelled. Forget to paste the block in before sending and there is nothing to check against later. The tool cannot detect its own absence, and no amount of interface design fixes that.
  • Sixty bits is a transcribability choice, not a security one. The code is short because a human has to read it down a phone. That makes an accidental collision impossible in practice and a deliberate one expensive, but a motivated forger holding this same free tool is a threat model it does not defeat.
  • It reads line items and money. Photographs, drawings, scanned attachments, and scope described in paragraphs are outside it.

What to do about it

If you send quotes and you have ever had this argument, the useful move is not to buy anything. It is to make the next quote you send self-verifying, which takes about fifteen seconds: paste your line items in, copy the stamp block, put it under your last line.

The value is not really cryptographic. It is that the argument changes shape. Today the exchange is "that's not the figure I sent you" against "yes it is," which has no floor and can go on for an hour. With a stamp on the document, it becomes a thing both people can check in ten seconds, from the copy in their own hand, without either of them having to be believed. Most of the time the number will match, the client will have misread it, and everyone moves on.

That is what the contractor was asking for. He described it as wording. It turned out to be arithmetic.