Barcode Generator Miscalculated the Check Digit, and the Scanner Read a Different Product

A miscalculated EAN-13 check digit made barcodes scannable but pointed to the wrong product, exposing the "self-generate, self-verify" testing trap.
A developer generated EAN-13 barcodes with his own function. Local scans passed, but at a real checkout counter they pulled up a different product — the barcode was valid but the check digit was wrong. The root cause: the test script called the same buggy function to verify output, forming a closed loop. The author distills three lessons: don't verify your own encoder with itself, use independent round-trip testing, and treat check digits as first-class. He also notes that Code128 without a check digit produces garbled scans that block workflows, while wrong-but-valid codes silently pollute data.
A Near-Miss That Almost Passed

One developer had a simple requirement: generate EAN-13 barcodes for product labels. The API rendered correctly, the scanner could read them, and all local tests passed — by any reasonable standard, the job was done.
Until someone actually scanned one at a checkout counter. Instead of a bar of soap, the POS system pulled up a bag of coffee. The barcode itself was perfectly valid — it just pointed to a different SKU. That's far more frightening than "won't scan": it doesn't error out, doesn't crash, it just quietly identifies one product as another.
The Culprit: The Last Digit of EAN-13
An EAN-13 code has 13 digits, and the last one isn't data — it's the check digit, computed using a modulo-10 weighted algorithm to verify that the first 12 digits were read correctly. The developer's problem lay here: under one specific leading-digit pattern, the check digit was calculated incorrectly.
What made it even more insidious was his testing method. The test script called the same buggy generation function to "verify" the output — essentially using a broken ruler to measure a broken thing, so of course it passed every time. All barcodes with that prefix passed local validation, then collectively failed at real checkout counters.
The fix itself was just a few lines of code: a single pass of modulo-10 weighted calculation. But the lesson was worth far more than the fix.
What Is the Modulo-10 Weighted Algorithm
The EAN-13 check digit uses a modulo-10 weighted method: counting the first 12 data digits from right to left, odd positions (1st, 3rd, 5th... from the right) have a weight of 1, and even positions have a weight of 3. Multiply each digit by its corresponding weight and sum them, then take the remainder when that sum is divided by 10. Subtract the remainder from 10 (if the remainder is 0, the check digit is 0) to get the check digit. For example, if the data digits are 690123456789, the weighted sum is 9×1+8×3+7×1+6×3+5×1+4×3+3×1+2×3+1×1+0×3+9×1+6×3. If the remainder after calculation is 2, then the check digit is 8. This weighting scheme is designed to detect single-digit errors and adjacent-digit transposition errors, and it's a mandatory standard in retail. The developer's mistake was that under one specific leading prefix (such as a country code range), the code's determination of odd/even position order was offset, causing incorrect weight assignment. The resulting check digit was self-consistent but differed from what a standard encoder would produce, ultimately being interpreted by the POS system under a different set of rules.
The Core Lesson: Your Test Oracle Can't Be Your Own Implementation
This lesson applies to any developer generating machine-readable artifacts:
- Don't verify your own encoder with your own encoder. The developer's approach was to bring in an independent decoding library (ZXing on the scanning side) and a second independent encoder for cross-validation, forming a true round-trip.
- Don't only test the one symbology you need right now. He initially only tested the one he was working with. Afterward, he incorporated the entire symbol matrix into testing.
- Treat the check digit as a first-class citizen. Wherever validation is possible, it must be done — and validated using an external implementation.
What Are Test Oracles and Round-Trip Testing
In software testing, a "test oracle" refers to an external reference standard used to determine whether test output is correct. When a developer calls the function under test itself to generate expected results, the oracle degenerates into tautology — it can only verify internal consistency of the code, not detect systematic deviations. The correct approach is to introduce an oracle independent of the implementation under test. For a barcode encoder, for instance, you could use a third-party decoding library like ZXing to reverse-decode the generated image, obtaining the original digit string and comparing it against expected values. This "encode-decode" path is called round-trip testing, and it can catch errors in both the encoder and the decoder. The developer later introduced a second independent encoder for cross-comparison, further eliminating the possibility of defects in a single decoding library's own implementation. Any scenario generating machine-readable artifacts — serialization protocols, PDF typesetting, QR codes — should treat round-trip as the most basic regression test.
Code128's Different Failure: Is No Check Digit Actually Worse?
During the process, he discovered another counterintuitive fact: Code128 doesn't have a check digit by default. This means a misprinted Code128 barcode scans as garbled text rather than "a different valid product."
He initially assumed garbled text was the milder failure mode, but the reality was the opposite. When a checkout counter scans garbled text, the cashier and system need to rescan and manually enter data, blocking the workflow. But when a "wrong but valid product" is scanned, the transaction completes as normal, and the error silently slips into inventory, sales, and financial data.
So he added an optional forced validation to symbologies that support check digits, and the API only returns a verified: true flag after a successful round-trip decode.
Code128's Check Digit Is Optional
The Code128 specification defines a mandatory check character (checksum) that sits after the start character and before the data characters. It's computed using a modulo-103 weighted calculation to verify data integrity during decoding. However, whether the check character must be encoded when printing or generating a barcode depends on the specific implementation and reader behavior — many lightweight generators omit it to shorten the barcode length, and some decoders can still output data when the check character is missing, just without validation. This creates a counterintuitive consequence: once a single bit is misprinted in a Code128 without a check digit, the scan result may be garbled text or a truncated string rather than a "changed but valid" product code. Since POS systems typically don't automatically retry on garbled text, human intervention is required. By contrast, symbologies with check digits directly reject the read when validation fails, more easily triggering standardized error-handling workflows. Therefore, the author recommends enabling forced validation by default for symbologies that support check digits, and using round-trip success as the return condition at the API layer.
A Tool and an Open Question
He packaged the entire logic into a barcode generator (x402.freeq.one/tools/qr_generator.html): supporting 14 symbologies, outputting SVG or PNG, returned as base64, with no need to maintain a local image library.
The article ends with a question posed to the community: Are you also generating machine-readable artifacts? What's your independent verification method?
This question is worth pausing to consider for every developer working with QR codes, barcodes, PDFs, configuration files, or serialization protocols. As long as there's a machine downstream consuming your output, "verifying yourself with yourself" is the most dangerous trap — errors won't report themselves; they'll disguise themselves as correct.
One-line Summary
A miscalculated EAN-13 check digit made barcodes scannable but pointed to the wrong product, exposing the "self-generate, self-verify" testing trap.
Paragraph Summary
A developer generated EAN-13 barcodes with his own function. Local scans passed, but at a real checkout counter, they pulled up a different product — the barcode was valid but the check digit was wrong, pointing to an entirely different SKU. The root cause was that the test script called the same buggy function to "verify" output, forming a closed loop of using a broken ruler to measure a broken thing. The author distilled three lessons: don't verify your own encoder with itself — bring in ZXing and a second encoder for independent round-trip; don't only test one or two symbologies; and treat the check digit as a first-class citizen. The article also contrasts Code128's default lack of check digit, where garbled scans are actually more troublesome because they block the workflow, while a wrong-but-valid product silently pollutes data. Finally, the author packaged the logic into an online generator supporting 14 symbologies, returning verified only after a successful round-trip.
Related articles

Free Unlimited Claude Code Solution: Agnes AI + CC Switch Setup Guide
No paid subscription needed — pair the free AI platform Agnes AI with the open-source tool CC Switch to provide a backend for Claude Code, enabling unlimited free calls to text, image, and video models. This guide covers the full setup, test results, and usage boundaries.

A Complete Zero-Cost Guide to Getting Started with Claude Code for Beginners in China
A beginner's guide to Claude Code for users in China: two installation methods (command line and IDE plugin), full configuration steps for replacing the official API with the free domestic LongCat model, and a hands-on demo of building a small tool with natural language, plus tips.

When the NSA Gets Involved: The AI Supply Chain Crisis Behind Open-Source CVEs and API Key Hijacking
Multiple open-source projects disclosed severe CVEs in one day, PHP flaws alarmed the FBI, and Railway and OpenCode were breached with API keys stolen. This article breaks down the vulnerability window, AI-accelerated exploitation, and the AI supply chain crisis behind the API key black market, with a developer defense checklist.