Turn a pile of vendor tenders for one building job into a single Excel file that compares every line item side by side, highlights the lowest rate per item, and ranks the vendors from cheapest to dearest.
TenderTally ships as a Claude skill called tender-comparison. The same scripts also run on their own from the command line.
An architect or contractor invites several vendors to bid on the same job. Each vendor sends back a tender. The trouble is that every vendor formats it differently. They name items differently ("800x800 vitrified tile flooring" vs "Vitrified tiles 800x800mm"), group them differently, quote in a different order, and some hand-write the rates on a scanned page with no text layer. Comparing them by hand is slow and error prone.
TenderTally lines them up item by item, keeps nothing hidden, and tells you who is cheapest for each item and overall.
One .xlsx file with a Summary sheet and one comparison sheet per job. Each comparison sheet has:
- one row per item, grouped under section headings
- a Rate column and an Amount column for every vendor
- the lowest rate in each row highlighted green, with a "Lowest bidder" column
- items a vendor did not quote flagged, never dropped
- a totals row, a net payable row that accounts for blanket discounts, and an L1 to Ln ranking with the winner in gold
The image above is built from the fictional example data. The openable file is at tender-comparison/examples/Example Comparison (anonymized).xlsx.
tenders (PDF/scan/xlsx/doc)
│ render_tender.py render scanned pages to images, read the rates
▼
tender JSON per job one master item list + every vendor's rates
│ cross_check.py GATE: each vendor's rates must tie to their stated total
▼
build_comparison.py JSON -> formatted Excel
│ self_check.py GATE: re-read the file, verify every number and highlight
▼
Tender Comparison.xlsx
The two gates are the point. Nothing gets built until the extracted rates reconcile with each vendor's own grand total, and nothing gets presented until a second pass re-derives every amount, highlight, formula, and ranking from the source.
Claude Code: copy the tender-comparison/ folder into your skills directory (for a project, .claude/skills/tender-comparison/). Then ask Claude to compare your tenders and point it at the folder of files.
claude.ai: run ./scripts/package_skill.sh to build tender-comparison.zip, then upload it under Settings, Capabilities, Skills.
The skill's instructions live in tender-comparison/SKILL.md, with detail in tender-comparison/reference/.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cd tender-comparison
# 1. render scanned tenders to page images, then read the rates into a JSON per job
python scripts/render_tender.py --indir "input" --outdir "pages" --dpi 200
# 2. gate: confirm each vendor's rates tie to their stated total
python scripts/cross_check.py job1.json job2.json
# 3. build the Excel comparison
python scripts/build_comparison.py --out "Tender Comparison.xlsx" job1.json job2.json
# 4. gate: validate the built file
python scripts/self_check.py --xlsx "Tender Comparison.xlsx" job1.json job2.jsonThe tender JSON schema is documented in reference/output-format.md, with a full worked pair under examples/.
Extraction from a scan is the weak link in any tool like this, so TenderTally leans on checks rather than trust:
cross_check.pyadds up rate times quantity for each vendor and compares it to the total the vendor wrote. A mismatch means a misread number, and it stops the run. A genuine slip in the vendor's own arithmetic is recorded with a note, because the quoted rate governs.self_check.pyre-reads the saved workbook and re-derives everything: every amount equals rate times quantity, every green highlight is the true row minimum, the net payable formulas are correct, the ranking is sorted, and no item was dropped or duplicated.tests/includes fault-injection tests that corrupt a workbook on purpose and assert the checker catches it. A validator that never fails would be useless.
What no tool can catch on its own is a rate read wrong that still happens to tie out. The cross-check is the main defence, and the ground truth is always the original tender.
pip install -r requirements-dev.txt
pytesttender-comparison/ the Claude skill (this is what you upload)
├── SKILL.md what the skill does and the step-by-step workflow
├── reference/ extraction, item matching, output format, edge cases
├── scripts/ render, cross_check, build_comparison, self_check
└── examples/ a fictional worked pair + a pre-built example .xlsx
tests/ pytest suite, including fault-injection tests
scripts/package_skill.sh build the upload zip
.github/workflows/ci.yml tests + pipeline smoke test on every push
Real tenders contain real vendor names and live bid prices. This repo commits none of that. The .gitignore excludes input/, output/, sample/, and .private/, and the bundled examples are fictional. Keep it that way.
MIT. See LICENSE.
