Iterating on ScamDetector, what I changed after publishing
What changed in ScamDetector after publishing. A third AI backend, persistent rate limiting, privacy-friendly logging, action steps by scam type, sharing results as an image, and PWA support.

I published ScamDetector two weeks ago, and by the next day I already wanted to change things. Not because it was broken, but because using it in production with real traffic teaches you things you don't see while developing locally. That's what this article is about, the decisions I made after launch and why each one felt more valuable than adding new features.
If you don't know the project, in the first article I explain what ScamDetector does, and in the second how it's built under the hood.
From two backends to three, and back to two
ScamDetector started with two interchangeable AI backends, n8n and Vercel AI Gateway. n8n let me tweak prompts and models from a visual interface without redeploying anything, which was handy for iterating quickly. But n8n is a heavy piece. It's a full service with its own database, workers, and memory footprint. For a project that basically makes a couple of calls to an AI API, having n8n in the middle started to feel like overkill.
So I added a third path, direct OpenRouter. A fetch() to the OpenRouter API from the Node.js server, no middlemen, no SDK, no workflow engine. Gemini for messages, Perplexity Sonar for phone numbers, Relace Search as a fallback if Perplexity doesn't respond, retries with exponential backoff. In about 200 lines of code it does the same thing as the full n8n workflow.
In the end I removed n8n from the architecture. Direct OpenRouter is simpler, starts faster, and is one less dependency to maintain. Vercel AI Gateway stays as an alternative for anyone who wants to deploy serverless. Of the three backends that coexisted for a few weeks, only two are left, which is what I should've had from the start.
What made it practical to keep multiple paths in parallel without turning it into a headache was pulling all the system prompts into a single file, prompts.js. Before that, each backend had its own copy of the prompt, which meant any tweak had to be replicated in two or three places. Now there's a single source of truth for the message analysis prompt, the prompt for images with no text, phone reputation, and URL extraction. The two remaining backends import the same prompts and run responses through the same normalizeResponse() function. The frontend can't tell who answered.
Rate limiting that survives restarts
The original rate limiting worked fine as long as the container didn't restart. Every time Dokploy redeployed the app, the counters reset and everyone got their 10 fresh requests again. On a low-traffic project this wasn't a serious problem, but it bothered me on principle.
The fix was to persist the counters to JSON files in /app/data/, a directory mounted as a named volume in Dokploy. Every 10 minutes, and on SIGTERM, the server writes the rate limit map to disk. On startup, it reads it back and drops expired entries. Writes are asynchronous with an empty callback so request handling doesn't get blocked.
While I was at it, I added a second layer, a global rate limit of 100 requests every 10 minutes across all IPs combined. The per-IP limit protects against one user abusing the service, but it doesn't protect against someone rotating IPs with a VPN or a botnet. The global one does. If the individual limit is exceeded, the server returns 429 (Too Many Requests). If the global limit is exceeded, it returns 503 (Service Unavailable). The frontend shows different messages in each case so the user understands what's happening.
Privacy-friendly logging
After a few days in production I realized I had no way to know how the tool was actually being used. Not how many visits, I can see that in the Traefik logs, but what kind of analysis people were requesting, how long it took, and what results it returned. Without that information, any decision about what to improve was basically a shot in the dark.
I implemented structured logging in JSONL format (one JSON line per entry) that records every operation handled by all endpoints: message analysis, URL scanning, and URL extraction from images. What gets logged is the IP hash, never the real IP, the phone hash if one was queried, the number of images, not the images themselves, the gateway used, the duration in milliseconds, and the analysis result. The hashes use SHA-256 with a random salt generated on first startup and persisted in the volume. That makes the hashes consistent across restarts of the same container (the salt doesn't change), but impossible to compare across different installations.
The file is automatically purged every 7 days. Cleanup runs in the same setInterval that persists the rate limits, every 10 minutes. I didn't want to set up a separate logging service or send data to third parties, so a local JSONL file felt like the right balance for a project this size. If I need to look something up, cat and jq are enough.
Smart paste
ScamDetector has a form with three main inputs, the text message, the URLs, and the images. In the first tests I noticed people would copy a suspicious SMS, land on the page, and stop to figure out where to paste it. The text field, the URL field, the image upload button... you have to choose.
I added a global paste listener that automatically detects what's in the clipboard and routes it to the right field. If you paste an image, it goes straight to the file upload area. If you paste text that looks like a URL (it has a protocol or looks like a domain), it goes to the URLs field. If it's normal text, it goes to the message field. In all three cases, a toast appears confirming what was pasted and where.
The important detail is that the listener only acts when you're not inside a text field, so it doesn't hijack native paste, and it only fills empty fields. If you've already typed something in the message field and paste, it won't overwrite it. It's about 40 lines of code that change how the tool feels to use.
Help panel and privacy policy
When you share a tool with non-technical people, the first question isn't "how does it work under the hood" but "what can I do with this". I added a sliding help panel that opens from a button in the corner and explains each feature with a screenshot. Message analysis, screenshots, phone numbers, URLs, smart paste, history, and color themes. It's implemented as a drawer with trapped focus (tab trap), Escape to close, and full ARIA support for screen readers.
I also wrote a privacy policy that explains exactly what data is processed, where, and for how long. Not the usual endless legal page, but a clear explanation. That IPs are hashed and not stored in plain text. That images are processed in memory and discarded right away. That text is sent to AI models through OpenRouter. That the log is purged after 7 days. That there are no cookies, analytics, or tracking of any kind.
I don't know how many people read it, but just having it there serves two purposes. First, real transparency for anyone who checks it. Second, writing it forced me to review exactly what data each part of the code touched, and that alone was worth it.
What to do after the analysis
At first, ScamDetector gave you a result and that was the end of it. Risk level, explanation, generic recommendation. But if someone gets a banking phishing SMS and the tool says "high risk, probably phishing", the immediate question is "okay, now what do I do?".
I added concrete action steps tailored to each type of scam. If it detects banking phishing, the steps are contacting the bank through its official channel, changing passwords, and turning on two-step verification. If it's Bizum inverso, check whether they're asking you to send money instead of receiving it. If it's fake parcel delivery, verify tracking on the carrier's official website. There are thirteen scam types with their own steps, plus generic fallbacks by risk level for cases that don't fit any specific category.
I also added the option to share the result as an image. The browser generates a styled PNG with the Canvas API at 2x resolution that includes the risk level, the scam type, the explanation, and the recommendation, all with the tool's cyberpunk look and adapted to light or dark theme. If your browser supports the Web Share API, you can send it directly through WhatsApp or save it. If not, it downloads as a file. The idea is that if someone analyzes a suspicious message received by a family member, they can forward the result visually without having to explain what the tool says.
PWA, minification, and clean shutdown
ScamDetector can now be installed as an app on your phone. A service worker with a hybrid cache strategy (cache-first for static assets, network-only for API calls) makes the interface load even without a connection. You won't be able to analyze messages without internet because calls to the AI models need the network, but you can still check local history and have the app ready for when the connection comes back.
On the build side, I added a minification stage with esbuild to the multi-stage Dockerfile. The first stage minifies JavaScript, CSS, and the service worker, the second copies the resulting files over the originals. The result is about a 35% reduction in the size of the served assets, without changing the development flow (locally you still edit the unminified files).
And one change you can't see, but that prevents real problems. The server now handles shutdown in an orderly way. When Dokploy redeploys and sends SIGTERM, the process stops accepting new requests, waits for the in-flight ones to finish, up to 10 seconds, and only then closes. Before that, a redeploy could cut responses off halfway through if it happened during an active analysis.
Small tweaks that change how the tool feels
Some changes don't show up in a feature list, but anyone using the tool for two days notices them on the third. A big part of the recent iterations was pure interface refinement, and it's worth talking about because those details add more to the perceived experience than any new feature.
The phone field used to sit inside a generic cyberpunk-style frame. It worked, but the user's phone is where they copy suspicious SMS messages from, so it made sense to lean into that metaphor. I redesigned the input as a realistic iPhone frame with Dynamic Island, a status bar with WiFi and battery icons, side buttons, and a gesture bar. The explanatory hint inside the frame is styled like an iMessage system note, with the typography and light gray tone Apple uses for that kind of message. The bezel uses a blue metallic gradient that picks up the site's cyan glow. The result is that when you open the page, the phone field already tells you what to paste there without needing extra text.
At the same time I added clear (X) buttons to the message textarea, the phone input, the URL input, and later the batch URLs textarea. They're elements that only appear when the user has typed something, stay out of the tab order with visibility: hidden when hidden, not just display: none, which screen readers may handle differently, and respect the 44-pixel touch target required by WCAG 2.2 via padding in content-box. Before, fixing a paste mistake meant selecting everything and hitting delete. Now it's one tap. You feel the difference every time someone pastes into the wrong place for the first time.
The "Scan URL" button used to be a separate button next to the analyze button. Over time I realized that when someone pasted only a URL and clicked either one, they got almost identical output, except the main button put the AI analysis on top. Two buttons for the same thing is one too many. I moved URL scanning into an inline action inside the input itself, a magnifying glass with a solid background and glow to show it's clickable. It changes state when the URL is valid, has a descriptive aria-label, and respects the touch target. In batch mode (a textarea with several URLs) the original button comes back below as "Scan all URLs", because a textarea doesn't support inline actions without extra complexity. A design decision that simplifies the main mode without breaking the advanced one.
The last tweak came from a bug that had been sitting there without me noticing. Phone reputation lookup sometimes returned a riskLevel of "low" or "unknown" along with a report that clearly described a scam, with phrases like "classified as a scam", "8/10 danger score", or "not trustworthy". The user saw a reassuring green badge next to alarming text, which is exactly the worst way to present a result. I added a deterministic guardrail that detects scam signals in the report text (keywords like scam, fraud, phishing, not trustworthy, spam, scores between 7 and 10, negative classifications) and bumps the badge from low or unknown to medium when there's a conflict. High is never lowered, and clean text is never raised. It's four small unit tests covering the expected cases. It's the kind of change nobody asks for, but it stops someone from ignoring an important warning because the color told them the opposite.
Iterating on what you've deployed
The temptation after publishing a project is to move on to the next one. But the time I put into these changes improved ScamDetector more than the weeks before launch. Persistent rate limiting, shared prompts, logging, action steps, and the PWA aren't features that sell in a README, but they're the ones that make the tool actually work well when someone uses it.
The code is still public in the repository, and if you want to try it, it's at scamdetector.josemanuelortega.dev.
What came after these iterations was a round of security hardening with Cloudflare Turnstile and ephemeral session tokens, prompt injection detection, Unicode sanitization, progressive penalties, and more than 240 tests (unit, integration, and end-to-end) with the native Node.js runner. All of that is in the next article.
Another entry in the Proyecto ScamDetector series. You're coming from Architecture, AI proxy, and security and continuing with Hardening security in production.

Jose, author of the blog
QA Engineer. I write out loud about automation, AI and software architecture. If something here helped you, write to me and tell me about it.
Leave the first comment
What did you think? What would you add? Every comment sharpens the next post.
If you liked this

Privacidad al máximo en ScamDetector, Vertex con ZDR y modo ofuscado
Cuatro cambios para que la privacidad de ScamDetector deje de ser un banner y esté en el código. Routing forzado a Google Vertex con Zero Data Retention, selector de envío con ofuscación server-side, política RGPD reescrita desde cero y fuentes autohospedadas para no filtrar la IP del usuario a Google.

Alertas push en el móvil con ntfy self-hosted
Añadí notificaciones push al móvil para ScamDetector con ntfy self-hosted. Cuatro alertas en tiempo real (inyección, ban, brute-force, backend caído) y tres monitores periódicos (digest diario, uso de disco, pico de tráfico). Un módulo de 90 líneas sin dependencias.

La arquitectura de ScamDetector, un proxy de IA que no expone secretos
Cómo está construido ScamDetector por dentro, desde el proxy server-side que protege las claves de API hasta los tres backends de IA intercambiables y las medidas de seguridad.