Every card authorization carries a small field that records how the card data reached the terminal: chip, contactless, magnetic stripe, fallback swipe, or typed in by hand. Most engineering teams never look at it. They should. A terminal or cashier that keys in far more sales than its peers usually points to a broken reader, a training gap, or someone deliberately bypassing the chip. Keyed sales also cost more and are harder to defend in disputes.
This article covers building a keyed-rate monitor: normalizing entry mode data, separating keyed-by-design traffic, scoring terminals and employees without drowning in small-sample noise, and triaging alerts. For the business side of why keyed sales cost more and how to document them, see Key-Entered Transactions: Why Manually Keyed Transactions Cost More & How to Reduce Them.
Where the signal lives
In ISO 8583-based messaging, the POS entry mode is carried in data element 22. Card networks define the values, and processors expose them in settlement files, reporting APIs, or webhook payloads, often renamed. Commonly seen values include:
| Code | Meaning | Bucket |
|---|---|---|
| 01 | Manual key entry | keyed |
| 05 | Chip (contact EMV) | chip |
| 07 | Contactless chip | contactless |
| 80 | Chip failed, fell back to mag stripe | fallback |
| 90 | Full mag stripe read | swipe |
| 91 | Contactless mag stripe mode | contactless |
Exact codes vary by network and processor, so build a mapping table rather than hard-coding them. Your processor's spec is the source of truth.
Two things matter for monitoring:
- Keyed is the outcome you're measuring.
- Fallback is the leading indicator. A chip reader that's dying usually produces fallback swipes before cashiers give up and start typing.
Step 1: Normalize into a clean fact table
Raw transaction feeds include refunds, voids, reversals, and declined attempts. Counting those inflates or distorts rates. Start with a view that keeps only approved sales and maps entry mode to a bucket.
CREATE VIEW sale_entry AS
SELECT
t.txn_id,
t.store_id,
t.terminal_id,
t.employee_id,
t.channel, -- 'counter', 'moto', 'ecom'
t.card_fingerprint, -- processor token, never the PAN
t.amount,
t.created_at,
COALESCE(m.bucket, 'unknown') AS entry_bucket
FROM transactions t
LEFT JOIN entry_mode_map m
ON m.processor = t.processor
AND m.raw_code = t.entry_mode
WHERE t.txn_type = 'sale'
AND t.status = 'approved';
The LEFT JOIN plus 'unknown' matters. When a processor adds a new code or a firmware update starts sending blanks, you want those rows visible, not silently dropped.
Step 2: Exclude keyed-by-design traffic
Phone and mail orders are keyed every time. If a store's back-office terminal handles MOTO orders, it will dominate any keyed-rate ranking and hide the counter terminal that's actually broken.
Filter on channel, not on terminal ID. Terminal assignments change; channel flags (if your POS sets them correctly) travel with the transaction. Track MOTO volume as its own metric.
SELECT
store_id,
terminal_id,
employee_id,
COUNT(*) AS n,
COUNT(*) FILTER (WHERE entry_bucket = 'keyed') AS keyed,
COUNT(*) FILTER (WHERE entry_bucket = 'fallback') AS fallback,
COUNT(*) FILTER (WHERE entry_bucket = 'unknown') AS unknown
FROM sale_entry
WHERE channel = 'counter'
AND created_at >= now() - interval '14 days'
GROUP BY store_id, terminal_id, employee_id;
Roll this up separately by terminal and by employee. The two views answer different questions.
Step 3: Score without fooling yourself
The naive approach, ranking by keyed / n, fails immediately. A terminal with 2 keyed sales out of 40 shows 5%, while a busy one with 40 out of 900 shows 4.4%. The first looks worse but is almost certainly noise.
Use a confidence interval on the proportion and flag only when the lower bound exceeds the peer baseline. The Wilson score interval behaves well at small n and near zero, which is exactly where keyed rates live.
import math
def wilson_interval(k, n, z=1.96):
if n == 0:
return (0.0, 1.0)
p = k / n
denom = 1 + z * z / n
center = (p + z * z / (2 * n)) / denom
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return center - half, center + half
def flag(k, n, baseline, min_n=30):
if n < min_n:
return False
low, _ = wilson_interval(k, n)
return low > baseline
Worked example
A store's counter terminals collectively key 1.5% of approved sales over 14 days. That's the baseline.
- Terminal A: 40 keyed of 900. Raw rate 4.4%. Wilson 95% interval ≈ 3.3% to 6.0%. Lower bound 3.3% > 1.5%, so flag.
- Terminal B: 2 keyed of 40. Raw rate 5.0%. Interval ≈ 1.4% to 16.5%. Lower bound 1.4% < 1.5%, so no flag yet.
Terminal B has the higher raw rate but not enough evidence. If it keeps keying at that pace, the interval tightens and it gets flagged within a week or two.
Choosing the baseline
Compare against a peer group, not a global number. A grocery store, a hardware store, and a bar have different card mixes and different damaged-card rates. Reasonable options:
- Same store, other terminals (best for catching hardware faults)
- Same store format across the chain (best for catching a whole store's habits)
- The entity's own trailing 90 days (best for catching sudden change)
Run more than one. A terminal that's normal for its store but the store is 3x the chain baseline is a different problem than one bad terminal.
Step 4: Triage with the terminal × employee matrix
Once something is flagged, cross-reference both dimensions. The pattern usually tells you the cause before anyone visits the store.
| Pattern | Likely cause | First action |
|---|---|---|
| One terminal high across all employees; fallback rose first | Failing or dirty chip reader | Swap or service the device |
| One terminal high across all employees; fallback normal | Reader disabled, misconfigured, or physically obstructed | Check terminal config and setup |
| One employee high across every terminal they use | Training gap or habit of skipping the chip | Retrain; review receipts and imprints |
| One employee high, keyed sales cluster on a few card fingerprints | Possible misuse of stolen card numbers | Escalate to loss prevention |
| Whole store high, all terminals and staff | Store-level practice, or MOTO traffic tagged as counter | Audit channel tagging, then store procedures |
unknown bucket spikes |
Processor code change or firmware bug | Update entry_mode_map; check release notes |
The fallback-then-keyed sequence is worth automating. A simple rule: alert when a terminal's fallback rate crosses its threshold, even if keyed is still normal. That catches hardware decay early.
Step 5: Add card-level checks for keyed sales
Rates catch systemic problems. Some abuse only shows at the card level. Two cheap queries:
Multiple keyed sales on one card, same store, same day. This can indicate an improperly split purchase, which card network rules generally prohibit, or repeated use of a compromised number.
SELECT store_id, card_fingerprint, created_at::date AS day,
COUNT(*) AS keyed_count, SUM(amount) AS total
FROM sale_entry
WHERE entry_bucket = 'keyed' AND channel = 'counter'
GROUP BY store_id, card_fingerprint, created_at::date
HAVING COUNT(*) >= 2;
Keyed sale on a card previously read by chip. If the same fingerprint was dipped successfully last week and keyed today, the card is probably fine. Today's keyed entry needs an explanation.
Both rely on a stable card fingerprint from your processor's tokenization. Never store or join on raw card numbers.
Common mistakes
Counting declines. A cashier who tries the chip three times and gets three declines before keying produces four rows. Only count approved sales, and track failed attempts separately if you want reader-health data.
Mixing channels. If MOTO traffic is mislabeled as counter, your top offender is a phone-order desk. Validate channel tagging before trusting any ranking.
Alerting on day-level data. Keyed sales are rare. Daily windows produce mostly noise. Use rolling 7 to 14 day windows with a minimum transaction count.
Ignoring shared logins. If employees share POS credentials, employee-level analysis is meaningless. Detecting a login used across simultaneous terminals is a useful data quality check on its own.
Treating every flag as fraud. Most high keyed rates come from hardware and training. Design the alert to route to operations first and loss prevention only when card-level signals point that way.
Logging sensitive fields. Keyed card-not-present flows often collect the card security code. PCI DSS prohibits storing it after authorization, including in debug logs, analytics events, or call recordings. Audit your pipeline so the monitor doesn't become a compliance problem.
Testing the monitor
Seed a staging dataset with known patterns: one terminal with elevated fallback followed by keyed, one employee keying across three terminals, a low-volume terminal with a high raw rate, and a batch of rows with an unmapped entry code. Assert that the first two flag, the third doesn't, and the fourth surfaces in the unknown metric. Run it whenever you change thresholds, baselines, or the mapping table.
The whole system is a view, a few aggregate queries, and a twenty-line scoring function. What makes it useful is the discipline around it: clean inputs, sensible peer groups, and a triage path that sends hardware problems to hardware people.
Top comments (0)