RealtyPad — research (evidence only)¶
Hard rules¶
- Never invent numbers. Prefer null over fiction. Mark estimates in comments /
raw(or structured tabs). - Trust but verify ask/list price. Stored money fields and prior automation output are not ground truth. On revisit, compare live portal ask → stored
ask_price→ patch when different (price cuts, re-lists, bid moves) and note the change; also refresh listing status when the portal shows it. - Read comments + open
action_neededhandoffs + comps before re-fetching; reuse prior sources. Do not treat observations as a second comment thread. - Do not set triage verdict (
ranked/passed) or aggressiveness judgment — that isget_agent_manual(workflow=underwrite)/get_agent_manual(workflow=triage). - Scenario matrix / select / apply belongs to
get_agent_manual(workflow=scenarios)(underwrite calls it). - Patching money fields via
update_dealauto-rescores. That is fine for clearing estimates. Never setstatus/ Status note (rationale) from research — useupdate_deal_statusonly in triage/UW. Money-onlyupdate_dealsfor multi-deal fills. - Prefer Firecrawl (or the user’s listing/MLS MCP) for web pages. Do not pull from ToS-hostile sites without approval.
Owns vs does not¶
| Owns | Does not own |
|---|---|
| Tax, HOA, insurance, rent / ADR+occ / ARV+repairs | Status rubric / aggressiveness |
listing_description, lat/lng, photos, amenities, listing URLs (deal_listings) |
Scenario matrix / runs (get_agent_manual(workflow=scenarios)) |
Listing history / Time on market (list_deal_listing_events, add_deal_listing_events) |
Scoring / buyer match (TOM is research signal only) |
Research comments, action_needed handoffs, structured comps (add_deal_comp), portal source URLs |
Pattern-pass clusters (triage) |
| Gap list for underwrite | Bulk market ingest (get_agent_manual(workflow=ingest)) |
Triggering get_agent_manual(workflow=trends) when market cache is stale |
Owning snapshot upsert playbooks (trends workflow) |
Triggering get_agent_manual(workflow=growth-drivers) for qualitative demand signals |
Owning Drivers grading playbook |
Pointing CapEx work to get_agent_manual(workflow=cost-estimate) |
Owning HD Apify BOM runs (cost-estimate workflow) |
Checklist¶
Research Progress:
- [ ] 1. Load deal (`get_deal` **core**) + `list_comments` / handoffs / `list_deal_comps` + read ``data_gaps`` (any blocking=true → fill before UW)
- [ ] 2. Note strategy money gaps from ``strategy_intent`` when set (else ``income_strategy``): ltr rent | str ADR+occ | flip/wholesale/redevelop ARV (exit/land) — intent can emit multiple (e.g. rent + adr)
- [ ] 3. Market trends: call **`ensure_geo_markets`** when geos missing (pass neighborhood when known), then read `markets[].latest_by_source`; gap-fill with **`refresh_deal_trends(gap_fill=true)`** or surgical refresh when stale/missing to brief (ZHVI/ZORI/AirROI chips + `period_count`). Do **not** tenant-upsert catalog CSVs; do **not** use `refresh_deal_trends` force or LLM-parse ZHVI/ZORI. Power tools (`refresh_catalog_trends` / `refresh_airroi_trends`) only when surgical.
- [ ] 3b. Growth drivers (**required** for import/research ready package): call **`get_agent_manual(workflow=growth-drivers)`** — search local news/planning/employers; log ≥1 active driver via `add_deal_growth_factor(s)` (strength×direction + source). Soft gap `missing_growth_drivers` until cleared. Do not confuse with Trends YoY Growth. Do not claim research complete without active drivers.
- [ ] 4. Verify ask / purchase / bid from source_url (label which): compare portal price vs stored `ask_price` / `purchase_price`; patch on price cut or other change; refresh listing status when shown
- [ ] 4b. Listing history / Time on market: scrape portal price history (Zillow `priceHistory` from detail Actor / Firecrawl) → persist with **`add_deal_listing_events`** (or `add_deal_listing_event`). Prefer structured events over freeform notes. Platform derives Time on market / True time on market — do not patch those scalars directly. Hosted tab **Research listing history** runs a refresh automation (`kind=listing_history`).
- [ ] 5. listing_description if missing — detail page / MLS / Firecrawl (never invent). Ingest should already have copied the write-up; if the deal is still a search card, run the same detail pull as ingest.
- [ ] 5b. Amenities when the listing shows them (`update_deal_property` amenities replace-array — never invent). Attach every portal detail URL found via `add_deal_listing` / ingest `listing_urls` (not only primary `source_url`).
- [ ] 5c. Property **attributes** (Interior/Exterior/parking/subdivision/`mls_id`/etc. from portal detail grids — HAR, Zillow, …): patch `update_deal_property` `attributes` JSONB (top-level shallow merge; null deletes a key). Soft convention keys in the glossary. Hosted listing refresh automations fill attributes the same way. **Do not** build portal feature adapters — research the detail page and patch. HOA/maintenance fee → `update_deal` `hoa_monthly` (or refresh `hoa_monthly` field) — never leave HOA inside attributes.
- [ ] 6. lat/lng (portal/MLS coords preferred; else Nominatim backfill)
- [ ] 7. tax_annual from **assessor history**, not listing quote alone. Prefer `listingTaxHistory` / `_details.taxHistory` already on the Zillow detail payload (persist via `add_deal_appraisal_tax_history`, `sync_deal_tax=true` for the current UW year). County CAD sites often block automation (`robots.txt`) — do not scrape them. If ingest already stored history, reuse it.
- [ ] 8. HOA if condo/HOA community
- [ ] 9. Income inputs: rent comps | ADR+occ | ARV+repairs (null if unknown) — store comps via add_deal_comp; for CapEx use **`get_agent_manual(workflow=cost-estimate)`** (HD materials + labor ranges)
- [ ] 10. insurance_annual if a real quote exists; else leave profile/STR default
- [ ] 11. Photos: full gallery vs listing (not “any photo exists”) — see Photos below
- [ ] 12. Patch **money only** via `update_deal` / money-only `update_deals` (never status/rationale); property beds/baths/sqft/year_built/lot/property_type/amenities/`attributes`/address via `update_deal_property`; comps + research comment with sources; `action_needed` handoffs only for next-agent todos
- [ ] 13. Brief: what filled, what still missing, market trend freshness, growth drivers logged (no ranked/passed)
MCP¶
RealtyPad MCP tools (same API as the hosted app):
| Step | Tool |
|---|---|
| Load | get_deal (core), list_comments, list_deal_comps |
| Market cache | via get_agent_manual(workflow=trends) (ensure_geo_markets → scan/gap_fill via refresh_deal_trends; surgical: refresh_catalog_trends / refresh_airroi_trends; tenant upsert only for custom overlays) |
| Patch | update_deal / update_deals (money + listing_description only — no status/rationale; prefer bulk for multi-deal fills); update_deal_property (beds/baths/sqft/year_built/lot_size_acres/property_type/amenities/attributes/address — not on update_deal) |
| Tab research (UI) | Deal detail tabs enqueue deal_tab_refresh via POST /api/automation-runs with deal_id + kind (goal=refresh). Full-deal Research / Underwrite use the same endpoint with goal=research|underwrite (MCP create_automation_run). Sync REST remains for listing/trends/appraisal (…/listing/refresh, …/trends/ensure, …/appraisal-tax/refresh-from-listing). Listing history uses a refresh automation (kind=listing_history) — not a sync refresh shortcut. Listing research must capture listing agent (link_deal_contact role=listing_agent or listing_agent on add_manual_lead) when the portal shows attribution — clears soft gap missing_listing_contact. Planner goals: read_doc("agents/automation-planner.md"). |
Expenses / comps tab research kinds (not Quick Analyze):
| UI | kind |
Playbook |
|---|---|---|
| Find comps | comps |
research |
| Research listing history | listing_history |
research |
| Research growth drivers | growth_drivers |
growth-drivers |
| Research fix-up costs | capex |
cost-estimate |
| Research monthly costs | opex |
opex |
| Contacts | list_deal_contacts / link_deal_contact / list_contacts (q matches name and phone/email) / get_contact / merge_contacts / delete_contact — listing agent or seller (FSBO). Prefer find-by-channel then contact_id; create+link with phone/email auto-reuses. |
|
| Comps | list_deal_comps / add_deal_comps (bulk) / add_deal_comp / update_deal_comp / delete_deal_comp (kind: rent | sale | str). Connectors: airroi_listings_comparables (STR), Zillow PPE (rent/sale), Firecrawl/web search — catalog: get_agent_manual(workflow=data-sources). |
|
| Notes | add_comment (research brief / findings); add_observation only kind=action_needed for next-agent todos |
|
| Photos | add_deal_attachment_urls (gallery bulk) / add_deal_attachment_url / delete_deal_attachment |
Comps: There is no deal-scoped find+persist shortcut. Discover with web search/browse first, then allowlisted connectors, then persist via add_deal_comps / add_deal_comp. Hosted Quick Analyze / Comps-tab Research comps run a hosted automation that finds and saves comps — that path is intentional; do not recreate a silent find tool for chat agents.
Strategy scope (cost-critical): Only search comps kinds implied by strategy_intent when set, else active income_strategy (owner-occ primary → rent+STR+sale). Map: LTR → rent; STR → str; flip/wholesale/redevelop → sale. Empty investment focus defaults to rent only — never “search everything.” Land/tear-down theses: get_agent_manual(workflow=redevelopment) — when qa_flags.teardown_signals is non-empty, the condition decides the strategies (redevelop only; no rent / flip ARV just to clear gaps).
| Kind | Tool | When |
|---|---|---|
rent |
Zillow PPE / Firecrawl — zillow.md | LTR (or primary) |
sale |
Zillow PPE / Firecrawl — zillow.md | flip / wholesale / redevelop (or primary) |
str |
airroi_listings_comparables only (paid); optional VRBO / AirDNA |
STR in focus — never call AirROI for LTR-only / flip-only / wholesale-only runs |
Always persist image_url (listing hero) from connector JSON when present — UI thumbnails depend on it. Automation apply also backfills heroes from tool payloads when the model omits them.
- Discover — Firecrawl / session web search for the in-scope kinds only. Prefer multiple portals over one source.
- Fill gaps —
airroi_listings_comparables(…)only if STR is in scope (needsAIRROI_API_KEY; returns candidates only); Zillow PPE for rent/sale near the subject when those kinds are in scope (needsAPIFY_TOKEN+ lat/lng). Cap spend. Multifamily bed/size rules: zillow.md. - Persist —
add_deal_compswithsource+source_url(upserts by kind+url).price= monthly rent (rent), list/sale $ (sale), or ADR nightly (str). Include address, beds/baths/sqft,image_urlwhen known. Setproperty_type(single_family, condo, townhouse, duplex, triplex, multi_family, or the portal home type) whenever the listing states it — the comp screen drops a condo/unit against a house only when that type is saved. A unit marker in the address (#A,Unit B,Apt) is stored as an apartment when you omit the type. Never invent. - Patch UW money only after evidence — do not auto-set rent/ADR/ARV from comps medians alone. For ARV on flip / wholesale, ARV is an after-repair price: derive it from sale comps only — median sale $/sqft × subject sqft → soft-filtered sale comps (beds±1 / distance) → raw sale-price median last resort — and only with ≥3 sale comps (prefer renovated / turnkey closed sales). The county appraisal / assessed value is an as-is value, never the ARV — keep it as a cross-check only. For redevelop (without flip/wholesale in focus), prefer lot/tear-down/as-is comps and treat the latest appraisal as exit/land value — it leads the method order there (
redevelopment_as_isresidual allowlisted). The hosted fill follows the same rules and re-derives an ARV it set itself when comps change. When methods disagree by ≥5%, leavearvnull (hosted fill recordsraw.automation_uw.arv_skipwith a code-inferred residual_reason — agents cannot set it via MCP). After one unexplained skip, stop retrying. Callestimate_arv(deal_id)(read-only) to see every method's value, which one the fill would pick, and why it would skip — then patcharvyourself only if you agree. Comp quality: ARV and rent fills (and their validators) use only comps that pass the comp screen — dropped forproperty_type_mismatch,size_mismatch(outside 0.65–1.5× subject sqft),beds_mismatch(±2+),too_far(>10 mi),stale(sold/listed >12 mo),future_date(as_of after today), orprice_outlier($/sqft outside 0.6–1.67× the kept median, ≥4 comps). Checks only fire when both sides carry the fact. See drops viascreen_deal_comps(deal_id, kind)orraw.automation_uw.comp_screen; when you have looked at a comp and disagree, pin it withupdate_deal_comp(quality_override=keep|exclude|clear). Replace dropped comps with better ones rather than overriding by default. Thin/no comps after one reasonable attempt → switch method ($/sqft, specialty); do not widen forever; do not “try STR next” unless STR is instrategy_intent. - Self-check with the automation's own tools (all read-only):
preview_uw_fill(deal_id)shows what the hosted fill would set on null rent / ADR+occ / ARV / vacancy / insurance / HOA and from what source;check_uw_values(deal_id, estimated_rent=…, arv=…)runs the checks that make automation clear a value (outside_basis_band,copied_single_comp,unadjusted_size_bias,unexplained_method_residual,disagrees_with_priority_method, …) — check beforeupdate_deal;get_deal_qa_flags(deal_id)returns the critic's QA flags (distress terms, repairs disclosed but no fix-up costs, foreign listing URLs, thin comps, amenity contradictions, money keys on attributes). Clear what they flag before calling research done. - Market-level STR series still go through
get_agent_manual(workflow=trends)/refresh_airroi_trends(catalog trends ≠ paid listings comps).
Expenses mental model (CapEx vs OpEx)¶
| Bucket | Persist how | Feeds | Never |
|---|---|---|---|
| Acquisition CapEx | add_deal_repair_item(s) via get_agent_manual(workflow=cost-estimate) (scalar only if no items) |
repair_estimate → cash_in; sets needs_rehab |
Invent into OpEx / bump maintenance_pct |
| Recurring OpEx | add_deal_opex_item(s) via get_agent_manual(workflow=opex) |
monthly_opex_total → monthly CF |
Day-0 rehab BOM |
Never use maintenance_pct / insurance_annual as a blended CapEx or OpEx proxy. Do not add a line that duplicates the hard-coded STR 20% platform fee. Soft gap: needs_rehab + null estimate → repairs_unknown — flag it; do not invent OpEx. CapEx is not subtracted from monthly LTR/STR CF, but is in cash_in (down + repairs) when set. When repair items exist, they own repair_estimate (scalar patches ignored).
Money fields: ask_price, purchase_price, estimated_rent, adr_nightly, occupancy_pct, arv, repair_estimate, needs_rehab, income_verified (STR ADR/occ confirmed — never from comps medians alone), tax_annual, insurance_annual, hoa_monthly, vacancy_pct, maintenance_pct, hold_months.
For AS-IS / incomplete reno listings: set needs_rehab=true (do not invent CapEx). When needs_rehab and repair_estimate is null, scoring sets raw.cashflow.repairs_unknown + low confidence and the deal appears in Soft gaps (has_soft_gap) — same soft-bar as rent capped.
What “verified” means¶
| Field | Acceptable sources |
|---|---|
| Tax | Assessor history first: Zillow detail listingTaxHistory / _details.taxHistory (already fetched at ingest — zillow.md), HUD/Auction docs, or county CAD when the site allows. Listing-quoted tax is not final — it can be ±20–35% off (homestead cap, exemptions). Store multi-year via add_deal_appraisal_tax_history (bulk) or add_deal_appraisal_tax; set UW with sync_deal_tax_as_of / sync_deal_tax=true — the platform derives post-sale tax (ask/purchase × median effective rate from history) and keeps it estimated. Never copy seller taxPaid with exemptions as verified. If the latest year jumps >15%, flag likely homestead-cap removal. County CAD (traviscad.org, etc.) often ROBOTS_DISALLOWED — respect robots.txt; do not work around. |
| HOA | Listing, HOA docs, builder page, MLS |
| Rent | Listed rent, lease, comps; Zestimate = estimate |
| ADR / occ | Airbnb/VRBO comps, VRBO geo, AirDNA on known Airbnb URLs, market reports — never invent |
| ARV | Flip/wholesale: ≥3 sale comps — $/sqft×subject, then filtered sale comps; raw median last resort; appraisal is as-is, cross-check only. Screened comps only — check estimate_arv / screen_deal_comps. Redevelop: appraisal (exit/land value) first. Mark estimate; ≥5% method residual → explain or leave null |
| Insurance | Quote or leave profile/STR default (flag estimated) |
| Purchase | Ask, starting bid, or explicit offer — label which |
| Listing description | Portal / MLS remarks — never invent |
| Lat / lng | Portal/MLS coords preferred; Nominatim fallback; never invent |
| Photos | Full listing gallery when the portal shows N images — not hero-only |
Photos (gallery completeness)¶
Check data_gaps on get_deal (core) / list_deals first — { code: "photos", blocking: true } when photo_count==0 and status != new. Batch: list_deals(has_gap="photos") or has_blocking_gap=true. Also list_deal_attachments → count kind=image vs the listing’s photo count (“See all N photos”, Auction.com “N Photos”, MLS media count).
Quick Analyze / auto-import / gallery refresh: call the detail Actor then get-dataset-items (not Firecrawl alone) and persist the full URL list (cap ~40) with add_deal_attachment_urls. A hero or 4–5 mini-card sample is not a gallery. Prefer portal CDN / Actor gallery arrays; Firecrawl is fallback only when the dataset has no gallery. goal=refresh (deal-tab Photos / Listing) always re-fetches. After photos are attached, hosted automation runs a vision keep/drop pass (subject interiors/exteriors kept; faces, other homes, maps, logos detached).
| Situation | Action |
|---|---|
Blocking photos gap / zero images |
Attach full gallery from Firecrawl/source_url or connector detail payload — do this, do not only leave a “missing gallery” note |
| Some images but fewer than listing N (within cap ~40) | Backfill the missing ones — do not stop because a primary already exists |
Connector search-only had 1 imgSrc / empty allPropertyPhotos |
Re-pull Zillow detail with addresses (zillow.md); Firecrawl carousel URLs → prefer -p_f.jpg hashes; Auction.com/imgix gallery |
Rules:
- First image
is_primary=trueif none yet; otherwise keep existing primary. - Skip map/satellite Google URLs, logos, and exact
source_urldupes (same CDN path or same photo hash). - Note in research comment if gallery still short of listing N after best effort.
- To-dos for the next agent use observation
kind=action_needed(handoffs only — not a research diary). “Missing gallery…” as a passive finding is wrong when photos are still attachable. - Any
data_gapsentry withblocking: truemeans do not advance to score/UW until cleared.
Ingest owns first-pass galleries (get_agent_manual(workflow=ingest)); research owns the gap-fill when triage/UW hits a thin gallery / blocking photos gap.
Travis protest band (user-owned nearby): listed tax is not permanent; note ~$700–$1,200/yr when relevant.
Connector preference (research fetch)¶
Use whatever the session has, in order:
- Deal
source_urlvia Firecrawl (or user listing MCP) - County / assessor / HOA public pages
- User MLS MCP when connected and the listing is MLS-backed
- Allowlisted detail Actor only when cheaper portals fail, spend is approved — recipes in
get_agent_manual(workflow=data-sources)(Zillow:addresses, not bare zpids / notsearchResultsDatasetId). Do not useapify/rag-web-browseror other non-allowlisted Store Actors.
Do not require paid connectors for research (except known-URL AirDNA enrich when you already have Airbnb room URLs and need listing-level ADR/occ).
Comps (multi-source) + optional STR enrich¶
Nearby comps (deal needs lat/lng for geo tools):
- Web search / Firecrawl for rent, sold, and STR listings (multi-portal when possible)
airroi_listings_comparablesfor STR candidates near the subject → review →add_deal_comps(kind=str, source=airroi)- Zillow PPE for rent/sale → map →
add_deal_comps(source=zillow) — zillow.md - Optional STR geo: VRBO (comps only — not sale ingest). Cap
maxTotalChargeUsd. - Review comps; then
update_dealwith rent / ADR+occ / ARV only after evidence (do not auto-patch from comps). Thin/no comps after one reasonable attempt is a market limit — switch valuation method; do not keep re-querying LTR. - Market-level STR series still go through
get_agent_manual(workflow=trends)/refresh_airroi_trends(deal_id=…)/ catalogairroi
Known Airbnb URL enrich (optional): airdna.md. Prefer AirROI / VRBO / web STR comps as market finders. Booking.com is not allowlisted. Never invent ADR/occ.
Comment template (research-only)¶
## Research — YYYY-MM-DD
**Strategy:** ltr | str | fix_flip | wholesale
**Purchase assumption:** $X (ask | bid | offer)
**Tax:** $Y/yr (source) | still missing
**HOA:** $Z/mo or none | still missing
**Income:** rent $R / ADR $A @ occ O% / ARV $V (source; estimate?)
**Market:** source / as_of / median or ADR (fresh | refreshed | still stale)
**Still missing:** …
**Sources:** URL1, URL2
Brief the user¶
- What was filled vs still null / estimated.
- Market trend freshness (and whether trends workflow ran).
- One next step: underwrite, triage, or more research — do not declare ranked/passed unless the user also asked for underwrite.