← operator playbook
Jun 09, 20266 min read

The screenshot problem: detecting fake payment proofs at scale

Automated cashiers rely on payment screenshots to trigger balance credit. Bad actors know this. Here's the layered detection stack we run to reject fakes before an agent ever sees them.

fraudoperationsengineering

Every fiat-adjacent cashier we've audited has the same weak link: a screenshot upload. The player pays via Zelle, Cash App, or a wire, and uploads proof. An agent — or a bot — approves the screenshot, and the balance credits.

The moment fraudsters find the pattern, the screenshots start looking too good. Correct fonts, correct layout, plausible transaction ID. The naive approach — trust the agent's eye — falls apart at any real volume.

Layer 1: metadata

EXIF data on the uploaded image tells you more than most operators check. Was it generated by a screenshotting tool on a phone (expected) or a desktop image editor (suspicious)? Was it re-encoded through a Telegram compression pass (fine) or through a photo-editing app's export pipeline (not fine)?

We reject any image whose creation timestamp is more than 24 hours before submission, or whose editor tag matches a known image-manipulation app.

Layer 2: perceptual hashing

A player who submits the same screenshot twice — same pixels, same crop — is either confused or malicious. Either way, the second submission needs a human. We hash every accepted screenshot with a perceptual hash (pHash) and reject exact matches on sight. Near-matches (Hamming distance under a threshold) get routed to review.

phash(image) = 91d3f4a2
phash(prior_accepted) = 91d3f4a2
> reject: duplicate proof submission

Layer 3: structural OCR

Real payment screenshots have predictable structure. Cash App confirmation pages have the amount in a specific font at a specific vertical position, above the payment ID. Zelle has a green check and a recipient line. Deviations from the template — amount in the wrong place, missing timestamp, wrong currency glyph — are cheap to detect and expensive to fake.

We OCR every screenshot into a structured record, then validate it against the expected template for that rail. Structural mismatches don't reject the payment outright — they route it to review with the specific field highlighted.

Layer 4: cross-rail verification

This is the one that catches sophisticated fraud. The screenshot claims a $500 Cash App payment at 14:22 UTC. We check the operator's Cash App account for a matching credit within a 15-minute window. No match, no balance credit. Fraudsters can fake the screenshot; they cannot fake the money actually arriving.

Why the layers matter together

Any one of these layers can be defeated by a determined attacker. Layered, they aren't. A fraudster who scrubs EXIF still trips the perceptual hash on their second attempt. A fraudster who generates a fresh screenshot still fails cross-rail verification. A fraudster who somehow does actually pay the operator has stopped being a fraudster — they're a customer.

At operator scale, the objective isn't zero false positives. It's driving the cost-per-fraud-attempt above what the attacker can profitably absorb. The layered stack does that without adding agent time.

// related reading