August 23, 2026
How to Build a Contactless Payment Skimmer Detector With PN532: Defensive RFID Auditing for Your…
Your wallet is having conversations without you. Let’s build a bouncer that tells you who is listening.

By Aeon Flex, Elriel Assoc. 2133 [NEON MAXIMA]
7 min read
Your pocket is noisy. Right now, your credit card, your office badge, your gym fob, and that hotel key you forgot to return are all sitting in the dark waiting for someone to ask them a question. They do not authenticate the asker. They just answer.
Hollywood sells skimming as a guy with a laptop in a trench coat. Reality is dumber and closer. It is a cheap reader in a backpack on a crowded subway, or a reader tucked under a cafe table. It does not need to steal your money to be a problem. It just needs to learn that your badge has a static UID that has not changed since 2019.
So we are not building a skimmer. We are building the thing that catches them. A defensive canary that lives in your wallet and screams when someone tries to talk to your cards without permission.
This is fully legal, fully defensive, and you should only test it on your own cards and badges.
What We Are Actually Building
Two modes, one device.
Mode 1: Field Canary. The device pretends to be a tag. It sits in your wallet doing nothing until an external 13.56MHz reader powers up the field and starts polling. When that happens, the PN532 wakes up, sees the interrogation, and triggers a buzzer and LED. You get a physical alert that someone just tried to inventory your pocket.
Mode 2: Wallet Auditor. Press a button and it flips to reader mode. Tap your own cards against it. It tells you what you actually carry: card technology, UID length, whether the UID is static and clonable, and whether your access badge is using 1990s era MIFARE Classic.
We are building this on ESP32-S3 because it has native USB, great low power, and enough brains to run both modes without breaking a sweat. Plus it fits in a wallet.
How Contactless Really Works, Minus The FUD
Contactless payment is not as fragile as TikTok makes it look. Your Visa or Mastercard does not shout your card number into the void. It performs a dynamic cryptogram, a one time transaction code that cannot be replayed. Even if someone reads it, they cannot clone it into a working card.
Your office badge is a different story. Most low cost access systems still rely on a static UID. The reader asks, "Who are you?" The card answers, "I am 4A 3B 2C 1D." Every time. Forever. That is clonable with $10 of hardware. This project will show you which one you are carrying.
Our detector does not decode payment data. We do not want it. We want to detect the interrogation attempt itself and audit our own gear.
BOM
You do not need much. The whole thing should cost under $35.
- 1x ESP32-S3 DevKitC-1. Any variant works, but S3 is ideal for USB and deep sleep.
- 1x PN532 NFC module. Get the one with HSU, I2C, and SPI switch. We will use I2C.
- 1x Small piezo buzzer, 3.3V
- 1x LED + 220 ohm resistor, or use the onboard LED
- 1x Tactile button for mode switching
- 1x Small LiPo 400mAh + TP4056 charger, or just power via USB for bench testing
- Jumper wires, small breadboard or perfboard
- A small case that fits in your wallet. I used a printed 60x40mm box.
No amplifiers, no antennas, no exotic parts. If your PN532 came with a little white antenna board, that is perfect.
Wiring
Set your PN532 to I2C mode. Usually that means DIP switches: Channel 1 OFF, Channel 2 ON. Check your board silkscreen.
Wire it like this:
PN532 VCC to ESP32-S3 3.3V PN532 GND to GND PN532 SDA to GPIO 8 PN532 SCL to GPIO 9 PN532 IRQ to GPIO 10, this is important for target mode wakeups PN532 RST to GPIO 11
Buzzer positive to GPIO 12, negative to GND Button one side to GPIO 13, other side to GND. We will use internal pullup. LED to GPIO 14 if you want an external one, otherwise we use onboard GPIO 48.
Keep wires short. NFC is picky about noise.
The Logic
Most tutorials only show you how to read a tag. We need the opposite too.
In auditor mode, the ESP32-S3 is the initiator. It polls, your card is the target.
In canary mode, the ESP32-S3 is the target. It emulates a dumb tag with a random UID. It waits. When an external reader, legitimate or shady, starts polling, it selects our fake tag. That selection is our alarm trigger.
The PN532 can run as an ISO14443A target. That is the key feature most people never use.
Code: The Detector
This uses the Adafruit PN532 library. Install it via Arduino Library Manager along with Adafruit BusIO.
This is an Arduino sketch for ESP32-S3. It is deliberately defensive. It never attempts to read payment cryptograms, never tries to crack keys, and only reports UID and card type for your own audit.
#include <Wire.h>#include <Adafruit_PN532.h>
#define SDA_PIN 8#define SCL_PIN 9#define IRQ_PIN 10#define RST_PIN 11#define BUZZER_PIN 12#define BUTTON_PIN 13#define LED_PIN 14
Adafruit_PN532 nfc(SDA_PIN, SCL_PIN, IRQ_PIN, RST_PIN);
enum Mode { CANARY, AUDITOR };Mode currentMode = CANARY;unsigned long lastButtonPress = 0;
void setup() { Serial.begin(115200); pinMode(BUZZER_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); Wire.begin(SDA_PIN, SCL_PIN);
nfc.begin(); uint32_t version = nfc.getFirmwareVersion(); if (!version) { Serial.println("PN532 not found. Check wiring and I2C switch."); while(1); } nfc.SAMConfig(); Serial.println("PN532 Ready. Starting in CANARY mode."); enterCanaryMode();}
void loop() { if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonPress > 500) { lastButtonPress = millis(); toggleMode(); }
if (currentMode == CANARY) { runCanaryLoop(); } else { runAuditorLoop(); }}
void toggleMode() { if (currentMode == CANARY) { currentMode = AUDITOR; Serial.println("\n--- Switching to AUDITOR mode ---"); Serial.println("Tap your OWN cards to see what they leak."); nfc.SAMConfig(); } else { currentMode = CANARY; Serial.println("\n--- Switching to CANARY mode ---"); enterCanaryMode(); }}
void enterCanaryMode() { // Emulate a Type 4 tag with random UID. We are the bait. uint8_t fakeUid[3] = {0x12, 0x34, 0x56}; nfc.AsTarget(fakeUid);}
void runCanaryLoop() { // In target mode, asTarget blocks until an initiator selects us // If we get here, a reader just tried to talk to us if (nfc.AsTarget()) { Serial.println("ALERT: External 13.56MHz reader detected!"); triggerAlarm(); // Re-arm as target immediately delay(100); uint8_t fakeUid[3] = {0x12, 0x34, 0x56}; nfc.AsTarget(fakeUid); }}
void runAuditorLoop() { uint8_t success; uint8_t uid[8]; uint8_t uidLength;
success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 200);
if (success) { Serial.println("\n--- Card Found ---"); Serial.print("UID Length: "); Serial.print(uidLength); Serial.println(" bytes"); Serial.print("UID: "); nfc.PrintHex(uid, uidLength);
if (uidLength == 4) { Serial.println("Type: Likely MIFARE Classic or old access badge"); Serial.println("Risk: Static 4-byte UID. Often clonable. This is what most cheap cloners copy."); } else if (uidLength == 7) { Serial.println("Type: Likely MIFARE DESFire, Ultralight, or modern payment card"); Serial.println("Risk: 7-byte UID with better randomization. Still check if system relies on UID only."); }
// We intentionally do not dump data blocks or attempt authentication // For your own audit, knowing UID behavior is enough to assess cloning risk
triggerScanBeep(); delay(1000); }}
void triggerAlarm() { for (int i = 0; i < 6; i++) { digitalWrite(LED_PIN, HIGH); digitalWrite(BUZZER_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); digitalWrite(BUZZER_PIN, LOW); delay(80); }}
void triggerScanBeep() { digitalWrite(BUZZER_PIN, HIGH); delay(80); digitalWrite(BUZZER_PIN, LOW);}#include <Wire.h>#include <Adafruit_PN532.h>
#define SDA_PIN 8#define SCL_PIN 9#define IRQ_PIN 10#define RST_PIN 11#define BUZZER_PIN 12#define BUTTON_PIN 13#define LED_PIN 14
Adafruit_PN532 nfc(SDA_PIN, SCL_PIN, IRQ_PIN, RST_PIN);
enum Mode { CANARY, AUDITOR };Mode currentMode = CANARY;unsigned long lastButtonPress = 0;
void setup() { Serial.begin(115200); pinMode(BUZZER_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); Wire.begin(SDA_PIN, SCL_PIN);
nfc.begin(); uint32_t version = nfc.getFirmwareVersion(); if (!version) { Serial.println("PN532 not found. Check wiring and I2C switch."); while(1); } nfc.SAMConfig(); Serial.println("PN532 Ready. Starting in CANARY mode."); enterCanaryMode();}
void loop() { if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonPress > 500) { lastButtonPress = millis(); toggleMode(); }
if (currentMode == CANARY) { runCanaryLoop(); } else { runAuditorLoop(); }}
void toggleMode() { if (currentMode == CANARY) { currentMode = AUDITOR; Serial.println("\n--- Switching to AUDITOR mode ---"); Serial.println("Tap your OWN cards to see what they leak."); nfc.SAMConfig(); } else { currentMode = CANARY; Serial.println("\n--- Switching to CANARY mode ---"); enterCanaryMode(); }}
void enterCanaryMode() { // Emulate a Type 4 tag with random UID. We are the bait. uint8_t fakeUid[3] = {0x12, 0x34, 0x56}; nfc.AsTarget(fakeUid);}
void runCanaryLoop() { // In target mode, asTarget blocks until an initiator selects us // If we get here, a reader just tried to talk to us if (nfc.AsTarget()) { Serial.println("ALERT: External 13.56MHz reader detected!"); triggerAlarm(); // Re-arm as target immediately delay(100); uint8_t fakeUid[3] = {0x12, 0x34, 0x56}; nfc.AsTarget(fakeUid); }}
void runAuditorLoop() { uint8_t success; uint8_t uid[8]; uint8_t uidLength;
success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 200);
if (success) { Serial.println("\n--- Card Found ---"); Serial.print("UID Length: "); Serial.print(uidLength); Serial.println(" bytes"); Serial.print("UID: "); nfc.PrintHex(uid, uidLength);
if (uidLength == 4) { Serial.println("Type: Likely MIFARE Classic or old access badge"); Serial.println("Risk: Static 4-byte UID. Often clonable. This is what most cheap cloners copy."); } else if (uidLength == 7) { Serial.println("Type: Likely MIFARE DESFire, Ultralight, or modern payment card"); Serial.println("Risk: 7-byte UID with better randomization. Still check if system relies on UID only."); }
// We intentionally do not dump data blocks or attempt authentication // For your own audit, knowing UID behavior is enough to assess cloning risk
triggerScanBeep(); delay(1000); }}
void triggerAlarm() { for (int i = 0; i < 6; i++) { digitalWrite(LED_PIN, HIGH); digitalWrite(BUZZER_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); digitalWrite(BUZZER_PIN, LOW); delay(80); }}
void triggerScanBeep() { digitalWrite(BUZZER_PIN, HIGH); delay(80); digitalWrite(BUZZER_PIN, LOW);}Upload this to your ESP32-S3, open Serial Monitor at 115200, and you are live.
Note on AsTarget(): The Adafruit library implementation varies by version. If your version does not expose it, use the EmulateTag example as base. The concept is identical. You are acting as a tag, not a reader. That is what lets you detect readers.
Testing It Without Being Creepy
Bench test only on your own property.
Test 1: Canary Mode. Put the device in your wallet, powered by a small battery. Take your office badge reader or an ACR122U USB reader you own. Bring your wallet within 3 to 5cm of the reader. The buzzer should chirp before your real badge even responds. That is the field detection. You just proved someone could have polled your pocket.
Test 2: Auditor Mode. Press the button. Tap your office badge. If you see a 4 byte UID that never changes, that system is checking ID, not cryptography. That is an educational finding about your own building. Tap your contactless debit card. You should see a 7 byte UID that changes or a random ID. That is intentional privacy. The card is doing its job.
Do not tap other people's cards, badges, or wallets. The whole point is defensive auditing for your own gear.
What Your Results Mean
If your canary chirps near a doorway you did not expect, you found a reader placement that is overly sensitive or poorly shielded. Tell your security team, do not exploit it.
If your auditor shows your access badge is a 4 byte static UID, you now have evidence to ask your employer to upgrade to DESFire EV2 or EV3 with challenge response. You can write a very compelling internal security ticket with logs.
If your payment card shows a random UID, relax. That is by design. Modern EMV contactless uses tokenization. Even if someone logged the conversation, the cryptogram cannot be reused.
The uncomfortable truth: your gym tag and your apartment fob are usually the leakiest things you own. Not your bank card.
Hardening Your Real Life
This project is not about paranoia. It is about measurement.
- Stop buying RFID blocking wallets as a talisman. Most are just aluminum foil with marketing. Measure first. Our detector tells you if blocking actually works. Many blockers leak at the seams.
- Separate your identities. Keep your high value static UID badges in a small Faraday sleeve inside your wallet, not loose. Keep payment cards outside that sleeve so they work for tap to pay.
- Ask for better tech. If you are an engineer in your company, push to deprecate UID only systems. The upgrade path exists and is not expensive.
- Add logging. The ESP32-S3 can log timestamps of field detections to flash. If you work in a sensitive lab, you can prove when and how often your pocket was scanned. That is useful data for a physical security review.
Where To Take It Next
You now have a platform. Here are three upgrades that stay defensive:
- Add a small OLED to display card type without a laptop
- Log field events to SD with timestamps and push them to Home Assistant
- Add deep sleep: wake only on IRQ from PN532, so a 400mAh battery lasts weeks in your wallet
You built a tool that makes the invisible visible. That is the core of good security work. You did not need to break anything. You just needed to listen.
If you build this, audit your own wallet first. You will be surprised how chatty it is. Then write up what you found. That story is exactly what Medium readers want from a tutorial like this.
Build it, measure it, then fix the real problem.
Field manuals from this build:
- $20 SOC — Tiny defensive monitor:
The $20 SOC: Build a Tiny Defensive Network Monitor Stop writing vulnerable tools. Start building memory-safe offensive and defensive systems in Rust.WHY THIS GUIDE?70% of…
- Physical Agents — ESP32 + OpenClaw bridge:
PHYSICAL AGENTS: Bridging OpenClaw with ESP32 Microcontrollers via Custom WebSocket Daemons Paid field manual in the AI agents / OpenClaw route. From prompt toy to working system: memory, routing, tools, and…
- War-Driving Box — Pi sniffer build:
Hardware Hacking for OSINT: Building a Raspberry Pi War-Driving Box Paid field manual in the ESP32 / WiFi / field hardware route. Practical enough to build, weird enough to keep, and…
Use your best judgement. Keep pushing. Keep building.