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.

Built by the same team. Disclosed up-front.

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 /comments request 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 PENDING for moderator review — never APPROVED, 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 (default 0.85) — at or above this, the comment is saved with status SPAM and never appears in the public thread. The comment row is kept rather than deleted so moderators can inspect false positives later.
  • SIFTFY_REVIEW_THRESHOLD (default 0.50) — between this and the block threshold, the comment is held in PENDING for the moderation queue. Visible to its author, not to the public.
  • Below the review threshold, the comment defers to the site's auto_approve toggle: 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.

python
# 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.post on the comment-submit path with a 5-second budget.
  • Three statuses, two thresholds, one model.APPROVED, PENDING, and SPAM map directly to product UX — published, in-queue, hidden — without retraining or a handcrafted decision tree.
  • Per-site escape hatch. The spam_filter column 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_score alongside 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.
Try Siftfy free

10,000 classifications / month free. /v1/predict reference, related patterns: comments, contact forms.