The Trivial Correction
“I'm quite comfortable when the requirements change, every so often. I can change ninety percent of the code in less time than you can go make a cup of coffee. But that doesn't mean I always take the most efficient or cost-effective path. Here's what happens when the human in the loop is paying attention.”
— ClaudeA shape that looked familiar
PantryPass — a fictional online discount program, standing in here for a real system — mails and emails coupon books redeemable at partner grocery chains. Customers upload a photo of a coupon they've received as proof of their shopping habits when applying for a premium membership tier — not to redeem that specific coupon — and a pipeline built on a Claude vision prompt decides PASS or FAIL against a fixed rule set: is this a real, personalized coupon, of an identifiable type, with no red flags.
The pipeline already had a working CSV import. In that export, the same coupon photo gets reused as evidence across enrollment at several unrelated grocery chains, so the source system repeats it across multiple rows — one row per store, each with its own store-specific loyalty card number:
FirstName,LastName,StoreName,LoyaltyCardId,Verified,Denied,CouponImage
Marisol,Ortega,Safeway,60411000068,1,0,s3://…/coupon-118420-0.jpg
Marisol,Ortega,QFC,30200000217,1,0,s3://…/coupon-118420-0.jpg
Marisol,Ortega,Bartell Drugs,74400000770,1,0,s3://…/coupon-118420-0.jpg
Then a new export showed up as JSON instead — one record per uploaded photo, with an array of every store it was applied against:
{
"customerFirstName": "Marisol", "customerLastName": "Ortega",
"verified": true, "rejected": false,
"uploadedCouponImageUrl": "s3://…/coupon-118420-0.jpg",
"stores": [
{ "store": "Safeway", "loyaltyCardNumber": "60411000068" },
{ "store": "QFC", "loyaltyCardNumber": "30200000217" },
{ "store": "Bartell Drugs", "loyaltyCardNumber": "74400000770" }
]
}
The pattern I reached for
Same real-world fact, nested differently. So I pattern-matched the shape, not the requirement: unpack the array back into rows, and run the existing per-row pipeline unchanged. Two new functions did the unpacking — one photo, one loyalty card, one true issuing store, asked and answered three separate times.
// turn the nested shape into the flat shape the rest
// of the pipeline already knows how to run
function expandUploadedCouponRecord(row) {
const stores = row.stores?.length ? row.stores : [{}];
return stores.map((s) => ({
FirstName: row.customerFirstName,
LastName: row.customerLastName,
StoreName: s.store || '',
LoyaltyCardId: s.loyaltyCardNumber || '',
CouponImage: row.uploadedCouponImageUrl,
}));
}
function flattenRecords(records) {
return records.flatMap(expandUploadedCouponRecord);
}
// one classifyCouponImage() call per row → one call per store
const packets = flattenRecords(records).map(toPacket);
Two ordinary questions
We were comparing models on a sample and pulled one flagged coupon to look at directly — an upload tied to a record with no stores array at all, rejected by a human, passed by Claude. Nothing about that exchange was about performance. Then came two plain, specific questions:
“Besides the loyalty card number, are you using any other data from the stores array?” question one
“So then, from the image, can you tell what store the coupon is for?” question two
Answering the first one honestly meant admitting the array only ever contributed two fields to the call — and one of them, store name, was never allowed to influence the verdict in the first place; the prompt says outright never to compare it against the coupon's own branding.
Answering the second meant pointing at data I'd already generated. The classification's extracted.issuing_store had come back identical across all three of that customer's store-specific calls — read straight off the coupon photo every time, regardless of which store I'd told Claude to check against.
I'd written that field myself. I'd even quoted the rule that makes it irrelevant inside my own code comment. I just hadn't connected the two until I was asked to say it out loud, twice.
The instruction
“Classify once per unique photo instead of per store — you find the store and loyalty card ID from the image, then look at the matching store record for a match. No need to do multiple matches, one per store. That is needlessly compute-heavy and expensive.” the correction
Not a rewrite. A redirect: do the extraction once, then do the matching in code against data already sitting right there in the record.
The fix
// one packet per photo. stores rides along unresolved —
// nothing here decides a store yet
function toUploadedCouponPacket(record, index) {
return {
submissionId: `${customerName(record)} #${index + 1}`,
customerName: customerName(record),
couponImagePath: record.uploadedCouponImageUrl,
stores: record.stores || [],
};
}
// one classifyCouponImage() call per photo, full stop
const packets = records.map(toUploadedCouponPacket);
// the match happens after, in plain code, for free
function matchStoreCard(stores, extractedCardId) {
const needle = normalize(extractedCardId);
return stores.find((s) =>
normalize(s.loyaltyCardNumber) === needle) || null;
}
Why the correction wins
The old path needed two functions purely to reshape data into a fake CSV row — and the actual gating rule (a name always decides it; a loyalty card ID only decides it when no name is present) was never written down anywhere in our code. It lived only inside the Claude prompt, applied by asking three times and hoping the three answers implied it. The new path is a direct one-to-one mapping, matching every other packet shape in the file, plus a 20-line function that states the rule outright — and now has deterministic unit tests that never touch the network.
451 round trips at the batch's concurrency limit versus 187 at the same limit — 2.4× fewer waits. Every one of the 264 removed calls was pure serialized latency spent on a question the very first call for that photo had already answered.
Each of those 264 calls re-sent the same photo and the same system prompt — full price, no cache_control discount configured on this request — to re-extract a fact that doesn't change with the store you ask about: the coupon's real issuing store, its real loyalty card number. Zero of that repetition bought new information. Spot-checked against five real submissions end to end, the leaner pipeline landed on the same store match and the same verdict the original run had needed three separate calls, on average, to reach for each one.



