Check before you swipe.
A card skimmer fitted to a fuel pump is usually built around a two-dollar Bluetooth serial bridge — an HC-05 or one of its clones — spliced across the card reader's data lines and powered from the pump. Nobody comes back for the card numbers. They are collected over the air, from a car in the lot. That radio is the one part of a skimmer you can find from the outside, and Skimscan is what looks for it.
It will never tell you a pump is safe. It will tell you exactly what it heard, and exactly why it scored it.
Sweep — listening · a SUSPECT · and the thing itself · Devices, worst first · the facts · the working · a score held down, and why · what to do · how the attack works · the tap · wiring with a live link check · settings
Every number in those screenshots came out of the real scoring engine.
make -C test dumpruns the scripted forecourt throughskim_score()and writes the JSON thattools_gen_mockups.pydraws from, so the README cannot drift away from what the app does.
- 🔍 It looks for the radio, not the skimmer. BR/EDR general inquiry plus LE scanning, through an ESP32 companion, because the Flipper's own Bluetooth stack can advertise but cannot inquire and has no BR/EDR at all.
- 🧮 Seven signals, three independent families. What it calls itself (identity), what kind of device it claims to be (declaration), and how it behaves over time (behaviour). A score is only allowed to climb as far as the breadth of the evidence justifies.
- 🧾 Every score shows its working. Open any device and the WHY page lists each signal that fired, what it was worth, and — the important part — which cap held the score down.
- 🚫 It never says a pump is safe. The best a sweep can say is
NO MATCH: nothing here looked like a skimmer radio. Most skimmers are not radios at all. - ⏱️ A fixture has to prove it is a fixture. Pass counting is what separates a module bolted into a pump from a passer-by's phone — and nothing reaches the top band on a single sighting.
- 📚 A six-panel walkthrough of the attack, drawn and animated on the device: where the module goes, why it is a radio, why nobody comes back for it, and exactly how little an inquiry response tells you.
- 🔌 Wiring diagram on the device, with a live is-it-actually-talking status strip.
- 🧪 Demo mode. A scripted forecourt — a few phones, a speaker, a beacon, a customer who drives off after two passes, and one HC-05 that is still there every time you look. No hardware needed.
- 🧷 CSV sweep logs to the SD card: address, score, verdict and the signals behind it — the form a station manager or a police report can actually use.
- 🔕 Three levels of alarm you can tell apart with the Flipper in a pocket, all optional.
- 🕶️ Listen-only. Skimscan never pairs, never connects, and is not itself discoverable.
| Signal | Family | Worth | What it means |
|---|---|---|---|
| Known module name | identity | 35 | The factory name of a serial bridge — HC-05, linvor, HMSoft, RNBT-…. Renaming it is one AT command, so it is a hint, not a finding. |
| Module maker's MAC | identity | 25 | The first three bytes belong to a firm that makes serial-bridge modules and little else. Independent of the name. |
| Odd MAC prefix | identity | 12 | A MAC shaped like a date (20:16:…), or one the device chose for itself. Clone firmware does this; finished products do not. |
| Declares no class | declaration | 15 | Real products say what they are. This left the Class-of-Device field blank, which is the module default. |
| Findable but nameless | declaration | 6 | Made itself discoverable on BR/EDR, then declined to say what it is. |
| Still here, pass after pass | behaviour | 15 | Three inquiry passes. People walk away; something wired into a pump does not. |
| Close enough to be here | behaviour | 10 | Loud enough to be inside this pump rather than in a car across the forecourt. |
35 + 25 + 15 + 15 + 10 = 100 is the only route to a full score — and the caps still take five off it.
Without them the engine is a keyword search that shouts SKIMMER at anyone's robot kit, and one bad call in a petrol station forecourt is worse than ten quiet ones.
| Cap | Held at | Why |
|---|---|---|
| Behaviour only | 14 |
The speaker in the shop is close by and never moves. If that were worth reporting, everything would be. |
| No identity signal | 39 |
Nothing about it says bridge module. |
| One family of evidence | 39 |
Two identity signals are one opinion told twice. |
| Fewer than three families | 69 |
The top band needs three independent kinds of evidence. |
| Seen once | 69 |
A single sighting cannot tell a fixture from someone walking past. |
| Anything at all | 95 |
No scan is ever certain from outside a locked panel. |
| Score | Verdict | Meaning |
|---|---|---|
| 0–14 | ORDINARY |
Looks like ordinary consumer Bluetooth. |
| 15–39 | NOTE |
One thing stood out. Probably nothing. |
| 40–69 | SUSPECT |
Several signals line up. Worth a second sweep. |
| 70–95 | SKIMMER? |
This looks like a bridge module bolted to something. |
A sweep's headline is its worst device — and a sweep that found nothing says NO MATCH, never
clear and never safe.
The Flipper's Bluetooth stack advertises; it cannot run an inquiry, and it has no BR/EDR radio at all. Skimmer modules live on BR/EDR. So the radio is an ESP32 on the GPIO header.
It must be a classic ESP32 (ESP32-WROOM-32 or similar). The S2, S3 and C3 have no Bluetooth Classic and cannot do this. The official Flipper WiFi devboard is an S2 — it will not work.
| Flipper | ESP32 | |
|---|---|---|
13 TX |
→ | RX0 |
14 RX |
← | TX0 |
8 GND |
— | GND |
1 5V |
— | 5V / VIN |
115200 8N1. The companion uses its own UART0, which is also its USB programming port, so the board talks to your computer or to the Flipper — never both at once.
Boards that also carry GPS can be moved to the LPUART (15/16) in Settings → Port, leaving
13/14 free.
Flashing: open esp32/skimscan_esp32/skimscan_esp32.ino in the Arduino IDE with the
ESP32 board package installed, pick a plain ESP32
Dev Module, and upload. No libraries to add — it uses the ESP-IDF Bluetooth APIs the core already
ships.
| Screen | Key | Does |
|---|---|---|
| Sweep | OK |
Open the device list |
OK (hold) |
Throw the sweep away and start again | |
| Devices | ↑ ↓ |
Pick a device |
OK |
Open it | |
| Device | ← → |
DEVICE · WHY · WHAT NOW |
| How skimmers work | ← → |
Walk the six panels |
From the release — grab skimscan.fap from
Releases and drop it in
SD Card/apps/Bluetooth/. It appears under Apps → Bluetooth → Skimscan.
From source — needs ufbt:
python3 -m pip install --upgrade ufbt && ufbt update --channel=release && ufbtThen ufbt launch with the Flipper plugged in, or copy dist/skimscan.fap across yourself.
No ESP32 to hand? Turn on Settings → Demo mode and sweep. The scripted forecourt runs off the tick and every screen works.
The engine is the product — everything on screen is a rendering of what skim_score() decided,
and no screenshot can vouch for any of it. So every table entry, boundary, cap and invariant is
checked on the host on every push:
make -C test
329673 checks, 0 failures
That number is mostly one thing: an exhaustive sweep of the whole evidence space — every combination of address, name, class, radio, level, pass count and threshold — asserting the claims the caps exist to make. Among them:
- a score may never exceed its raw total, and a capped score must always say why;
SKIMMER?requires three families, an identity signal, and a second sighting;NOTErequires more than sitting still;- LE is never penalised for behaving like LE (random addresses and nameless beacons are normal there);
- and the wording rules — no headline may claim safety, no cap reason may be too long to print, no line of explanation may be too wide for a 128-pixel screen.
There is also a false-positive suite: every factory name in the table must match itself, and a car
park's worth of ordinary Bluetooth (iPhone, JBL Flip 5, Ford SYNC, ELM327, AirPods Pro, …)
must match nothing.
- Most skimmers are not radios. They store to flash, or use GSM, or are a pinhole camera over the PIN pad. None of those are visible to this. A quiet sweep is not a clean pump.
- A renamed module defeats the name table. It is one AT command. That is why a name alone is capped well below a verdict.
- A hobbyist's HC-05 will score. So will a robot kit in a rucksack, until it leaves. The pass counter is what sorts that out, and it needs you to sweep for more than a few seconds.
- RSSI is not distance. "Close" is a rough proxy for "inside this pump", and metal panels make it a poor one.
- The MAC tables are not exhaustive and never can be. They cover the module makers that turn up in practice.
- This is a screening aid, not evidence. It tells you what it heard and why it scored it. The judgement is yours.
- Do not open a pump, pull at a reader, or touch wiring. If a pump scores, pay inside, tell the staff which pump it was, and report it.
skimscan.c / skimscan_i.h app shell, alarm ladders
helpers/skim_sigs.{c,h} signature tables: names, OUIs, class of device ← pure, host-tested
helpers/skim_score.{c,h} the scoring engine and its caps ← pure, host-tested
helpers/skim_db.{c,h} device table, pass counting, worst-first order
helpers/skim_link.{c,h} UART worker and line protocol to the companion
helpers/skim_store.{c,h} settings persistence + CSV sweep log
helpers/skim_demo.{c,h} the scripted forecourt
views/ sweep, list, detail (3 pages), learn (6 panels), wiring, splash, card art
scenes/ start, sweep, list, detail, learn, wiring, settings, about
esp32/skimscan_esp32/ the companion firmware (Arduino / ESP-IDF APIs)
test/ host tests for the engine, and the demo dump the mockups draw from
tools_gen_*.py icons, banner and mock screenshots (Pillow)
MIT — see LICENSE.
Built by at0m-b0mb. Part of a family of Flipper Zero counter-surveillance tools: Nyx finds hidden cameras by the infrared they emit, Vulpes finds hidden transmitters by the RF they emit, and Skimscan finds the radio a card skimmer gives itself away with.
Sweep pumps you are about to use, or where you have permission. Report what you find — do not touch it.













