Projects I’ve built

Applied AI · multi-tenant platform · current work

Keystone platform

One platform for a real-estate investing operation · St. Louis

A multi-tenant backend and operator console that takes an investor from messy lead data to a closed deal: lead generation from public notices, market imports and inbound sites; follow-up in one CRM; an evidence-backed offer range; a photo-based property brief after the walk; and buyers matched to what comes through. Built for a first client, now being generalized for more than one operator.

In development · one operator · first tenant was a client engagement
Illustrative architecture
  1. 01Find and organize leads
  2. 02Follow up in one CRM
  3. 03Offer range from evidence
  4. 04Walk, photograph, brief, close

The problem

A real-estate investor’s day is scattered across sources that don’t talk to each other: public foreclosure notices with a 20-day clock, expired listings and tax-delinquent lists, inbound sellers from the website, photos on a phone after a walk, a spreadsheet of buyers. Every hand-off between those is where leads go cold and numbers get invented. The first client, a St. Louis investor, needed one place where a clean lead arrived on time, follow-up was tracked, an offer had evidence behind it, and a walked property came back as a brief rather than a pile of photos.

What I built

I designed and built the platform as its only engineer and operator: a multi-tenant FastAPI backend on Cloud Run, a Next.js operator console on Vercel, and the pipelines between them. For the first tenant it ran as three audience-specific sites feeding one CRM, an inbound triage service (Scout) that weighed seller leads, a bulk-import pipeline (Deal Hunter) for distress lists, and a public-notice pipeline that put a clean lead in the CRM the day a property hit 20 days from foreclosure. The current work generalizes that into a platform any investor can run their business on, with a research console and a matching buyer system at the center.

The operator’s journey

Six stages, each labeled with what is built, what came from the earlier client build and what is still direction.

  1. 01

    Find and organize

    Built for the first tenant

    Deal Hunter normalizes mass imports (expired listings, tax-delinquent and other distress lists), deduplicates and matches records, and ranks them against the tenant’s buy box. The public-notice pipeline extracts notices, enriches them with property details and delivers qualified leads the same day. Three audience-specific sites capture inbound sellers into one CRM.

  2. 02

    Follow up

    Built for the first tenant · CRM being replaced

    Scout scored inbound seller leads against the buy box and pushed them to the CRM with a verdict. Lifecycle automation created follow-up cadences by status with a dedup guard, drafted call-prep briefs and queued outbound email and direct mail. The first tenant’s CRM was Podio; a Keystone-owned CRM is the direction and not yet built.

  3. 03

    Preliminary offer range

    Built · in use now

    The research console turns an address into a value range from county public records: bounded lookups, validated comparables, a deterministic $/sq ft range with a confidence tier, and a fail-closed state when the evidence isn’t there. That is the recorded replay below.

  4. 04

    Walk, photograph, brief

    Earlier build retained · being folded into the console

    Walk the property, take photos and notes, send them to Scout. PropertyVision analyzes the photos, a repair estimate is priced from a tiered catalog with risk multipliers, and the result is a property brief with a maximum allowable offer and estimated repairs, for the operator to review. It worked in practice: well over the 17 photos in the documented test went through it and the estimates were usable for offer decisions, though I never measured accuracy as a percentage. It is a working tool, now being brought into the console.

  5. 05

    Match buyers

    Intake and matching built · distribution off

    Buyers and capital partners enter their criteria as structured profiles; the platform matches incoming deals to them. Welcome emails and deal-package distribution stay switched off pending review, so matching is operator-visible, not automated outreach.

  6. 06

    Contract, underwrite, close

    Direction · not built

    Contract generation and underwriting to close are the next stages of the journey and exist as design direction only. Nothing on the site claims them as working software.

Built for more than one investor

Every record, buy box and rule is scoped by tenant: API keys carry a tenant and a scope (read, write, paid API, admin), the buy box zones and thresholds live in per-tenant config, and the staged PostgreSQL schema enforces row-level security on every tenant table. The first tenant was a client engagement; the platform runs today with one operator and no paying customers.

Where AI actually sits

Language and vision models sit where the input is unstructured: extracting fields from public notices and inbound email, analyzing property photos for the repair estimate, and drafting call-prep and contact summaries for a person to read. Scoring, ranking, the offer range and the maximum allowable offer are deterministic rules over validated fields. An early extraction run wrote borrower names into address fields, which is why the data contracts now forbid person-identifying keys outright, and why every model output is a proposal a person reviews rather than a write.

Stage 03 · preliminary offer range

See the research console decide.

Five recorded lookups from the platform’s own offline evidence set. Pick one and read what the system did and why. No live calls.

Recorded lookup replay

Pick a case and read what the system did and why.

Internal decision-support figures over candidate-grade evidence. Not an appraisal, not a defensible valuation, not verified truth. Subjects and comparables are anonymized; no live source is called.

Attended county facts and a 17-row sold-comp capture. Six comps selected; the subject parcel’s own earlier sale is retained as evidence but excluded from selection.

EnteredSubject A (a single-family parcel in Crestwood, MO 63126)

  1. Route by ZIPSt. Louis County
  2. Resolve parcelretained record
  3. Subject factsRanch · 1,351 sq ft
  4. Comp capture17 accepted · 0 rejected
  5. Select comps6 of 17 included
  6. Value range$348,000 · high
Every step resolved · value range produced

6 comparables selected from 17 accepted rows; 0 requests to the source.

Subject facts

Location
Crestwood, MO 63126
Dwelling
Ranch · Brick · 1 story
Built
1956
Living area
1,351 sq ft · rec room 675
Rooms
2 bed · 2 full / 0 half bath · Full basement
Grade · condition
C · VG - Very Good
Evidence
county assessor public record · candidate grade

Value range

$348,000$315,000 – $379,000

$257.5 / sq ft over 6 comps · confidence high

  • comp count: 6 (high tier needs >= 5)
  • $/sqft dispersion (weighted CV): 0.085 (high tier needs <= 0.12)
  • max comp distance: 0.49 mi (high tier needs <= 0.75)
  • newest usable sale age vs window end: 0 days (high tier needs <= 365)
  • subject-facts completeness: 6/6 (high tier needs >= 5)

score-weighted $/sqft over selected comps: weighted-median point, weighted P25/P75 band widened by ±0.25×CV dispersion, rounded to the nearest $1,000

Stated gaps
  • sold-comp evidence is limited to the county assessment study window 2022-01-01 to 2025-04-01 — sales after the cutoff do not exist in this source
  • interior condition unverified — no walk / photo evidence informs this number
  • comp interior condition and remodel status unknown — county characteristics only
  • post-window sales evidence would improve recency — the newest usable sale predates the study cutoff

Comparables considered 17 rows · window 2022-01-01 to 2025-04-01

#SoldPriceSq ft$/sq ftDist.ScoreDecision
110/3/2023$334,0001,400$2390.07 mi94.3includedrank 1 of 12 by similarity score (94.31)
210/30/2023$339,9001,320$2580.49 mi85.1includedrank 2 of 12 by similarity score (85.07)
39/21/2023$376,0001,368$2750.49 mi84.9includedrank 3 of 12 by similarity score (84.88)
46/15/2022$412,0001,391$2960.21 mi81.4includedrank 4 of 12 by similarity score (81.42)
510/4/2024$700,0002,893$2420.06 mi64.3includedrank 5 of 12 by similarity score (64.28)
64/1/2025$455,0001,931$2360.07 mi63.9includedrank 6 of 12 by similarity score (63.88)

sold-comp evidence is limited to the county assessment study window 2022-01-01 to 2025-04-01 — sales after the cutoff do not exist in this source

Keystone platform County demo evidence set, regenerated offline from retained public-record fixtures and anonymized for this site. Recorded 2026-09-01. Arms-length means the county validity code is X; anything else is treated as not arms-length. Fewer than 3 accepted arms-length comps means no dollar output.

How the offer range is computed

  1. Comparables come from the county assessor’s sold-sales dataset for the subject’s area, limited to the assessor’s study window; sales after the cutoff don’t exist in that source, so the report says so.
  2. Arms-length is closed-world: only the county validity code X counts. Rows are validated against a closed schema; missing price, unparseable date, person keys or duplicates are rejected with a fixed reason. The subject parcel’s own sale is excluded from selection.
  3. Survivors are scored for similarity (distance, recency, living area, age, style, exterior, basement, rooms, county-designated bonus); the top six are selected and every candidate keeps its reason.
  4. The point estimate is the score-weighted median $/sq ft times living area; the band is the weighted P25–P75 widened by dispersion. Confidence is a tier from five printed factors. Fewer than three accepted comps means no dollar figure.

Reliability and controls

  • Offline by default. Tests load no environment file, deselect anything marked live and fence sockets, DNS and Firestore. One verification gate runs Ruff, mypy, pytest, Vitest and the console build without network access; dependencies install from hash-pinned locks.
  • Budgets, stated honestly. County fetches are capped per lookup; Firestore listings stop at 500 rows and report truncation instead of pretending completeness.
  • Keyless identity between clouds. The backend is closed to the public internet. The console reaches it through a per-request Vercel OIDC token exchanged for a short-lived Google identity, scoped to one service and one environment. No long-lived keys.
  • Tenant isolation, staged. The PostgreSQL research schema carries row-level security on every tenant table, enforced with FORCE and proven at synthetic 100× scale with a concurrency corpus. It is selectable but default-off; Firestore remains the runtime store.
  • Human review before anything writes. Agents and models propose. Candidate publications require human approval; nothing model-generated mutates a record or reaches a public surface.

Decisions and tradeoffs

One platform, many tenants, from the first line.
Tenant scoping, per-tenant buy boxes and scoped API keys cost time when there was one client. They are why a second investor is configuration, not a rewrite.
Models extract; rules decide.
Scoring, the offer range and the maximum allowable offer are readable rules, so the operator can argue with a number line by line. Models handle text and photos only.
No dollars below three comps.
A thin estimate is worse than none for offer decisions. Some real properties show ‘insufficient’ until more evidence is captured.
Attended capture instead of scraping the county.
The county walls its sold-comp view to runtime HTTP. A person captures rows in the browser and the code validates them: slower per lookup, no brittle scraper, honest fail-closed states.
Closed schemas that forbid person keys.
Redaction by omission can’t leak. New fields need a deliberate contract change, which is the point.
Firestore stays default; PostgreSQL arrives behind a flag.
Rollback after a cutover isn’t lossless, so the relational store was built, migrated and proven offline first, with production provisioning kept as a separate approval.
A research desk, not an engineering queue.
The first console was a workbench of queues and states that made sense to me and not to the operator. Rebuilding it around the subject property produced a tool someone actually opens.

What I’d change next

  • Fold the photo-to-brief flow into the console so a walked property comes back as a brief in the same place the offer range lives.
  • Replace the legacy CRM with the Keystone-owned pipeline so follow-up and buyer matching share one record.
  • Build an evaluation set for the extraction and vision lanes so model output quality is measured, not assumed.
  • Close the two recorded persistence findings and resolve canonical identity, which is documented as an open question rather than solved.

Let’s talk.

Have a project or a question? I’d like to hear it.