DEV Community

Multigrid
Multigrid

Posted on • Originally published at multigrid.ai

Extracting Structured Data From an Auction Sale Receipt

An auction receipt states a hammer price, a buyer’s premium and a total. The obvious validation is that the premium is the house’s advertised percentage of the hammer price. On most real receipts that check fails, and the receipt is right.

Three numbers, not one price

The confusion this document creates starts with vocabulary. The hammer price is what the winning bid was — the number the auctioneer called. The buyer’s premium is the house’s commission, charged to the buyer on top of the hammer price. The total or invoice amount is what the buyer actually pays, being hammer plus premium plus tax plus any lot-level fees. Three different numbers, all of which a careless schema calls price.

They are not interchangeable and the difference is large. A 25% premium on a 20,000 hammer price is a 5,000 gap between the number the press reports and the number the buyer pays. If you are extracting receipts to build a price history, using the total where you meant the hammer price inflates every observation by roughly the premium rate, and using the hammer price where you meant the total understates what the goods cost the buyer. Neither error is visible in the data; both are visible in the conclusions.

There is a fourth number on consignor-side paperwork, which is the seller’s net: hammer price minus the seller’s commission, minus insurance, photography, storage and any unsold-lot fees. It appears on a different document from the buyer’s invoice and is worth keeping in a different table, because a receipt that carries both is ambiguous about which commission the word commission refers to.

Why the premium check fails

Buyer’s premium is almost never a single rate. Houses publish a tiered schedule: one rate on the first band of hammer price, a lower rate on the portion above it, sometimes a third rate above that. The bands are applied marginally, in the way income tax brackets are, so the effective rate on any given lot lands somewhere between the top and bottom published rates and equals neither.

So a validator that computes premium / hammer and compares it to the headline rate will disagree with the receipt on every lot above the first band. The disagreement is not an extraction error and treating it as one buries genuine errors under a pile of false positives. The correct check needs the schedule, and the schedule is not on the receipt — it is in the house’s conditions of sale. That is a configuration input to your pipeline, keyed by auction house and effective date, not something to extract.

A second reason the naive check fails: some houses apply the bands cumulatively as described, and some apply a single rate determined by which band the hammer price falls into. Those give different answers for the same lot. You cannot tell which convention is in use from one receipt; you can tell from two receipts either side of a band boundary, or by reading the conditions of sale. Record which convention you assumed alongside the check result.

A worked reconciliation

Take a synthetic lot, with every input labelled as an assumption rather than a measurement. Assume a hammer price of 8,400.00, a published marginal premium schedule of 25% on the first 5,000.00 and 20% on the portion above, and a sales tax rate of 8% applied to hammer plus premium.

hammer                                    8,400.00

premium, first band  5,000.00 x 0.25      1,250.00
premium, above band  3,400.00 x 0.20        680.00
premium total                             1,930.00

effective premium rate 1,930.00 / 8,400.00 = 22.98%
       -- not 25%, and not 20%.  The naive check fails here.

taxable base = hammer + premium          10,330.00
tax  10,330.00 x 0.08                       826.40

invoice total                            11,156.40
Enter fullscreen mode Exit fullscreen mode

The effective rate of 22.98% is the number that makes a single-rate validator complain. Nothing is wrong with the receipt. What you can check, and should, is the identity that has to hold on any receipt under any schedule:

hammer + premium + tax + fees - discounts == stated_total
Enter fullscreen mode Exit fullscreen mode

That check requires no knowledge of the house’s schedule, uses only fields printed on the document, and catches the errors that actually happen — a missed line item, a misread digit, a fee block on a second page that the extractor never saw. It is the document-specific case of a cross-field amount validation rule. Run it to the cent, and treat a discrepancy of a cent or two as a rounding signal rather than a transcription error, since houses differ on whether tax is computed per-line or on the summed base.

The tax base is the second trap

Notice that the tax in the worked example was applied to hammer plus premium, not to hammer alone. That is the common treatment in US jurisdictions, where the premium is part of the consideration for the goods. It is not universal: the base varies by jurisdiction, some lots are exempt, some buyers hold a resale certificate, and in European sales a margin scheme may mean the VAT shown relates to the premium rather than the lot.

The extraction consequence is that you must capture the tax base if the receipt states it, and never infer the rate by dividing the tax by a base you assumed. A receipt showing 826.40 of tax on an 8,400.00 hammer price implies a 9.8% rate if you assume the wrong base and 8% if you assume the right one, and both look like plausible tax rates. Where the base is not printed, record the tax as an amount with an unknown base rather than deriving a rate that will later be treated as fact.

Lot-level fees are the same problem in miniature. Storage, shipping, artist resale royalty, import duty and card surcharges all appear as separate lines, and some of them are taxable while others are not. Extract them as a list of typed line items rather than summing them into a fees scalar, because the sum is recoverable from the list and the list is not recoverable from the sum.

Extracting so the check is possible

The schema follows directly from the identity you want to test. Name each of the three prices distinctly, keep fees as a list, and keep the stated total as the document’s own assertion rather than something you compute:

{
  "house": "Example Auctions",
  "sale_date": "2026-04-18",
  "lot_number": "142A",
  "hammer_price": { "amount": 8400.00, "currency": "USD" },
  "buyers_premium": { "amount": 1930.00, "currency": "USD" },
  "tax": { "amount": 826.40, "base_stated": 10330.00, "rate_stated": 0.08 },
  "fees": [
    { "label": "Shipping", "amount": 0.00, "taxable": false }
  ],
  "stated_total": { "amount": 11156.40, "currency": "USD" },
  "foots": true
}
Enter fullscreen mode Exit fullscreen mode

The foots flag is the whole point of the exercise: a boolean computed at ingestion that says whether the document’s own numbers agree with each other. It is far more useful as a review trigger than a model’s confidence score, because it is deterministic and it is about the document rather than about the extractor. A receipt that does not foot goes to a human whatever the model thought. The same self-consistency idea drives the hotel folio and delivery manifest pages in this cluster, and it is the single highest-value check available on any document that states its own total.

Related

Top comments (0)