Skip to content

Craig Explainable rental-scam checks

A second look before a renter sends money.

Craig promo with the captain mascot sailing under the Golden Gate Bridge beside the line Ready to win the housing war?
Role
Product · Design · Engineering
Timeline
Sep–Oct 2026
Shipped
Chrome Web Store · 16 checks
Stack
Go · WebAssembly · TypeScript

Flagging rental scams honestly meant designing for unrun checks, compiling a Go rule engine to WebAssembly, and showing the listing words behind every flag.

01 · Context

The risky moment is the minute before a renter sends money.

Craigslist rental scams follow a familiar pattern: photos copied from a real Zillow or Redfin listing, a rent well below the neighborhood, an owner who is conveniently out of the country, and a deposit by wire or gift card to hold the unit. Each one is easy to spot alone and easy to miss under deadline pressure.

Craig started as the counterpart to an earlier apartment-search tool. That project paid an API to find and filter listings. Craig works the other way: the renter finds the listing, and the tool judges it in place, running the costly checks only when asked.

Craig result panel showing a Scam likely verdict, 14 of 16 checks ran, a score of 100, reverse-image matches against Zillow, and highlighted listing phrases
One result panel shows the verdict, the coverage line, matched Zillow photos, and the exact listing words behind each flag. Sample listing from the launch page.

02 · The decision

A check that could not run is not a check that passed.

Show the evidence. Count the coverage. Never let the scored party argue the score down.

Photo checks need a Google Cloud Vision key and a monthly free-tier budget, so "no key" and "budget used up" are everyday states, not errors. A model with only matched or not matched would show those states as silence, and silence reads as reassurance. Every result therefore separates not-evaluated checks from passed ones and leads with a line like "14 of 16 checks ran." If too few checks ran, the result switches to a Partial check state no matter what the score says.

The same rule shaped the scoring. An early design let green flags, such as "happy to meet in person," lower the score. That fails against an adversary: anyone who reads a shared rule set can pad a listing with reassuring phrases and cancel a real payment red flag. Green flags are still shown, but the rule parser rejects any weight other than zero for them.

Calibration got the same treatment. A fee, however large, is not proof of fraud, and neither is a low rent. Fee checks, the title-versus-rent mismatch, and the HUD below-market comparison are caution-only by design, with parser checks and tests. A feature request that would have let an extreme fee raise a hard flag was caught against that test and reverted before it shipped.

Engineering noteSkipped results are a typed state, not a missing value.

Every signal result carries a closed set of skip reasons, and the assessment model keeps a separate not-evaluated bucket next to passed and flagged checks.

  • Skip reasons include no_api_key, vision_budget_exhausted, no_images, missing_field, and provider_error, and each is shown in plain language.
  • Skipped and failed provider results are never cached as clean, so they are retried rather than remembered as an answer.
  • The parser rejects any nonzero green-flag weight and any hard flag on fee checks, so editing or importing a rules file cannot get around either rule.

03 · What shipped

Sixteen checks, an engine in the browser, and evidence on every flag.

The built-in checks cover payment methods with no recourse, owners who say they can't meet in person, pressure tactics, relay-only or disguised contact details, nonstandard fees like holding fees or key money, fees above typical limits, payment asked for before a tour or keys, rent that doesn't match the title, and rent at or below 65% of HUD's FY 2027 benchmark across 1,395 ZIP codes in the Bay Area, Los Angeles, and the New York metro. With a key, photo checks look for listing images on real-estate sites, stock photos, and MLS watermarks.

Each flag shows its evidence. Text rules highlight the exact phrase with four words of context on each side, and photo matches show the listing image next to the matching image and the page where it was found. A collapsed execution log records the real extension, engine, detector, cache, and Vision steps for each request, without credentials, listing text, or image URLs.

The first build was a Go daemon on localhost that the extension called with a bearer token. A launch-readiness review found the real barrier to adoption was installation, not detection: renters at the highest risk would not run a terminal command. The shared Go engine was then compiled to WebAssembly and moved inside the extension, so installing from the Chrome Web Store is the whole setup, and the daemon remains as an open reference build.

Readable at a glance

Grid of six Craig mascot poses labeled Low concern, Look closer, Verify first, High concern, Scam likely, and Partial check
Six result states, each a front-facing variant of one locked base sprite. Scam likely and Partial check add a hat glyph to their sibling state, so each pair can't drift apart.
Engineering noteOne Vision call per photo serves every image rule.

Vision bills per feature per image against a 1,000-unit monthly free tier, so the engine shares one response across all the image rules that need it.

  • A MatchGroup binds match tokens to the rule that owns them, so a single WEB_DETECTION call serves both the reverse-image and stock-photo rules while keeping each flag attributed.
  • Only images.craigslist.org URLs are sent to Vision. The extension caps photos per listing, keeps a monthly budget per check type, and caches provider evidence for 24 hours, re-scoring it against the current rules each time.
  • The extension requests only activeTab and storage permissions plus Vision's host. The Vision key stays in extension storage and is sent only to Google.

04 · Evidence

The live site kept finding cases the fixtures missed.

Several real failures came from the gap between saved test fixtures and live Craigslist pages. A listing with a "$400 admin holding fee" scored clean because the fee was in a structured attribute block the extractor never read. The fee rule was correct but never received the text. Later, current listings paired each photo with a cropped thumbnail the URL normalizer did not recognize, so photos were sent twice and listings with 13 or more photos failed the payload limit. Both fixes started with a failing test built from the real markup, and the fixture was rewritten to match live pages.

A reverse-image miss led to a stricter view of what a no-match result means. Replaying a user-supplied photo pair, nearly identical pixel for pixel, through two fresh Vision requests found no match, and the source photo wasn't among the visual candidates either. The no-match wording now names the provider, coverage counts show how many photos were actually checked, and the launch page tells renters to double-check with Google Lens instead of claiming parity.

On the release branch, the extension suite records 36 passing tests after the thumbnail fix, checked against four live listings with 9 to 24 photos each. The mascot pipeline records 73 passing tests, including a locked base-sprite snapshot. No install, retention, or false-positive data has been collected yet, so these claims rest on shipped behavior, recorded tests, and live-listing replays.

05 · Reflection

Trust came from admitting limits, not from a confident number.

The key product decision was treating coverage as part of the answer. Every later feature — fee cautions, the HUD comparison, photo provenance, and the Partial check state — followed from refusing to let an unrun check look like a clean one.

The engineering lesson was about calibration. Rules like "fees are never hard flags" and "green flags weigh zero" only held because the parser enforced them. A reasonable-sounding feature request nearly broke one, and a test caught it.

The next gap is measurement. The thresholds for the HUD ratio, the 10% rent mismatch, the $50 application fee, and the Partial check cutoff are reasoned defaults, not tuned ones. The next step is a labeled sample of listings from each supported metro to measure false positives and to see whether renters read the coverage line before the score.

Continue with the project

Explore Craig in context.