WhatsApp content moderation
A background-hosted "digital curtain" for a personal WhatsApp account: it links as a companion device, runs messages from chosen contacts through a local LLM classifier, deletes flagged ones locally and warns or temporarily blocks after repeated strikes.

At a glance
- TypeScript, Baileys, Docker Compose, Ollama
Tags
A localised, background-hosted "digital curtain" for a personal WhatsApp account. It links to the account as a headless companion device, runs incoming messages from an operator-chosen set of monitored contacts through an LLM classifier, deletes flagged messages locally ("delete for me"), sends a warning, and temporarily blocks a contact after repeated strikes. Repeated unanswered calls are handled the same way.
A web control app shows the moderated contacts, an activity log, the settings and the policy. It is pre-release: it ships with a shadow mode that only logs, and a live demo with invented data.
The classifier
Messages are judged by a local model run with Ollama, so there is no per-message cost and nothing leaves the machine. Each verdict is structured JSON: who the message targets, whether it is mutual banter and whether it is flagged. Banter, where the user teased first and the contact answered in kind, is let through, except for real threats, sexually explicit messages and pressure after a refusal. The last few messages are sent as context.
The classifier fails open: any error, timeout or malformed answer leaves the message alone rather than guessing. The rules are written in plain language by the operator, and small models follow concrete criteria much better than abstract ones, so naming the topics and the words to look for works, and describing a feeling does not.
Strikes and blocks
A flagged message is deleted, answered with a warning and recorded as a strike, once per incident. Reaching the threshold blocks the contact for a set time, plus or minus some random jitter so there is no detectable rhythm, then unblocks them automatically. Strikes decay with time, not with good behaviour. Each contact can be paused, unblocked early or given a stricter threshold, from the web app rather than through chat commands.
Repeated unanswered calls are tracked separately: past a threshold, the call is rejected with a warning, and a call strike counts toward a block of its own.
Warnings
The warning says plainly that the message was removed, that an automated system and not the account owner is watching the chat, and how many repeats remain before a block. It is sent from hand-written templates in English, French, Spanish and Polish, and an experimental option has a second model write it in the contact's own language.
Running it
It is meant to live on a small always-on machine, with Docker Compose, a two-core CPU-only box being the reference target. State is kept in SQLite, with a full audit log of messages and verdicts, which is the only record once a message is deleted. The control app is a React frontend served by the app, with live updates, and access can be restricted to one identity with Tailscale.

