A Test Card That Sat in Production for Three Weeks
A subscription checkout page turned up a few orders at month-end reconciliation that were valid in format and never charged. The card field held 4242 4242 4242 4242, the generic test number from a foreign payment gateway's public documentation. A test colleague had used it years earlier to walk the front end through a flow, and it slid into production through the very same validation.
That front end did one thing: add the digits by Luhn (mod 10) and look at the remainder. The function was not wrong; what went wrong was reading passed as this card is real. 4242424242424242 comes back on the check page as Luhn check passed, card network Visa, length 16 digits matches convention, BIN 424242, check digit 2, checksum 80 divided by 10 leaves remainder 0. Nothing in that output says it is a test number.
Luhn Answers for One Mistyped Digit
The rule is one sentence: count from the right, double every even position, subtract 9 when the product goes above 9, leave the other digits alone, add everything and see whether 10 divides it. The doubled positions walk the mapping f: 0→0, 1→2, 2→4, 3→6, 4→8, 5→1, 6→3, 7→5, 8→7, 9→9. On a 16 digit card the doubled positions are positions 1, 3, 5, 7, 9, 11, 13, 15 counted from the left, so a single 42 pair contributes 4→8 from the doubled digit plus 2→2 from the plain one, which is 10, eight pairs make 80, and 10 divides it every time. Letting this through is arithmetic rather than a list: the string 4242 appears 0 times in the template, and none of the 8 demo buttons carries it either.
Push it further: repeating that 42 pair 5 to 9 times gives lengths 10, 12, 14, 16 and 18 digits with checksums 50, 60, 70, 80 and 90, all divisible by 10, and only the 10 digit one is stopped first by the 12 to 19 length guard. Relax it to any ab pair repeated 8 times and it passes whenever the doubled mapped digits plus the plain digits are a multiple of 5; there are 20 such pairs, and 00 and 88 are caught earlier by the all-same-digit guard.
Figure 1 | Every extra copy of the same 42 pair adds 10 to the checksum.
What It Really Is Good At: Catching One Mistyped Character
Luhn earns its place in the front end. Replace every digit of the 7 demo and test cards with each of the other 9 digits and the exhaustive 1008 single-character variants leak 0, a detection rate of 100.00%. Add 4242 4242 4242 4242 and none of the 1152 variants across 8 cards leaks either. Change only the last digit and all 63 are caught.
Swap Two Digits and There Are Two Ways It Goes Completely Blind
The trouble starts with order: between account digits of a 16 digit card, exhaustive adjacent swaps run to 1260 pairs and 28 leak (2.2%), every one of them a 0 traded against a 9. The second kind is the real hole. When the two swapped slots share the same doubled status, an even distance apart on a 16 digit card, each side gets back the same weight, the checksum does not move, and detection never happens. That family of 4410 pairs leaks 4410 pairs, 100.00%. One worked case: swapping position 3 with position 5 of 4532015112830366 gives 4502315112830366; both pass, checksum 50→50.
Mistype two digits at once, replacing rather than swapping: on a 16 digit card 1080 of 9720 pairs leak, detection 88.89%; on a 15 digit card 945 of 8505 leak, the same 11.11% miss rate. The familiar claim about 99% of two-character errors is out of reach here.
Figure 2 | Missed counts for five kinds of entry error on a 16 digit card, bars scaled by miss rate.
Made Up on the Spot, About 1/10 Still Gets Through
The second kind of person a form has to survive is not a cashier copying digits wrong but somebody inventing them. Out of 10,000 random 16 digit numbers generated under a fixed seed, Luhn passes 987, which is 9.87%. Pin the BIN to 424242 and exhaust the remaining 1,000,000 combinations and exactly 100000 pass, 10.00%, every one of them labelled Visa. That 1/10 comes from modulo 10 itself rather than from sampling, and 19 of the 25 published test numbers already pass Luhn on their own, 4000000000000002 among them.
One Line Reading Card Network: Visa Will Not Carry a Risk Decision
The structure table is easy to read as a registry. In practice it is 7 prefix patterns plus one length table: of the 1,000,000 possible 6 digit BIN values only 288300, 28.83%, land in one of the 7 networks, and the remaining 711700, 71.17%, fall into unknown, outside the common number range table. The BIN row only pastes the first 6 digits onto one fixed sentence about the issuing institution and card type. The prefix table is wider too, since the Mastercard rule swallows ranges outside 2221 to 2720, and 2799999999999999 is still judged Mastercard. The length column is a whitelist, not a span: across the 7 networks the len arrays hold only 15 entries, nobody recognises length 12, and 411111111117 passes with a note that it does not match convention.
| What the form wants to ask | What it can actually answer | Measured |
|---|---|---|
| One digit copied wrong | Yes, the one thing it is reliable for | 0 of 1008 variants missed |
| Two digits in the wrong order | Never between slots of the same parity | 4410 pairs, all missed (100.00%) |
| Is this card real | Completely unrelated | Random numbers pass 987/10000 |
| Which bank issued it | Card network level and no further | 711700 BINs cannot be told |
The Last Digit Should Be X Is Always Unique and Often Rewrites the Wrong Number
Every failed branch ends with one more sentence: if the first 15 digits were entered correctly, the last digit should be X. The mathematics is sound. With the leading part fixed there is exactly one digit that makes the checksum divisible by 10, and across 100,000 random leading parts the count of solutions is 1 for 100000 of them. The premise is what fails. Of 738 single-character errors spread over 5 cards only 45 sit in the last digit; the other 693 sit in the middle, and applying the suggestion to those returns the original number 0 times, yet all 738 end up passing, and 66 of them quietly change the card network label as well. That swaps a broken number for another valid one; it repairs nothing.
Layering, and the Boundary This Check Cannot Draw
The division of work should be plain: Luhn stays in the front end as a brake on typing errors, the structural comparison reaches card network level and stops, and whether a card exists belongs to gateway authorisation, since test numbers only work inside a sandbox. To lay a number out digit by digit I use the bank card number check; the identity card number is a weighted sum modulo 11, not the same family as doubling even positions modulo 10 here, so it is worth reading beside the identity card number check and the identity card check digit note.
The limits need stating too. One number at a time: the template carries 1 <input>, 9 buttons and 0 <textarea> or file entry point, and the expiry date and security code keywords hit 0 times. The script layer hits 0 entries for fetch, sendBeacon and localStorage, so the number never leaves the browser, while the page shell still loads a stylesheet and an analytics script. Not uploading the card number and making no requests are two different claims, and only the first is true.
Q: Does passing Luhn prove the card is real?
It does not. Passing means nothing more than the checksum dividing by 10: 4242424242424242 passes with checksum 80, and invented numbers pass at 987/10000. Luhn is an error detecting code, not issuer data, and not authorisation.
Q: Can it tell which bank or which card product issued it?
No. The BIN row prints the first 6 digits against one fixed sentence and the table holds no issuer data at all, and 711700 of the 6 digit BIN space cannot even be matched to a network.
Q: Can it check the expiry date and the security code while it is in there?
No, and it cannot run several cards at once either. The keywords for expiry, CVC, CVN and CID hit 0 times, and a security code is not part of the card number, so no web page can verify it for you.