Ask ChatGPT Ask Claude

Act on a verdict

Every Bewary answer carries one of four verdicts. Block or warn on scam, warn on suspicious, let safe through, and never treat unknown as safe: unknown means the evidence was not enough or a source failed. The code below does that for an inbox, a sign-up form and a link preview, with the retries and timeouts that match each endpoint.

What each verdict means and what to do

VerdictInbox or chatSign-up formLink previewAI agent
scamWarn loudly, hide the linkRejectNo preview, mark the link dangerousStop, tell the user why
suspiciousWarn and show whyReject, or ask for another addressShow the bare link, no preview cardAsk the user first
safeNo warningAcceptFull previewContinue
unknownSay it could not be verifiedAccept (a Gmail address answers unknown)Show the bare link, no preview cardAsk the user first

Never treat unknown as safe. A check that would have been safe but lost a source answers unknown, never safe. Verdicts can be wrong, so when one blocks a person, such as a rejected sign-up, show them a way to reach you. Build a preview card (fetch the page from your server) only for a safe link: fetching a scam page can itself trigger it.

One function for all four

Python
def decide(result, context="inbox"):
    """Map a Bewary answer to an action. context: inbox, signup or link."""
    verdict = result["verdict"]
    if context == "signup":                # a throwaway address answers suspicious
        return "reject" if verdict in ("scam", "suspicious") else "accept"
    if verdict == "scam":
        return "block"
    if verdict == "safe":
        return "allow"
    return "warn"            # suspicious or unknown
TypeScript
import type { Envelope } from "./bewary"

export function decide(r: Envelope, context: "inbox" | "signup" | "link" = "inbox") {
  if (context === "signup") return r.verdict === "scam" || r.verdict === "suspicious" ? "reject" : "accept"
  if (r.verdict === "scam") return "block"
  if (r.verdict === "safe") return "allow"
  return "warn"              // suspicious or unknown
}

Show the user why

reason_codes lists why, strongest first. For a lookalike link or domain, the domain entity’s facts.lookalike.brand_id is the brand it imitates. Turn the first code into a sentence your users understand, or name the brand: “This link imitates USPS”. Every code is listed on the reason codes page.

Finer control

p_scam (0 to 1) and risk_score (0 to 100) let you set your own line, such as warning from 0.5. Until the calibrated model ships, models.calibration reads interim-band-v0 and the number follows the verdict’s band, so prefer the verdict for now.

Timeouts, retries and failures

CallMeasuredTimeout to set
Lookupsmedian 113 to 477 ms10 seconds
Deep link or domain lookupLonger: it reads the page60 seconds
Batch of 1,000depends on the mix120 seconds

Retry 429 and 503 after the Retry-After header, and a 500 once. Do not retry 400, 401 or 402: the request or the key has to change. A 401 means the key is missing or wrong: every call needs one.

Python
import time, requests

def bewary(method, path, timeout=10, max_wait=5, **kwargs):
    """One API call with the retries the API supports. max_wait is the longest Retry-After
    worth waiting for: keep it short in a request a person is waiting on."""
    r = None
    for attempt in range(3):
        try:
            r = requests.request(method, "https://api.bewary.app" + path, timeout=timeout, **kwargs)
        except (requests.Timeout, requests.ConnectionError):
            if attempt == 2:
                raise
            continue
        retry_after = r.headers.get("Retry-After", "")
        wait = int(retry_after) if retry_after.isdigit() else 2
        if r.status_code in (429, 503) and attempt < 2 and wait <= max_wait:
            time.sleep(wait)               # a daily-limit 429 waits hours: it is returned instead
            continue
        if r.status_code >= 500 and r.status_code != 503 and attempt == 0:
            continue                       # our failure: retry once
        return r
    return r

When the API cannot answer, decide on purpose: an inbox should show “not checked” rather than “safe”, and a sign-up form should accept rather than block real customers during our outage.

Caching

Lookups reuse recent source data, such as a domain’s registration date, for up to a week. Store verdicts on your side too: re-checking the same link on every page view costs credits and adds nothing.

Updated · This page as Markdown · Changelog