All endpoints are accessed through the Caddy reverse proxy. In dev: http://localhost.
- MCP server:
http://localhost/api/... - Agent runner:
http://localhost/agent/...
For direct access (bypassing Caddy): mcp-server at :8001, agent-runner at :8000.
Health check. Always public.
Response 200:
{"status": "ok", "version": "0.1.0", "service": "mcp-server"}Dashboard KPIs, news, and mortgage rate data for a market.
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
market |
string | "National" |
MSA name or "National" |
Rate limit: 60/min (no auth required)
Response 200:
{
"kpis": {
"avg_list_price": "$425,000",
"active_listings": "1,234",
"rate_30yr_fixed": "6.82%",
"median_dom": "28 days",
"price_per_sqft": "$210",
"new_listings_30d": "156",
"price_reductions": "11%",
"months_supply": "2.4 mo"
},
"recent_news": [
{
"headline": "Housing Inventory Rises Nationally",
"summary": "Active listings up 8% month-over-month.",
"source": "HMI Engine",
"relevance_score": "high"
}
],
"market_snapshot": {
"total_listings": 1234,
"average_median_price": 425000,
"latest_mortgage_rate": 6.82,
"as_of_date": "2026-04-17"
},
"mortgage_rates": [
{"term_years": 30, "rate": 6.82, "rate_type": "fixed"},
{"term_years": 15, "rate": 6.12, "rate_type": "fixed"}
]
}KPI data source priority:
- Latest
KPISnapshotrow for the market (pipeline push) - Computed on-the-fly from
HouseListing+NeighborhoodTrendtables
Monthly time-series for all markets in one request, keyed by market name. Used by the Trends tab to render the full MSA distribution (grey mass).
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
years |
int | 5 |
Number of years of history |
Rate limit: 10/min
Response 200: Object keyed by market name, each value is an array sorted by month ascending. Same snapshot shape as GET /history/{market}.
{
"National": [...],
"Austin": [...],
"Denver": [...]
}Monthly time-series for a single market (used by Trends and Yearly Comparison tabs).
Path params:
| Param | Type | Description |
|---|---|---|
market |
string | MSA name or "National" |
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
years |
int | 5 |
Number of years of history |
Response 200: Array sorted by month ascending.
[
{
"market": "Austin",
"month": "2021-04-01",
"median_dom": 14,
"months_supply": 0.8,
"mortgage_rate_30yr": 3.12,
"price_per_sqft": 248,
"active_listings": 1200,
"median_sale_price": 385000,
"sales_volume": 890,
"new_listings": 950,
"yoy_active_listings": 12.4,
"yoy_median_sale_price": 28.6,
"yoy_sales_volume": -3.1,
"yoy_new_listings": 8.9
}
]YoY fields (yoy_*) are computed server-side: (current - prior_year) / prior_year * 100. Returns null if no prior-year row exists.
Latest snapshot value for all markets, sorted by metric (used by Rankings tab).
Query params:
| Param | Type | Default | Description |
|---|---|---|---|
metric |
string | median_sale_price |
One of 8 KPI fields (see below) |
sort |
string | desc |
asc or desc |
limit |
int | 100 |
Max markets to return |
Valid metric values:
- YoY metrics (returns % change):
active_listings,median_sale_price,sales_volume,new_listings - Absolute metrics (returns raw value):
median_dom,months_supply,mortgage_rate_30yr,price_per_sqft
Response 200:
[
{
"market": "Austin",
"value": 28.6,
"rank": 1
}
]For YoY metrics, value is the YoY % change. For absolute metrics, value is the raw figure.
Natural language query over housing data using Haiku tool-use.
Auth: JWT required (rate: 10/min); anon allowed (rate: 3/min).
Request body:
{"query": "What is the average price per sqft in Austin vs Denver?"}Constraints:
querymax 500 chars- Rejected (400) if query contains SQL keywords:
DROP,DELETE,INSERT,UPDATE,EXEC
Response 200:
{
"answer": "Austin averages $248/sqft and Denver averages $312/sqft based on current listings data.",
"tools_used": ["search_houses", "get_housing_market_snapshot"]
}Error responses:
400— query too long or contains rejected keywords408— query timed out (15s limit)429— rate limit exceeded
Internal behavior: Haiku tool-use loop, max 3 rounds, read-only tool whitelist (search_houses, get_valuation_data, get_neighborhood_snapshot, get_mortgage_rates, get_housing_market_snapshot). Final synthesis max_tokens=300.
Direct tool call. Protected (JWT required). Rate: 20/min.
Path params:
| Param | Description |
|---|---|
tool_name |
One of the 6 MCP tools (see below) |
Request body: Tool-specific parameters (see tool schemas below).
Response 200: Tool result object.
Available tools:
| Tool | Key Parameters |
|---|---|
search_houses |
query, city, state, zip_code, min_price, max_price, property_type, limit, offset |
get_valuation_data |
zip_code |
get_neighborhood_snapshot |
zip_code |
get_mortgage_rates |
(none) |
get_housing_market_snapshot |
city_filter |
calculate_roi |
purchase_price, monthly_rent, property_tax_annual, insurance_annual, maintenance_annual |
Push KPI snapshot for a market. Requires API key (X-API-Key header).
Request body:
{
"market": "National",
"kpis": {
"avg_list_price": "$425,000",
"active_listings": "1,234",
"rate_30yr_fixed": "6.82%",
"median_dom": "28 days",
"price_per_sqft": "$210",
"new_listings_30d": "156",
"price_reductions": "11%",
"months_supply": "2.4 mo"
},
"as_of_date": "2026-04-17"
}Response 200:
{"status": "ok", "market": "National", "as_of_date": "2026-04-17"}Prometheus metrics. Public.
Returns text/plain Prometheus exposition format. Key metrics:
http_requests_total{method, endpoint, status_code}http_request_duration_seconds{method, endpoint}hmi_feed_fetch_total{feed, status}hmi_research_total{status}hmi_hitl_approval_total{outcome}
Response 200:
{"status": "ok", "version": "0.1.0"}Start a research run. Returns immediately with a plan for approval.
Request body:
{"query": "housing market in Austin Texas"}Constraints: query min 3 chars, max 500 chars.
Response 200:
{
"run_id": "a3f7c1b2-...",
"status": "awaiting_approval",
"plan": [
"Collect housing listings, market data, and mortgage rates for Austin",
"Analyze price trends, inventory levels, and investment metrics",
"Research recent Austin housing market news"
]
}What happens internally:
graph.ainvoke(initial_state, {thread_id: run_id})- Supervisor decomposes query, writes
research_planto state, returns - Run registered in
_runsdict with statusawaiting_approval
Poll run status.
Path params: run_id — UUID from POST /research
Response 200:
{
"run_id": "a3f7c1b2-...",
"status": "complete",
"plan": ["..."],
"result": {
"report": {
"report_markdown": "# Austin Housing Market Report\n\n## Executive Summary\n..."
},
"dashboard": {
"kpis": {
"national": {
"mortgage_rate": 6.82,
"avg_median_price": 425000,
"market_status": "Low Inventory"
},
"metro_specific": {
"sample_size": 47,
"avg_price": 512000,
"estimated_roi": 4.2
}
}
},
"messages": [...]
},
"error": null
}Status values:
| Status | Description |
|---|---|
awaiting_approval |
Plan created, waiting for human approval |
running |
Pipeline executing in background |
complete |
Pipeline finished; result populated |
rejected |
User rejected the plan |
error |
Pipeline failed; error populated |
Run TTL: Entries are deleted from the registry 60 minutes after reaching a terminal status.
Approve or reject a research plan.
Path params: run_id
Request body:
{"approved": true}or
{"approved": false}Response 200 (approved):
{"run_id": "a3f7c1b2-...", "status": "running"}Response 200 (rejected):
{"run_id": "a3f7c1b2-...", "status": "rejected"}Error responses:
404— run_id not found (may have expired)409— run is not inawaiting_approvalstatus
What happens on approval: asyncio.create_task(_run_to_completion(run_id, config)) is called. The graph resumes from the MemorySaver checkpoint using the same thread_id, supervisor sees is_approved=True and routes to agents.
All endpoints return errors in FastAPI default format:
{
"detail": "Run 'abc123' not found"
}Rate limit errors (from slowapi):
{
"error": "Rate limit exceeded: 10 per 1 minute"
}