The scanner
The scanner gives every new and edited message in the scanned channels a score from 0 to 100,
decides what to do from the server’s thresholds, does it, and posts a mod card. It runs in
memory, needs no network (no link is ever opened), and takes about a millisecond per message.
The rules live in the polar-scan crate, which has no Discord dependency so it can later
scan game chat and e-mail too.
message ──► skip rules ──► signals (links, threat lists, content, behaviour, adult links)
│
▼
fusion: score 0–100
│
thresholds + log-only mode + NSFW-link setting ──► action
│
delete / timeout / kick / ban (Discord) ──► punishment history, counters
│
▼
card queue ──► log channel ──► buttons (Scam / Not scam / Undo)
1. Which messages are scanned
A message is skipped when any of these holds:
- it is not in a server (DMs), comes from a bot, a system user or a webhook, or is not an ordinary message or reply (joins, pins, boosts, …);
- it has no text and no attachments; an edit is scanned only when its text changed;
- the scanner is off in this server (
/scanner disable); - the channel is exempt, or, in Only monitored channels mode, not monitored
(
/scanner channel,/scanner channel-mode); a thread follows its parent channel’s rule; - the author has an exempt role (
/scanner exempt-role); - the author is a Moderator or higher (the server owner and members with Administrator are Owner).
For a forwarded message (empty text), the text of the forwarded messages is scanned, and forwarded attachments count too.
2. Signals
Every rule that matches adds a signal: a reason the card can show and a weight. Weights are log-odds: positive weights make a scam more likely, and several weak signals add up.
| Group | Signal | Weight | Notes |
|---|---|---|---|
| Links | On a threat list | +7.0 | ≈250 000 phishing domains from the configured lists. Sets a floor of 90. |
| On the server’s block list | +6.0 | /scanner block. Sets a floor of 90. | |
| Lookalike of a known brand | +4.5 | Edit distance and Unicode homoglyphs: dіscord.com (Cyrillic і), steamcommunlty.com. | |
| Masked link that hides where it goes | +3.5 | [discord.com](https://evil.example) | |
| Suspicious top-level domain | +2.5 | ||
| Punycode domain | +2.0 | xn--… | |
| IP address instead of a domain | +1.5 | ||
| Link shortener | +1.5 | ||
| On the server’s allow list | −3.0 | /scanner allow. Turns off the link-shape checks for that domain, never the threat lists or the block list. | |
| Content | Scam phrase | +1.0 to +3.0 each | Also in leetspeak; at most 3 phrases count. “free nitro” is +3.0, “promo code” +1.0. |
| Critical phrase | +3.0 | “I accidentally reported you”, “verify your account to avoid a ban”, … Sets a floor of 90, unless the message is clearly about a scam (a warning, a “PSA”, “someone sent me…”, the phrase in quotes); then it counts as an ordinary phrase. | |
| “Move off Discord” step | +1.5 each, +2.0 for two or more | “add me on snap, it’s in my bio”, “texting from my alt because I’m shy”, “text me on my main account”, “saw you were online” | |
| Link to a Discord user profile | +1.0 | Often the last step of such a funnel. | |
| Discord invite | +1.5 | Not with /scanner invites allow:True. | |
| Mass mention | +2.0 | 5 or more user and role mentions. | |
@everyone / @here attempt | +1.5 | ||
| New account (under 7 days) | +1.0 | Only together with a link or a scam phrase. | |
| Joined under an hour ago | +1.0 | Only together with a link or a scam phrase. | |
| Recent punishments | +0.5 each, at most +1.5 | Bans, kicks, timeouts and warnings in this server in the last 180 days. | |
| Behaviour | Hacked-account burst | +5.0 | The same content with a risky link in 3 or more channels within 120 s. Sets a floor of 97. |
| Flood | +1.5 | 8 or more messages within 10 s. | |
| Adult | Link to an adult site | 0 | A separate category (below); never part of the score. |
Official domains (discord.com, discord.gg, steamcommunity.com, github.com,
youtube.com, …) are never flagged by the threat lists or the lookalike check, even if a list
contains them. At most 20 links per message are looked at.
The behaviour tracker keeps, per member and server, a fingerprint (a 64-bit simhash) of their last messages for 2 minutes; two messages match when their fingerprints differ in at most 3 bits. Only links that could be malicious count towards a burst: an official or allow-listed link shared in several channels is not one, unless it is on a threat list.
3. Fusion
score = round(100 × 1 / (1 + e^-(−4 + sum of weights)))
score = max(score, floors) floors: listed domain or critical phrase 90, burst 97
Without signals a message scores 2. One lookalike domain (+4.5) gives 62: a card. A lookalike plus “free nitro” (+3.0) gives 97. A domain on a threat list scores at least 90 whatever else the message says.
4. What happens
The server’s settings decide (/scanner thresholds, /scanner punishment, /scanner hacked, /scanner actions):
| Condition (checked in this order) | Action |
|---|---|
| Score below log (default 60) | Nothing (at log level 9 the score is logged) |
Log-only mode (/scanner actions enabled:False) | Card only |
Hacked-account burst and /scanner hacked is not Do nothing | Delete every message of the burst; with Delete the messages and time out, also time the author out |
| Score at or above punish (default 97) and a punishment is set | Delete and punish: timeout (default 1 hour), kick or ban |
| Score at or above delete (default 90) | Delete |
| Otherwise | Card only |
Adult-site links (/scanner nsfw-links): Ignore, Log, Delete or Delete and time
out. Never applied in a channel Discord marks age-restricted (a thread follows its parent; a
channel whose flag cannot be read counts as age-restricted), and only Log in log-only mode.
When both the scam score and the adult-link setting call for an action, the stronger one is
taken.
Repeat offenders: within 2 minutes after the scanner punished a member, further flags of that member only delete the message, so a spam wave gives one punishment and one ping.
Executing an action:
- every Discord request carries the audit-log reason
PolarBot scanner: score N; - a message that is already gone counts as deleted;
- before a timeout, kick or ban, PolarBot checks that the member is not the server owner or the bot and that its own role is above the member’s top role; otherwise the card says why it could not act;
- a timeout, kick or ban is recorded in the punishment history with the bot as moderator (a plain delete is not a punishment and is not recorded);
- the server’s daily counters (flagged, acted on) are increased for
/scanner status.
5. Cards
When the server has a log channel and a log level of at least 1, a flag becomes a card in
the log channel. Without a log channel, or at log level 0, there is no card and no log entry
(the action still happens). A card shows the
author, the score, the action taken (or why it failed), the message text, up to 8 reasons
(strongest first, an adult link always first), up to 20 domains (risky ones marked ⚠), a jump
link while the message still exists, and how many further messages were merged into it. The
mod role (/scanner mod-role) is pinged only for a punishment or a hacked account. User text
is escaped and never pings anyone.
The card is the only record of a flag: everything the buttons need is on the card itself (the
button state in its custom_id, the text and domains in the embed). Nothing about the message
is stored in the database.
Buttons
Only Moderators and above can press them; the level is read fresh from the database. A card’s buttons work once: the first press disables them all, and a second press, even a simultaneous one, gets “already handled”.
| Button | Does |
|---|---|
| Scam | Confirms the flag: puts the card’s risky domains (those with a domain signal of their own, never official ones) on the server’s block list and saves an anonymized training example marked violation. On a log-only card it then offers the moderator (privately) Delete & time out, which checks the moderator’s rights against the member exactly like /timeout before acting. |
| Not scam | Saves an anonymized training example marked ok, lifts the scanner’s timeout or ban, and deletes the scanner’s punishment-history row for it, so a false positive does not count against the member. Nothing is allow-listed (use /scanner allow). |
| Undo | Lifts the scanner’s timeout or ban (shown only when there is one to lift). |
Training examples are anonymized before they are stored: mentions, IDs, the member’s names,
invite codes, e-mail addresses and link paths are replaced with placeholders such as [user]
and [path], no server or user ID is kept, and only the day is recorded. The operator exports
(and thereby deletes) them with polarbot export-training.
6. Dry runs: /scanner analyze
/scanner analyze and the Analyze with PolarBot message command run the same link, content
and threat-list rules with the server’s lists and settings on pasted text, a message link or a
right-clicked message, and say what the scanner would do. They do not feed or read the
behaviour tracker (no flood or burst signals), store nothing, delete nothing and post nothing.
They also check up to three attachments by name and type only (nothing is downloaded):
programs and installers (+4.0), scripts (+2.5), double extensions such as photo.png.exe
(+5.0) and a content type that contradicts the extension (+2.5). The live scanner does not use
the attachment rules yet.
7. Domain lists
The threat lists and the adult-site lists are plain domain lists or hosts files downloaded over https from the URLs in the config file. Each list is cached in the data directory and loaded at startup, so the scanner works right away after a restart, even offline. A new download replaces the cache only when it parses to at least one domain: an empty download, an HTML error page or garbage keeps the previous list. The in-memory index is swapped atomically, so the scanner never waits for an update.