customer · comments
EchoThreadBeta uses Siftfy to classify every new comment before it goes public.
Updated May 12, 2026
EchoThread is a privacy-first comment platform — a 15 KB embeddable widget for blogs, docs sites, and web apps. Every new comment runs through POST /v1/predict synchronously, on the same HTTP request that submits the comment, before the widget renders it back.
The product
EchoThread is a drop-in comment widget: one <div>, one <script>, and a thread is live on any page. Threaded conversations, emoji reactions, image attachments, Google OAuth for commenters, and a magic-link dashboard for site owners. No third-party cookies, no ad tech, and no data sales — see echothread.io for the full pitch.
The product is free during beta, with paid plans post-beta. Site owners get a moderation dashboard with three buckets — pending, approved, spam — and the Siftfy probability is surfaced as a colour-coded badge alongside each comment in the queue.
Why synchronous, why fail-closed
Comments are the inverse of email. Email is a 1:1 channel into a private inbox; if a borderline message slips through, the recipient sees it and can correct course. A comment is a public broadcast — once it renders for one reader, it renders for everyone. Two consequences for the integration shape:
- Synchronous, not background. The Siftfy call lives on the
POST /commentsrequest path. The submitter is already waiting on a network round-trip; budgeting a 5-second timeout for classification inside that wait is cheaper than building a queue, a worker, and a hold-then-publish state machine. - Fail-closed, not fail-open. If Siftfy times out or returns a 5xx, the comment lands in
PENDINGfor moderator review — neverAPPROVED, even on sites configured to auto-approve. The webmail counterpart fails open for exactly the inverse reason; here, the cost of one held comment is a few minutes of moderator latency, and the cost of one auto-approved spam comment is a public footprint that gets indexed.
Two thresholds, three buckets
Siftfy returns a calibrated probability between 0 and 1. EchoThread reads two thresholds from config and routes the comment into one of three statuses:
SIFTFY_BLOCK_THRESHOLD(default0.85) — at or above this, the comment is saved with statusSPAMand never appears in the public thread. The comment row is kept rather than deleted so moderators can inspect false positives later.SIFTFY_REVIEW_THRESHOLD(default0.50) — between this and the block threshold, the comment is held inPENDINGfor the moderation queue. Visible to its author, not to the public.- Below the review threshold, the comment defers to the site's
auto_approvetoggle: published immediately on auto-approve sites, queued otherwise.
That's the three-bucket pattern the use-case docs describe — same model, two thresholds, no retraining or second classifier.
The integration, end-to-end
The classifier input is just the comment body — no URL, no parent thread, no commenter identity. Authorship and page-context signals are kept out of the classifier on purpose: the spam decision should turn on the text the reader will actually see, not on who's posting it. One value lands on the comment row after classification: spam_score, the raw probability. Status is derived from it.
# Simplified from the EchoThread comment service. The site row carries
# a per-site spam_filter toggle and an auto_approve toggle; Siftfy returns
# a calibrated probability that's compared against two thresholds.
import httpx
async def process_new_comment(comment, site) -> Comment:
# Per-site escape hatch. Some operators run trusted-author-only sites
# where every comment is hand-curated — no point paying for a classifier
# call. The Site row stores the toggle.
if not site.spam_filter:
comment.status = (
CommentStatus.APPROVED if site.auto_approve
else CommentStatus.PENDING
)
return comment
spam_score = await predict_spam(comment.body)
# Fail closed. A Siftfy timeout, network error, or 5xx means we
# couldn't classify — never auto-publish unclassified text to a
# public thread. Borderline mail in a private inbox is one thing;
# an unclassified comment broadcast to every reader is another.
if spam_score is None:
comment.status = CommentStatus.PENDING
return comment
comment.spam_score = spam_score
if spam_score >= settings.SIFTFY_BLOCK_THRESHOLD: # 0.85
comment.status = CommentStatus.SPAM
elif spam_score >= settings.SIFTFY_REVIEW_THRESHOLD: # 0.50
comment.status = CommentStatus.PENDING
elif site.auto_approve:
comment.status = CommentStatus.APPROVED
else:
comment.status = CommentStatus.PENDING
return comment
async def predict_spam(text: str) -> float | None:
# Empty key = disabled. Useful for local dev and for sites that
# haven't onboarded yet. Caller treats None the same as a timeout.
if not settings.SIFTFY_API_KEY:
return None
try:
async with httpx.AsyncClient(
timeout=settings.SIFTFY_TIMEOUT_SECONDS, # 5.0s
headers={"X-API-Key": settings.SIFTFY_API_KEY},
) as client:
resp = await client.post(
f"{settings.SIFTFY_BASE_URL}/v1/predict",
json={"text": text},
)
resp.raise_for_status()
return float(resp.json()["spam_probability"])
except Exception:
return None # Caller fails closed → CommentStatus.PENDING.The moderation dashboard surfaces spam_score as a percentage badge — red above the block threshold, amber above the review threshold, grey below — so a moderator triaging the queue can see at a glance which holds the classifier was confident about and which were borderline.
What we got from it
- One synchronous call, no queue. No worker pool, no Redis, no DLQ. The classifier is a single
httpx.AsyncClient.poston the comment-submit path with a 5-second budget. - Three statuses, two thresholds, one model.
APPROVED,PENDING, andSPAMmap directly to product UX — published, in-queue, hidden — without retraining or a handcrafted decision tree. - Per-site escape hatch. The
spam_filtercolumn on the Site row disables Siftfy entirely for invite-only sites where the operator already controls who can post. The classifier doesn't even get called. - The probability stays on the row. Storing
spam_scorealongside the comment lets moderators see the classifier's confidence in the queue UI and audit borderline decisions later — the same data that drove the routing is the data the moderator sees. - Build cost: a few hours. One service module, one config block, one moderation-dashboard badge. The hardest part was picking the two threshold defaults.
10,000 classifications / month free. /v1/predict reference, related patterns: comments, contact forms.