Skip to content

Repository files navigation

Beyond Price — Spanish Property Intelligence

License: MIT

What you're actually buying into, not just the listing.

Project status

Built end-to-end — Barcelona, Madrid, and Valencia coverage, city-aware tax/legal logic, an 8-dimension scoring engine, a documented incident-driven review protocol — then open-sourced under MIT once the build was complete, so the engineering could be useful (and visible) beyond a single hosted app. Issues and PRs are welcome.

Read more:

Screenshots

Beyond Price homepage — address input form Beyond Price report — AI verdict, composite score, disclosures

What it does

Enter any address in Barcelona, Madrid, or Valencia and get a report covering what estate agents don't tell you and sellers aren't legally required to disclose:

  • AI verdict — plain-language summary generated by Claude, personalised to your buyer profile
  • Market position — how the asking price sits against active Fotocasa listings in the same district, with comparable table and percentile bar
  • Acquisition cost breakdown — total cash needed including ITP/IVA, notary, registry, gestoria
  • Seller economics — estimated seller costs (agency commission, plusvalía, energy cert) → seller floor price and negotiation headroom
  • Hidden cost breakdown — IBI, community fees, derrama risk, energy upgrade liability
  • Pre-purchase disclosures — 4–6 must-know items before signing (legal, structural, cost, neighbourhood)
  • Tourist apartment pressure — Airbnb density in the building and within 500m
  • School quality composite — type, language of instruction, proximity, Google ratings
  • Noise ecosystem — day / night / weekend scores, floor-adjusted
  • Neighbourhood trajectory — rising, stable, or declining area
  • 8-dimension composite score — Convenience, Safety, Property, Market, Risk, Liveability, Hidden Costs, Intangible
  • Personalised buyer questionnaire — 8 questions adjust scoring weights to your situation
  • Compare mode — AI side-by-side comparison of 2–3 properties
  • Bilingual — full English and Chinese (中文) interface

Why it's different from Idealista / Fotocasa / BBVA Valora

Feature Spanish portals Beyond Price
Acquisition cost calculator (ITP + fees)
Seller floor price & negotiation room
Tourist apartment saturation
School quality (not just proximity)
Floor-adjusted noise analysis
Hidden costs (IBI + community + derrama)
AI plain-language verdict
Multi-property AI comparison
Neighbourhood trajectory
Bilingual EN / 中文

Quick start (local dev)

Prerequisites

  • Python 3.11+
  • Node 20+
  • Docker Desktop (for Redis — optional, degrades gracefully without it)
  • Claude Code installed and authenticated (claude --version)

1. Clone and install

git clone https://github.com/Hao1992/property_analysis.git
cd property_analysis

# Backend
python -m venv .venv && source .venv/bin/activate
pip install -r backend/requirements.txt

# Frontend
cd frontend && npm install

2. Configure environment

cp backend/.env.example backend/.env
# Required: GOOGLE_PLACES_API_KEY (Google Cloud free tier — see .env.example for setup notes)
# AI narrative — pick one:
#   - Local dev: leave ANTHROPIC_API_KEY unset, set USE_CLAUDE_CLI=true (routes through
#     `claude -p`, requires a Claude Code subscription, zero extra API billing)
#   - Server deployment: set ANTHROPIC_API_KEY=sk-ant-... (the claude CLI isn't available
#     on most remote hosts)
# Optional: DATABASE_URL=<postgres-url> (persistent analytics storage; Neon recommended)
# Optional: ANALYTICS_TOKEN=<secret> (enables /admin/analytics dashboard)
# Optional: AREA_VERDA_CACHE_TTL_SECONDS=86400 (refresh official parking map cache daily)

3. Start services

Three terminals:

# Terminal 1 — Redis (optional but enables 24h analysis caching)
docker compose up redis

# Terminal 2 — Backend (from repo root)
source .venv/bin/activate
cd backend && uvicorn main:app --reload --port 8000

# Terminal 3 — Frontend (from repo root)
cd frontend && npm run dev

Open http://localhost:5173.

Why not Docker for everything? The AI narrative uses claude -p (subprocess calling the Claude Code CLI). The claude binary lives on your host machine, not inside Docker. Running the backend locally means it can call the CLI without complex volume mounting.


Architecture

property_analysis/
├── backend/
│   ├── main.py                         # FastAPI app, CORS, router registration
│   ├── api/routes/
│   │   ├── analyze.py                  # POST /analyze  POST /compare
│   │   │                               # GET  /admin/analytics (HTML dashboard)
│   │   │                               # POST /track (frontend events)
│   │   └── health.py                   # GET /health
│   ├── services/                       # External data fetchers (all async)
│   │   ├── geocoder.py                 # Nominatim: address → lat/lng
│   │   ├── overpass.py                 # OSM: POIs, bars, schools, transit
│   │   ├── google_places.py            # OSM baseline ratings (Google fallback)
│   │   ├── catastro.py                 # Catastro: surface, age, energy cert, cadastral value
│   │   ├── ine.py                      # INE: census section (reference data)
│   │   ├── fotocasa_scraper.py         # Fotocasa: active listing comparables by district
│   │   ├── open_data_bcn.py            # BCN open data: district safety, business licences
│   │   ├── airbnb_saturation.py        # Inside Airbnb: tourist apartment density
│   │   ├── school_quality.py           # OSM + ratings: school composite score
│   │   ├── noise_ecosystem.py          # OSM + floor level: noise estimate
│   │   ├── neighbourhood_trajectory.py # BCN licences API: area trend
│   │   ├── ai_narrative.py             # Claude Code CLI: plain-language verdict
│   │   └── disclosures.py              # Rule-based pre-purchase disclosure items
│   ├── scoring/                        # Pure calculation, no I/O
│   │   ├── engine.py                   # 8-dimension composite + penalty multipliers
│   │   ├── convenience.py              # Transit + amenity density + Google quality
│   │   ├── safety.py                   # 4 crime indices → safety score
│   │   ├── property_score.py           # Age + energy cert + unit features
│   │   ├── market.py                   # Price fairness (Fotocasa) + yield + trend
│   │   ├── risk.py                     # Structural + flood + legal + trajectory
│   │   ├── liveability.py              # School + noise + neighbourhood trend
│   │   ├── hidden_costs.py             # IBI + community fees + derrama risk
│   │   ├── transaction_costs.py        # Buyer acquisition costs + seller economics
│   │   └── valuation.py                # INE reference (background signal, not displayed)
│   ├── models/
│   │   ├── request.py                  # AnalyzeRequest (address, price, UserAnswers)
│   │   ├── response.py                 # All Pydantic response models
│   │   └── user_profile.py             # UserAnswers questionnaire + weight computation
│   └── utils/
│       ├── cache.py                    # Redis @cached decorator (graceful fallback)
│       ├── analytics.py                # Event log: analyses + frontend events
│       ├── dashboard.py                # HTML dashboard generator (Chart.js, self-contained)
│       └── rate_limiter.py             # 5 analyses/day/IP limit
├── frontend/src/
│   ├── App.tsx                         # Tab routing: Analyse / Compare
│   ├── pages/
│   │   ├── Home.tsx                    # Address input + buyer questionnaire
│   │   ├── Report.tsx                  # Full analysis report (data-section attributes)
│   │   └── Compare.tsx                 # Side-by-side comparison page
│   ├── components/
│   │   ├── NarrativeCard.tsx           # AI verdict (hero element)
│   │   ├── ScoreCard.tsx               # 8-dimension grid + score adjustments
│   │   ├── ValuationModule.tsx         # Market comparables + acquisition cost + seller economics
│   │   ├── HiddenCostBreakdown.tsx     # Monthly cost waterfall
│   │   ├── DisclosureSection.tsx       # Pre-purchase disclosure cards
│   │   ├── AirbnbSaturation.tsx        # Tourist apartment gauge
│   │   ├── SchoolQualityModule.tsx     # School composite display
│   │   ├── NoiseEcosystem.tsx          # Day/night/weekend bar chart
│   │   ├── NeighbourhoodTrajectory.tsx # Trend arrow + business count
│   │   ├── SafetyModule.tsx            # 4-index horizontal bars
│   │   ├── NeighborhoodModule.tsx      # POI category grid
│   │   ├── PropertyMap.tsx             # Leaflet map with POI pins
│   │   ├── PropertyDetails.tsx         # Cadastral property details
│   │   ├── NegotiationTips.tsx         # Rule-based negotiation cards
│   │   └── WaterfallChart.tsx          # Recharts waterfall (hidden cost detail)
│   ├── hooks/
│   │   └── useAnalytics.ts             # IntersectionObserver section tracking + events
│   ├── contexts/
│   │   └── LanguageContext.tsx         # EN / 中文 language context
│   ├── i18n/strings.ts                 # All UI strings in EN + ZH
│   ├── api/client.ts                   # Axios: analyzeProperty, compareProperties, trackEvent
│   └── types/analysis.ts              # TypeScript interfaces matching Pydantic models
├── docker-compose.yml                  # Redis only
└── README.md

Data sources

Source What Cost
Nominatim Geocoding (address → lat/lng) Free
Overpass API POIs, schools, bars, transit within 500m Free
Catastro Surface, year built, energy cert, cadastral value Free
Fotocasa Active listing comparables by district and size Free (scraper)
INE Census sections, median price/m² (reference only) Free
Open Data BCN Safety indices, business licences, works permits Free
Area Verda Official regulated street-parking segments, tariffs, hours, resident zones Free
Inside Airbnb Tourist apartment density Free
OSM Baseline Ratings POI ratings from OSM name/type tags Free
Claude Code CLI AI narrative generation Free (subscription)

Price signal: Fotocasa comparables (primary)

The primary price signal is real active listings scraped from Fotocasa, filtered by district and size range. This gives an accurate picture of what similar properties are asking today.

INE median price/m² data (census section level) is retained as a background reference signal but is no longer shown as a "fair value estimate" — INE data underestimates the Barcelona market by 30–50% and the resulting valuation range (±24%) was too wide to be actionable.

AI narrative: Claude Code subscription path

The narrative verdict is generated by calling the claude CLI as a subprocess — identical to the pattern used in voice_blog's ClaudeCodeBackend. No separate Anthropic API key billing; it runs against the developer's existing Claude Code subscription.


Scoring system

8 dimensions

Dimension What it measures Key signals
Convenience Day-to-day access Transit proximity, amenity density (log-saturated), Google quality
Safety Crime risk Theft, vehicle crime, vandalism, night safety (Open Data BCN)
Property Building quality Age + ITE status, energy certificate, unit features (lift, terrace, parking)
Market Price position Fotocasa comparables position, price trend, rental yield
Risk Hidden liabilities Structural age, flood zone, legal clarity
Liveability Quality of life School quality, night noise, day noise, neighbourhood trajectory
HiddenCosts Ongoing cost burden IBI, community fees, derrama risk, energy upgrade liability
Intangible Neighbourhood character Cultural density, local market, green/urban quality

Buyer questionnaire (8 questions)

The questionnaire replaces static buyer profiles. Each answer adjusts dimension and sub-dimension weights:

Question What it adjusts
Q1: Children? Liveability (school weight), Safety
Q2: Stay duration? Market weight, Risk weight
Q3: Drive regularly? Convenience sub-weights (transit vs. amenity), Safety (vehicle crime)
Q4: Rental intent? Market weight, rental yield sub-weight
Q5: Noise tolerance? Liveability noise sub-weights
Q6: Work situation? Convenience (transit), Liveability (day noise)
Q7: Renovation appetite? HiddenCosts, Property, Risk
Q8: Neighbourhood character? Intangible dimension weight

Legacy string profiles (balanced, family, investor, retiree, expat) remain supported for the /compare endpoint.

Penalty multipliers

Applied multiplicatively after the weighted sum:

Penalty Trigger Multiplier
Critical risk Any risk sub-score < 20 ×0.70
Overpriced Listing well above district comparables ×0.80–0.94
Derrama risk Derrama risk = high ×0.93
Tourist saturation Airbnb risk = very_high (owner-occupier) ×0.94

Scoring design principles

  • Missing data = neutral, not worst-case. If Catastro doesn't return energy cert or year built, scoring uses district-average neutral values. Absence from a public registry is common in Spain and does not indicate a problem.
  • Floor level adjusts noise. Each floor above ground adds ~5 points to the noise score (cap +40 at ~8th floor).
  • Bars are convenience, not just noise. Bars/pubs/cafes contribute to Convenience amenity density. Only amenity=nightclub is weighted heavily as a noise source.
  • Log-saturated POI density. Diminishing returns on POI counts: 100 × (1 − 1/(1 + count/half_sat)).
  • Price fairness from real listings. price_fairness sub-score in the Market dimension uses the Fotocasa comparables position (well_below→100, within_range→65, well_above→20), not INE estimates.

API reference

POST /analyze

{
  "address": "Carrer de Mallorca 401, Barcelona",
  "listing_price": 480000,
  "buyer_profile": "balanced",
  "user_answers": {
    "children_age": "young",
    "stay_duration": "long",
    "has_car": false,
    "rental_intent": "none",
    "noise_tolerance": "need_quiet",
    "work_situation": "commute",
    "renovation_appetite": "move_in_ready",
    "lifestyle_priority": "important"
  },
  "year_built": 1975,
  "floor": 3,
  "surface_m2": 90,
  "energy_cert": "D",
  "condition": "renovated",
  "language": "en"
}

All fields except address are optional. user_answers overrides buyer_profile when provided.

Returns AnalyzeResponse — see backend/models/response.py for the full schema. Key fields:

Field Description
market_comparables Fotocasa active listings in district, P25/median/P75, asking position
acquisition_costs ITP or IVA+AJD, notary, registry, gestoria, total cash needed, min savings
seller_economics Agency commission, plusvalía estimate, seller floor, negotiation headroom %
disclosures 4–6 pre-purchase disclosure items (severity: red/yellow/green)
score Composite + 8 dimensions + penalty multipliers
narrative AI verdict, key risks, key positives, negotiation angle

POST /compare

{
  "addresses": ["Carrer de Mallorca 401, Barcelona", "Passeig de Gràcia 55, Barcelona"],
  "listing_prices": [480000, 650000],
  "buyer_profile": "family"
}

POST /track

Frontend behaviour events (section views, PDF downloads, language switches). Fired automatically by useReportAnalytics hook.

{
  "session_id": "uuid-from-sessionstorage",
  "request_id": "uuid-from-analysis",
  "event": "section_view",
  "data": { "section": "valuation" }
}

GET /admin/analytics?token=<ANALYTICS_TOKEN>

Returns the HTML analytics dashboard. Shows DAU, district distribution, price ranges, language split, Fotocasa success rate, duration percentiles, section views, and a recent analyses table.

Raw JSON at /admin/analytics/json?token=<token>.

GET /health

{"status": "ok", "service": "property-analyzer"}

Analytics

Analytics uses Postgres when DATABASE_URL is set, and automatically creates the required tables on first use:

  • analysis_reports — successful generated reports
  • analysis_errors — failed analysis attempts and exception summaries
  • report_events — frontend behaviour events such as section views and PDF downloads

When DATABASE_URL is unset or unreachable, analytics falls back to $ANALYTICS_FILE (default /tmp/pa_analytics.jsonl) and $EVENTS_FILE (/tmp/pa_events.jsonl). This fallback is useful locally but ephemeral on Railway.

Each analysis log entry captures: timestamp, anonymised IP hash, district, composite score, price bucket, language, duration (ms), Fotocasa scraper success, UserAnswers summary, score by dimension.

For production, create a Neon Postgres database and set DATABASE_URL in Railway using the pooled connection string, usually ending in sslmode=require. Set ANALYTICS_TOKEN to enable the dashboard at /admin/analytics.


Caching

Full analyses are cached in Redis for 24 hours keyed on all input parameters. The @cached decorator in utils/cache.py falls back gracefully to no-cache if Redis is unavailable (development without Docker).

To clear during development:

docker exec property_analysis-redis-1 redis-cli FLUSHALL

Known limitations

Area Current state Notes
Fotocasa comparables Scraped on each fresh analysis; may fail if anti-bot measures change Falls back to district statistics
Catastro data Coordinate-based lookup; some buildings return no data Score falls back to neutral values
Acquisition costs Barcelona/Catalunya, Madrid, and Valencia rates supported; other autonomous communities are not New build (IVA+AJD) supported
Seller plusvalía Estimated from cadastral value + ~10 year ownership assumption Actual varies significantly
Airbnb data Quarterly CSV snapshot Auto-downloaded on first run, cached locally
Safety data District-level, not street-level 10 BCN districts
Market data Annual growth and yield are 2024 static averages INE reference only
Madrid / Valencia ITP/IBI/Plusvalía, disclosures, and Airbnb heuristics are city-aware Safety scoring and neighbourhood trajectory remain Barcelona-only — no equivalent open data source found yet for the other two cities
Cities beyond BCN/Madrid/Valencia Not supported Geocoder resets to unsupported city and degrades gracefully (see backend/services/geocoder.py)

Roadmap

  • Buyer questionnaire (8 questions replacing static profiles)
  • Fotocasa market comparables as primary price signal
  • Acquisition cost calculator (ITP/IVA, notary, registry)
  • Seller economics panel (seller floor + negotiation headroom)
  • Pre-purchase disclosures module
  • Full Chinese (中文) i18n
  • Analytics pipeline (event log + HTML dashboard + frontend tracking)
  • Madrid and Valencia support (city-aware tax/legal logic; safety data stays BCN-only)
  • Freemium gate (5 free analyses/day currently; paywall for AI narrative)
  • PDF export (print CSS exists; dedicated export page TBD)
  • Property save + price-change alerts (requires auth)
  • Renovation cost estimator (surface × condition → cost range)
  • Community meeting minutes analyser (PDF → AI summary)
  • is_new_build flag in request (currently defaults to resale for acquisition costs)
  • Safety/crime and neighbourhood-trajectory data sources for Madrid and Valencia

Contributing

Bug reports and PRs welcome. See CONTRIBUTING.md before opening a PR — this project had a real quality incident early on (10 "done" features, 18 bugs, 3 production crashes) and the repo's CLAUDE.md documents the review protocol adopted afterward. It's a good read even if you're not using Claude Code.

License

MIT — see LICENSE.

About

Spanish property due-diligence tool — AI-generated report covering hidden costs, taxes, disclosures, and neighbourhood data for Barcelona, Madrid & Valencia. Built end-to-end, then open-sourced under MIT.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages