CommentObserver on create and on body edit.
Checkers run in
spam.checkers order. The worst verdict wins (deny > review > allow). A deny short-circuits remaining checkers. Reasons merge.
When an external checker throws or times out, spam.fail_mode is allow (fail open) or review (hold). Failures are report()ed.
Built-in checkers
heuristic (default, no network)
Configured under spam.heuristic:
With
guests.stricter_spam (default true):
- Guest missing email while
guest.require_email→ denyguest_missing_email - Guest with any link → review
guest_with_link - Duplicate window also matches guest email + IP
akismet
Set AKISMET_KEY. Posts to {key}.rest.akismet.com/1.1/comment-check with IP, user agent, author, email, body, and comment_type reply or comment.
- Akismet
true+ headerX-akismet-pro-tip: discard→ denyakismet_discard - Akismet
trueotherwise → reviewakismet_spam - Anything else, or missing key → allow (skips)
.env. Blank key at runtime is a no-op.
toxicity
Google Perspective Comment Analyzer. See AI toxicity. Add 'toxicity' to spam.checkers and set COMMENTIFY_PERSPECTIVE_KEY. Billed per call.
Logging
Non-allow verdicts persist tocommentify_spam_logs when spam.log is true: user_id, ip, body_hash (SHA-256), 250-char excerpt, decision, reasons, checker. The pipeline runs before the row exists, so logs store a hash rather than a comment id.
”Not spam” in Filament
On the Spam Log resource, Not spam re-approves the matching held comment (hash + author) and submits ham to Akismet when configured. Unmatched logs still write an audit row (not_spam_unmatched). See Filament.
Custom checkers
ImplementUsamamuneerchaudhary\CommentifyPro\Contracts\SpamChecker and list the class name in spam.checkers (built-in aliases heuristic, akismet, toxicity are resolved first; anything else is treated as a FQCN):
CommentCandidate includes body, author, ip, userAgent, parentId, guestName, guestEmail, plus links(), isGuest(), and authorFrom().

