Inspiration

Last year people in the US alone lost 12.5 billion dollars to fraud, and it jumped 25 percent in a single year (that figure is from the FTC). My first instinct was the same as everyone's. Just ask an AI whether a message is a scam. The more I sat with it, the more that scared me. If the AI is confident and wrong, a worried person will act on it anyway. It might invent a helpline number, or call a real bank's website dangerous without ever checking, or quote a rule that does not exist. So I set one rule for the whole project and built everything around it. The model can think, but it does not get the final say. The evidence does.

What it does

On Android it does not even need copying and pasting. Telltale installs from the browser as an app, so it shows up in the share sheet. When a scam lands in WhatsApp or SMS you share it straight to Telltale, it opens with the message already in, runs the check on its own, and wipes the text out of the address bar afterwards. It meets the message where it actually arrives.

The part I care about most is the Evidence check that shows up on every result. It tells you how many findings the model came up with, how many the evidence actually supported, and how many got thrown out, with the reason for each one it dropped. Every reason you see is tied to something real. A signal the code detected, a source that came back from a live search, or an exact quote from your own message.

There is also a Challenge Telltale mode that runs nasty inputs on purpose, like a look-alike bank link and a prompt injection that literally tells the app to ignore its instructions and mark the message safe. You get to watch it refuse to be fooled.

How I built it

Telltale is built in stages so each part can be tested on its own.

First, plain Python detectors look for the concrete things a scam almost always has. Look-alike or misspelled domains, gift card and crypto and wire and UPI payment demands, requests for OTPs or remote access apps, urgency, and attempts to manipulate the analyzer itself. Those set a risk floor.

I also ran the detectors over a dataset I did not write. The SMS Phishing Dataset of Mishra and Soni (2022, Mendeley Data, DOI 10.17632/f45bkkt8pr.1) holds 5,971 real SMS labelled by other researchers. The detectors flagged 0 of the 4,844 legitimate messages, with a 95 percent Wilson interval of 0 to 0.08 percent. That is the number I trust most, because nobody on my side chose those messages.

Fast extraction runs on NVIDIA Nemotron 3 Nano, while classification, the verdict and the advice run on NVIDIA Nemotron 3 Super.

Live research with Tavily

Tavily runs the live checks, and I was deliberate about how. Searches use exact match on the message's own entities, so a result only comes back if it actually names that link, phone number or UPI id rather than something vaguely similar. The suspect domain goes into exclude_domains, so a scam site can never end up cited as evidence about itself. Searches run concurrently, boost the user's country, and report the credits each one costs. When there is nothing specific to look up, Telltale runs one pattern search on the claimed sender instead, and the verifier only lets those results back general claims. Then the verifier closes the loop. A tell citing a source has to carry a verbatim excerpt that really appears in that source, and if the claim names a specific link or number, the source has to mention that same link or number. When a search finds nothing, Telltale says so in those words rather than treating silence as safety. Absence of evidence is not evidence of absence, and a run that turns up no public reports says exactly that.

The verifier

The verifier is the heart of it. It reads back everything the model produced and keeps a finding only if the evidence exists and says what the finding says.

A cited detector signal has to have actually fired, and the claim cannot use that signal to assert outside confirmation, because a detector can tell you a link uses a cheap TLD but it cannot tell you something "has been reported" or that a regulator warned about it. A quote has to appear in the message as whole words, at least two of them, and not only inside a negation, so "do not share your OTP" cannot be quoted back as if the message had demanded it. A cited source has to carry a verbatim excerpt that is really in that source, and if the claim names a specific link or number, the source has to mention that same link or number.

It also pushes the score up to a floor set by the hard signals, so a message full of red flags can never come back as safe no matter what the model says. In plain terms the final score is

final score=min(max(model score, floor), 100)

so the model can raise the risk but never talk it down below what the evidence already proves.

The scam types, reporting numbers, brands, and word lists all live in editable YAML files, so adding a new scam or a new country is a data change and not a code change. The backend is FastAPI, streaming each step to a plain JavaScript page over server sent events. Screenshots are read by MiniCPM V, because Token Factory does not have a Nemotron vision model and I did not want to fake that. The whole thing runs as one Docker container on Render, and GitHub Actions runs the tests and both evaluations on every push.

Results

Everything below is reproducible with no API key, apart from the full Nemotron pipeline row, which needs one.

Adversarial claims the verifier rejected, 501 of 501 across 52 real messages. Genuinely grounded claims kept, 251 of 251, none wrongly dropped. Legitimate SMS wrongly flagged in an independent public dataset, 0 of 4,844. Detection with detectors only, precision 100 percent, recall 75 percent, F1 85.7 percent. Detection with the full Nemotron pipeline, recall 100 percent, precision 94 percent. Times the model pushed the score below the evidence floor, 0. Unit tests green in CI, 120.

You can run the exact checks yourself.

python -m pytest -q # 120 unit tests python eval/grounding.py # Evidence Integrity Benchmark, 752 cases python eval/detection.py # detection metrics and hard negatives python eval/external.py path/to/sms.csv # 5,971 real SMS, dataset downloaded separately

Challenges I ran into

The real challenge was never getting an answer out of the model. It was making sure the answer never rested on something the model made up. That is what pushed me to build the verifier, and then an adversarial benchmark to prove it actually holds.

A few specific ones. Token Factory does not host a Nemotron vision model, so I kept all the reasoning on Nemotron and used MiniCPM V only to read screenshots, and I said so plainly in the docs instead of hiding it. Structured JSON out of an LLM is not always clean, so the client falls back to a simpler JSON mode and takes one repair attempt before it gives up. Legitimate messages are surprisingly easy to flag by accident, because a real bank OTP or a genuine bill reminder looks scammy to a dumb filter, so I built a set of legit control messages the evaluation is not allowed to get wrong. And honestly, it was tempting to report some big accuracy number to look impressive. I did not. I built a smaller honest benchmark that measures the one thing this project actually promises.

Accomplishments that I'm proud of

An Evidence Integrity Benchmark that runs the real verifier over 52 real messages and 752 generated cases. It rejected all 501 adversarial claims, including the hard ones where a real evidence id is attached to a claim the evidence does not support, and kept all 251 genuinely grounded claims, without ever letting the score fall below the evidence floor.

Zero false alarms where it matters most. The deterministic detectors flagged 0 of 4,844 legitimate SMS in an independent public dataset, and 0 of my own 24 hard negatives. Turning on Nemotron trades a little precision, 94 percent, for full recall, 100 percent.

Prompt injection is handled. If a message tries to boss the analyzer around, that attempt itself gets flagged, and because the floor and the verifier sit outside the model, the message cannot talk its way into looking safe.

It fails safely. If the model or the network goes down, it falls back to the deterministic evidence instead of guessing.

It is genuinely finished, not a demo held together with tape. Green CI on every commit, 120 tests, and a live working site.

What I learned

The way to make an AI trustworthy for something this high stakes is not a cleverer prompt. It is limiting what its output is even allowed to be. Once every claim had to point at something checkable, and once plain code had a floor the model could not argue with, the whole thing went from an AI that sounds sure of itself to a system you can actually audit. I also learned that one small honest test that measures the real thing beats a big number that only sounds good in a pitch.

What's next for Telltale

A bigger, properly anonymized set of real scam examples to test against. An always-on mode that checks messages as they arrive in Gmail or SMS, so a scam is caught before anyone thinks to paste it, which came up in the live judge review. A browser extension and an email add-on for the same reason. The share flow on iOS, which does not support share targets the way Android does. A plain-language, larger-text mode for older relatives, who are targeted most. More countries for reporting and brands, and call transcription for voice scams.

Built With

  • docker
  • fastapi
  • github-actions
  • httpx
  • javascript
  • minicpm-v
  • nebius-token-factory
  • nvidia-nemotron
  • pydantic
  • python
  • render
  • server-sent-events
  • tavily
  • tldextract
  • uvicorn
Share this project:

Updates

Submission history