◉CLAUDE LABJP
●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…●2.1.300 — Adds virtualText for Mod prompt decorations; background commands still running 1.5 seconds after exit are now force-killed●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 46 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●CACHE — Prompt cache reads on Sonnet 5.5 dropped from $0.20 to $0.10 per million tokens (Oct 7)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude.ai
◉ Claude.ai/2026-10-15Beginner

Let Claude Draft the Quote's Wording, Keep the Numbers in Your Own File — A Small Source of Truth and a Check Script

How a freelancer can have Claude polish the wording of quotes and invoices while the amounts stay in a JSON file and get verified by a short Python script.

Claude51quoteinvoicefreelance4workflow41verification5Python22

The night before I send a quote for a client's website, the items are settled, the prices are settled, and all that remains is the short note that goes with the document.

Claude is very good at that note. It returns something polite but not stiff in a few seconds.

But when I put the amounts into the request as well, I noticed that numbers could quietly move around in the reply. Not by a digit — the figure sitting next to "total" would turn out to be the subtotal, or the pre-tax amount.

A sentence I can fix by rereading it. A number I can't, not reliably. That became the starting point for where I draw the line.

Don't hand over the numbers at all

I'm not doubting Claude's arithmetic. Addition and sales tax usually come back correct.

The trouble comes earlier: once amounts are scattered through a piece of prose, I stop knowing which one is the source of truth. So I split the roles.

RoleOwnerWhere it lives
Items, unit prices, quantities, tax rateMeA JSON file I keep
Subtotal, tax, totalA scriptThe Python below
Cover note, subject line, closing wordsClaudeThe chat draft
Final cross-checkScript, then meRight before sending

The quote PDF itself is produced from the same kind of JSON by a separate step. This article covers only the part where Claude writes the words.

The source of truth is one small JSON file

Nothing elaborate: item names, unit prices and quantities.

{
  "items": [
    {"name": "Top page", "unit_price": 120000, "qty": 1},
    {"name": "Inner pages", "unit_price": 30000, "qty": 3}
  ]
}

Save it as quote.json. When an amount changes, I change this file and nothing else. I never go back to the chat to say "rewrite it with this new amount."

Ask Claude with holes where the numbers go

I give Claude the request with the amounts masked:

Write a cover note for a quote to send to a client, in polite plain English, 4 sentences max.
- Subject: Quote for corporate website production
- Two items (top page / 3 inner pages)
- Do not write any amounts; put "[TOTAL]" where the total goes
- Put "[VALID UNTIL]" where the expiry date goes
- End with a short line inviting questions

The draft comes back with "[TOTAL]" and "[VALID UNTIL]" intact. The only number that goes into the first hole is the one the script prints.

The check script: catch numbers that slipped in

After I fill in the draft, I run a script that does three things: it computes subtotal, tax and total from the JSON; it collects every number of four or more digits in the draft; and it stops if any number can't be derived from the JSON.

import json, re, sys
from decimal import Decimal, ROUND_DOWN
 
 
def load(path):
    with open(path, encoding="utf-8") as f:
        return json.load(f)
 
 
def calc(items, rate=Decimal("0.10")):
    subtotal = sum(Decimal(i["unit_price"]) * Decimal(i["qty"]) for i in items)
    # Round tax once per rate, not per line (rounding down here)
    tax = (subtotal * rate).quantize(Decimal("1"), rounding=ROUND_DOWN)
    return subtotal, tax, subtotal + tax
 
 
def amounts_in(text):
    found = set()
    for m in re.finditer(r"[0-9][0-9,]*", text):
        s = m.group().replace(",", "")
        if len(s) >= 4:  # ignore fragments of dates and phone numbers
            found.add(int(s))
    return found
 
 
def main(json_path, draft_path):
    doc = load(json_path)
    subtotal, tax, total = calc(doc["items"])
    allowed = {int(subtotal), int(tax), int(total)}
    allowed |= {int(Decimal(i["unit_price"]) * Decimal(i["qty"])) for i in doc["items"]}
    allowed |= {int(i["unit_price"]) for i in doc["items"]}
    with open(draft_path, encoding="utf-8") as f:
        draft = f.read()
    stray = sorted(amounts_in(draft) - allowed)
    missing = sorted({int(total)} - amounts_in(draft))
    print(f"subtotal {int(subtotal):,} / tax {int(tax):,} / total {int(total):,}")
    if stray:
        print("not in the source:", [f"{n:,}" for n in stray])
    if missing:
        print("total missing from the draft:", [f"{n:,}" for n in missing])
    sys.exit(1 if stray or missing else 0)
 
 
if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Save it as check_amounts.py and run:

python3 check_amounts.py quote.json draft.txt

With the JSON above, the subtotal is 210,000, tax 21,000 and total 231,000. A draft that says 231,000 passes. A draft with 213,000 — two digits swapped — stops with two lines: one number not in the source, and the total missing. Swapped digits are exactly the kind of slip eyes slide past and a script catches.

Why round in one place only

Computing tax per line and summing can leave the total off by one yen, which is why calc rounds in a single spot.

Whether you round down or to nearest depends on your agreement with the client and the format you use. I round down here; swap the single rounding= line to match your own practice. What matters is deciding where the rounding happens once — the same idea as not scattering amounts through the prose.

Small rules that settled in

As an indie developer who gradually started taking on client sites, I've accumulated a few rules:

  • Amounts change only in quote.json; I never ask the chat to "redo it with this amount"
  • Client and contact names are masked when I ask Claude for wording
  • Once the script passes, I read the whole thing aloud one time

The third stays my job, not the script's. Reading aloud tends to surface "would this phrase sound off to the client?" before it surfaces any number problem. Numbers belong to the source file, words to Claude, the last read to me. I try to keep that split even in busy weeks.

What to do next

Pick one quote you sent recently and copy only its items and prices into quote.json. For the next one, start from a request that leaves the "[TOTAL]" hole open — I think you'll feel the difference right away.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◉ Claude.ai2026-06-29
Three Ways to Start with Claude — Browser, Desktop, and Mobile, and How to Use Each
A clear walkthrough for getting started with Claude on the browser, desktop app, and iPhone/Android — plus how to combine all three so your day flows smoothly. Written from the perspective of a solo app developer who lives across all three.
◉ Claude.ai2026-10-09
I Kept Rewriting My First Request to Claude Until I Handed It One Page About My Work
If Claude's answers keep missing the mark, the cause may not be your wording. It may be that you never explained your work. Here is how a non-engineer can write a one-page work description today and fold it into the first request.
◉ Claude.ai2026-10-03
My Usage Limit Ran Out Before Noon, and the Reason Was Conversation Length, Not Message Count
When the usage-limit banner shows up on Pro or Max sooner than you expected, here is the order I check: the two bars in Settings > Usage, the length of the conversation and its attachments, then the model and effort level. The morning I got cut off before lunch, and the lines I drew afterwards.