Skip to content

AI AGENT · REAL ESTATE

Lewis

Every agent owns a list of a few hundred people who once meant it and then went quiet, and nobody works it, because working it is four hundred separate conversations that each start with remembering one specific thing. Lewis reads the export, ranks it, drafts the opener quoting the note you typed two years ago, and waits for your yes. The interesting half is what it refuses to do: a deterministic policy engine sits between the model and every send, and two of its rules cannot be switched off.

Visit uselewis.com
uselewis.com
The Lewis home page, with an iMessage thread between an agent and Lewis over a coastal painting
68k
lines of TypeScript in the product
13
standing rules between the model and anything irreversible
47
test files, one reproduction per compliance bug
41
marketing pages generated from one copy file

Counted from the repository on 21 October 2026, each with a command anyone holding the source can re-run. There are deliberately no customer, revenue or message-volume figures: the product’s own marketing site forbids itself from inventing those, and this page is not going to be looser than the product.

FEATURE BY FEATURE

What Lewis actually does

Twenty-two dormant leads ranked, each with the note that put it there, above a Let Lewis handle it control

01

The same list, in the order you should work it

What comes back is not a longer list. It is the list you already own, ordered by how much there is to open with, each name carrying the reason it is where it is, and the ones to leave alone marked as such. Pre-approved to $415k and went quiet outranks kicking tires.

BUILT WITH

The ranking is a scoring function in code, not a prompt: recency of last contact, engagement signal, and how close the lead is to the next step in their funnel stage. It was a model judgement once and the picks were inconsistent, which is the same reason the policy engine is deterministic.

Three sample messages labelled ALLOW, HOLD and BLOCK, the held one tagged commitment:time and the blocked one property_fact

02

Three verdicts, and they are not three severities

Every draft is screened on its output before anybody sees it. Allow goes to the queue. Hold goes with its reason attached, and hold is the normal verdict for a good draft. Block never reaches the queue, because a message that can be approved by a tired thumb at 7am will eventually be approved by one.

BUILT WITH

Categories are set deterministically by the tool that is about to act, then matched against thirteen standing rules. Fair housing and fabrication are hard gates: the send path refuses flagged text even when the rule is switched off, so the guarantee does not depend on a setting.

The Lewis CRM audit page: find the leads already sitting in your CRM, with an upload box and a note saying nothing is saved without an account

03

A real audit, on your own export, before you trust it with anything

Paste or drop a CRM export and it is parsed, read, scored and ranked in front of you, with the reason for each lead built out of that lead's actual fields rather than a plausible sentence. The page says plainly that nothing is kept unless you make an account, because it is asking a stranger for real client contact details before they have decided to trust the product.

BUILT WITH

The progress stages are the pipeline's real stages and advance when each one finishes rather than on a timer. It is a public unauthenticated endpoint that triggers model calls, so it carries a file-size cap and a per-session rate limit.

An iMessage thread: in the car, anything i need to do, answered with nancy ruiz replied, still wants champlain, moving before the school year

04

It lives in Messages, not in another dashboard

The agent side of Lewis is a thread in Messages. You text it the way you text everybody else, and it texts your leads the same way. In the car, between showings: "anything i need to do?" and back comes the one thing that moved.

BUILT WITH

A lead's inbound reply fires the same event a user's own message does and re-enters the same orchestrator with the whole prior thread as input, which is what makes it converse across a thread rather than answer one message at a time.

01The list everybody owns and nobody works

A working agent's CRM holds a few hundred people who were real leads once. Somebody was pre-approved to 415 and the husband was the holdout. Somebody wanted the Champlain school district before the school year. Those sentences are sitting in a notes column, in that agent's own handwriting, two years old.

Nobody works that list, and the reason is not laziness. Working it means four hundred separate conversations that each have to open by remembering one specific thing about one specific person, and the alternative every tool offers is a drip: the same sentence to four hundred people, which is why nobody answers one.

So the unit of the product is not a campaign. It is one message to one person that could only have been written by somebody who read their file.

02The notes column is the whole trick

Every CRM exports a notes column and almost nothing reads it. "Just checking in, still looking?" is what the drip already sends. "Still after the Champlain school district?" is a question only somebody who knows them could ask, and the difference between those two is the entire product.

The import reads the notes and the draft quotes them. Quotes, not paraphrases, and that distinction is load-bearing rather than stylistic: a paraphrase of a two-year-old note is exactly where an invented detail gets in, and this is a market where an invented detail about a house is a licensing problem rather than an embarrassment.

Numbers that cannot be texted leave at the door rather than being filtered later, which sounds like the same thing and is not. A number that never entered the process cannot be reached by a later bug, a re-run, or somebody tapping the wrong control at 7am. The opt-out list is checked again at the moment of sending anyway, because that one is a legal obligation rather than one agent's filing.

03A policy engine, not a prompt

Thirteen standing rules sit between the model and every action that reaches the outside world. A tool sets a category on what it is about to do, deterministically, in code; the rules decide whether that category may send on its own, must wait for the agent, or may never go at all. The model's opinion is not an input to that decision.

The three verdicts are not three severities of one thing. Allow goes to the queue. Hold goes to the queue with its reason attached, and hold is the normal verdict for a good draft, because any message that names a day or a figure is a commitment and every commitment belongs to the licensed human. Block never reaches the queue.

That last distinction is the one worth defending. A blocked message is not held for review, because holding it would eventually send it: an agent skimming approvals before the school run, one thumb, will wave through "42 Oak is 1,850 sq ft with a new roof" without knowing whether it is true. Not through carelessness, but because it reads like a normal sentence and a queue's job is to be got through. The only way to be certain it never goes out is for there to be no button, which is a stronger promise than any amount of care.

Two rules are hard gates: fair-housing language, and the fabrication rule below. The send path enforces both even when the rule has been switched off in settings, because a compliance guarantee a user can disable is a default rather than a guarantee.

04The night it lied, and what that cost to fix

A four-hour adversarial session against production's own data found something a code review would not have. The agent, replying to a buyer asking about competing offers on a house, wrote that Judson would check with the listing agent. Judson was the listing agent. In the same thread it said he had not connected with the listing agent that evening, and that he was wrapping up his schedule and preparing for tomorrow's client tours. He was at dinner. Six replies, five of them containing a claim about reality that nothing on record supported.

Every guardrail that existed was pointed at structured data: a calendar time, a budget figure, a dollar amount. "Judson checked with the listing agent" is not a field going into a form. It is an assertion about the world, and it was not modelled as anything at all, so nothing was checking it.

The worst of the five is the one that looks safest. "We don't have confirmation on that tonight, but Judson is checking" was the model's own attempt at a careful, hedged answer, and it was false. That is the reason the fix is a fixed library of deflections rather than a better instruction: a model generating its own safe-sounding substitute is the exact failure mode that produced the original lie.

So the rule finds claims by shape, not by wording. An actor, then a contact or verification verb, in one of five forms: done, not done, under way, promised, or a whereabouts. Each is grounded only against records, and an ungrounded one is replaced with a pre-written line. The honest version is always available and always correct: a true "I can't share that" never needs a specific status to be invented behind it.

05Never trust your own paraphrase

The same session surfaced the quieter version of the same bug. A showing was never moved to 11:00, so the record still said 10:00, and four later texts restated 10:00 while the agent's own proposal of 11:00 was still open in the thread. Elsewhere an offer on record at $1,175,000 was reported to the realtor as $1,180,000, because an earlier turn had phrased it loosely and the next turn read the conversation instead of the record.

The guard that resulted is more general than the bug. Any time-bearing, status-bearing or dollar-bearing claim is re-fetched from its source record at the moment the message is composed, never assembled from how a previous turn said it. It is the hallucination rule extended one step: do not invent a value, and do not trust your own earlier paraphrase of a real one either.

Approvals got the same treatment. Expiry was being checked when the approval card was shown rather than when the approved action ran, which on a serverless host means a frozen instance can wake up and send something twenty-seven minutes late. It now re-validates twice at execution: has the approval's own window closed, and has the action's own deadline passed. Either one blocks it.

Every one of those fixes carries a reproduction test that was run against the unchanged commit before it, failed there for the reason described, and passes on the fix. That is the part I would point at: not that the bugs were fixed, but that the repository can still demonstrate they were real.

06How it is built

TypeScript throughout. A Node agent server with a ReAct loop and sixteen tool modules, a React web app, a policy engine and a shared protocol as their own packages, Postgres on Supabase, and the whole thing bundled into a single Vercel function. A lead's inbound reply re-enters the same orchestrator a user's own message does, so the demo path and the production path are the same code and differ only in which message channel is wired in.

The channel is an interface with one method. Today it writes to a local outbox; a real carrier is a registry change rather than a rewrite, and nothing downstream knows the difference. The same holds for the agent surface: Lewis is a thread in Messages rather than a dashboard, because almost none of the real work in this job happens at a desk. It happens in the fifteen minutes between showings, and that is a phone.

The marketing site is forty-one static pages generated from a single copy file, plus two written by hand. The generator reads the real header and footer out of the home page rather than keeping a template copy, so a change in one place lands on all forty-one; the build fails on a CSS class that appears in the markup and in no stylesheet, on a route whose file is missing, and on a logo path that has drifted from its coordinates. That stylesheet is a compiled build, so an un-emitted utility is silently inert: the page still renders, just at the wrong width, and nobody looking at it can tell that was not the intent.

07What is genuinely unfinished

Lewis v2 replaced the original engine in October 2026 and kept the repository, the domain and the marketing site. That site's copy was written against the engine it replaced, and the file that holds it says so at the top: the claims have not yet been re-checked line by line against v2, and that review is open. It is the first thing I would want closed before pushing the product at anyone, because a landing page that is ahead of the code is how a product ends up with a page it has to apologise for.

There are no usage figures on this page because there are none worth stating. What exists is a working engine, a compliance tier with reproductions behind it, and a site. Two agents' instances negotiating with each other is an idea that keeps coming up and is deliberately not being built: it is the same misrepresentation risk this whole compliance pass exists to close, at a higher-stakes version, and it does not get designed until the single-agent case has held up for longer than it has.

08Built with

  • TypeScript
  • Node.js
  • React
  • Vite
  • Anthropic API
  • Tool use
  • Supabase
  • PostgreSQL
  • Vercel
  • Playwright
  • Astro
  • Tailwind CSS
  • Twilio
The Lewis home page hero
The ranked dormant lead list
Allow, hold and block on three sample messages
The free CRM audit page

More work