Skip to content

Market snapshot

The simplest possible Estaite example. One tool call — get_estaite_cbsa_overview — returns the average rent, vacancy, year-over-year growth, and the list of submarkets inside a metro area.

The code

Save as market-snapshot.py:

"""
Market snapshot — give it a city, get a rental market summary.
Setup:
pip install mcp
export ESTAITE_API_KEY="mk_..."
Usage:
python market-snapshot.py --city "Austin"
"""
import argparse
import asyncio
import json
import os
import sys
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
MCP_URL = "https://mcp.estaite.com"
async def fetch_snapshot(api_key: str, city: str) -> dict:
headers = {"x-api-key": api_key}
async with streamablehttp_client(MCP_URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(
"get_estaite_cbsa_overview",
{"cbsa": city},
)
if not result.content:
raise RuntimeError("Empty response from Estaite")
return json.loads(result.content[0].text)
def print_snapshot(city: str, data: dict) -> None:
print()
cbsa_name = data.get("cbsa_name") or city
state = data.get("state") or ""
as_of = data.get("as_of") or ""
print(f" ── {cbsa_name}{', ' + state if state else ''} rental market ──")
if as_of:
print(f" as of {as_of}")
print()
if data.get("error"):
print(f" No data: {data['error']}")
return
summary = data.get("cbsa_summary") or {}
apts_2br = (summary.get("segments") or {}).get("apartments", {}).get("br2", {})
avg_rent = apts_2br.get("avg_rent")
avg_yoy = apts_2br.get("avg_yoy")
avg_vac = summary.get("avg_vacancy_rate")
avg_inc = summary.get("avg_household_income")
n_subs = data.get("submarket_count", len(data.get("submarkets", [])))
# YoY and vacancy come back as whole-percent (e.g. 3.4 means 3.4%), not decimals.
if avg_rent is not None: print(f" Avg apt 2BR rent: ${avg_rent:,.0f}/mo")
if avg_yoy is not None: print(f" Avg apt 2BR YoY: {avg_yoy:+.1f}%")
if avg_vac is not None: print(f" Avg vacancy: {avg_vac:.1f}%")
if avg_inc is not None: print(f" Avg household income: ${avg_inc:,.0f}")
print(f" Submarkets covered: {n_subs}")
print()
# Top 5 submarkets by 2BR apartment rent
subs = data.get("submarkets", [])
if subs:
def br2_rent(s):
return ((s.get("segments") or {}).get("apartments") or {}).get("br2", {}).get("median_rent") or 0
top = sorted(subs, key=br2_rent, reverse=True)[:5]
print(" Top 5 submarkets by apt 2BR median rent:")
for s in top:
name = (s.get("name") or "")[:34]
rent = br2_rent(s)
if rent:
print(f" {name:35s} ${rent:,.0f}/mo")
print()
if data.get("attribution"):
print(f" {data['attribution']}")
print()
def main():
parser = argparse.ArgumentParser(description="Rental market snapshot for a US city/metro")
parser.add_argument("--city", required=True, help='City or metro name (e.g. "Austin")')
args = parser.parse_args()
api_key = os.environ.get("ESTAITE_API_KEY")
if not api_key:
print("ERROR: ESTAITE_API_KEY env var not set.")
sys.exit(1)
print(f"\nFetching market snapshot for {args.city}...")
data = asyncio.run(fetch_snapshot(api_key, args.city))
print_snapshot(args.city, data)
if __name__ == "__main__":
main()

Run it

Terminal window
python market-snapshot.py --city "Austin"

Sample output

Fetching market snapshot for Austin...
── Austin-Round Rock-San Marcos, TX rental market ──
as of 202603
Avg apt 2BR rent: $1,672/mo
Avg apt 2BR YoY: -4.3%
Avg vacancy: 3.8%
Avg household income: $116,174
Submarkets covered: 32
Top 5 submarkets by apt 2BR median rent:
Tarrytown / Clarksville $2,465/mo
East Cesar Chavez $2,438/mo
Dripping Springs $1,956/mo
Lakeway / Bee Cave $1,894/mo
Del Valle / Easton Park $1,872/mo
Data provided by Estaite Solutions (estaite.com)

(Numbers shift each month as the data refreshes — yours may differ.)

What’s happening

  1. session.initialize() — MCP protocol handshake. Every session starts with this.
  2. session.call_tool("get_estaite_cbsa_overview", {"cbsa": city}) — single call to fetch the entire metro view, including submarkets.
  3. The tool returns a JSON string in result.content[0].text; we parse it and pretty-print the parts we care about.

A note on units

Estaite returns percentages as whole-percent values (e.g. 3.4 means 3.4%, not 0.034). Don’t multiply by 100 before formatting — you’ll get absurd numbers like “340%”. This applies to avg_yoy, avg_vacancy_rate, and any other field with _rate, _yoy, or _change in the name.

Next steps

  • Switch metros without restarting the session. Wrap the call in a loop and pass different cbsa values inside the same with block — saves the handshake overhead.
  • Drill into a single submarket. Take an id from data["submarkets"] and feed it to query_estaite_submarket_index for full per-segment metrics.
  • Compare two metros side by side. Run two get_estaite_cbsa_overview calls and diff the cbsa_summary blocks.