PropIntel is a production-style real estate lead intelligence platform that ingests raw lead datasets, enriches them from external sources, verifies contact quality, and ranks lead readiness for outreach.
Built as a portfolio project to demonstrate practical delivery across backend systems, data workflows, and operator-friendly product UI.
- You get a repeatable lead intelligence workflow: upload files, run jobs, monitor progress, and export ranked leads.
- It is resilient for long-running processing: batching, partial result persistence, termination controls, and resume support.
- It is operator-friendly: dashboard tabs for analytics, job history, exploration, and runtime settings profiles.
- Flexible ingestion: CSV, JSON, and PropFlux-style inputs.
- Website enrichment: contact extraction, chatbot detection, freshness signals, website speed scoring.
- Google Maps enrichment: business matching, phone/website/location augmentation.
- Conflict resolution: source-aware candidate merging with enrichment history.
- Contact verification:
verified/likely/lowquality model. - Lead scoring: configurable scoring engine with explainable
lead_reason. - Batch processing: incremental writes to DB, lower memory pressure, partial visibility.
- Resumable jobs: failed/terminated jobs can be resumed.
- Concurrency + rate limiting: provider-aware limits for Serper and Google Maps.
- Responsive dashboard: control panel, analytics, job history, data explorer, engine settings.
PropIntel currently runs end-to-end as a complete enrichment pipeline + dashboard system with:
- strict config schema validation
- SQLite-backed job/result persistence
- batch lifecycle tracking (
pending/processing/completed/failed/terminated) - partial results during processing
- stop + resume controls
- provider-aware runtime controls for concurrency and request pacing
Ongoing work is focused on deployment and post-MVP extensions (integrations, additional enrichment sources, and operational hardening).
- Python 3.11+
- Node.js 20+ (for dashboard build/dev)
- API keys for optional external enrichment:
SERPER_API_KEYGOOGLE_MAPS_API_KEY
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r requirements.txtCreate .env from .env.example and fill keys if you want external enrichment:
cp .env.example .envpython runner.py api --host 127.0.0.1 --port 8000 --reloadHealth check:
curl http://127.0.0.1:8000/healthcd frontend/dashboard
npm install
npm run devOpen:
- Dashboard:
http://127.0.0.1:5173 - API:
http://127.0.0.1:8000
runner.py supports:
apiβ start FastAPI serverrunβ execute pipeline locally and export artifacts
Examples:
# Start API
python runner.py api --host 0.0.0.0 --port 8080 --reload --log-level debug
# Run pipeline with CSV input
python runner.py run --input data/leads.csv --input-format csv --config config/sources.yaml --output output --log-level info
# Run pipeline with PropFlux-like JSON
python runner.py run --input data/propflux_export.json --input-format propfluxCLI runs create a timestamped folder under output/:
output/<timestamp>/
Artifacts per run:
leads_<timestamp>.jsonleads_<timestamp>.csvrejected_rows_<timestamp>.jsonrun_summary_<timestamp>.json- logs in
logs/propintel_<timestamp>.log
PropIntel is built for repeatable operations, not one-off scripts.
- A dataset is uploaded (
POST /jobs) or run through CLI. - Input rows are mapped/validated, normalized, deduplicated.
- Job is split into batches and persisted in SQLite (
job_batches). - Enrichment runs with configurable concurrency and provider-aware rate limits.
- Each completed batch writes leads immediately to DB (partial results available).
- Verification, conflict resolution, and scoring produce final lead intelligence fields.
- Dashboard/API surfaces telemetry, progress, outputs, and resume/termination controls.
The dashboard lives in frontend/dashboard/ and talks to the FastAPI backend.
- Upload dataset (
csv | json | propflux) - Start, terminate, and resume jobs
- See live progress (started/completed batches, row progress)
- See recent jobs and quick-select active job
- Filter by score, contact quality, chatbot signal, freshness signal
- Export active job results (JSON/CSV once completed)
- Shows partial results while job is processing
- Total jobs, completed jobs
- Average lead score
- Verified contact rate
- Full job listing with statuses and run metadata
- Mobile-friendly card layout for non-desktop widths
- Select a job and inspect rows in detail
- Live reload + responsive card/table behavior
- Validate and save profile JSON
- Activate/delete profiles
- Active profile is used by job processing
POST /jobsβ create job from uploaded fileGET /jobsβ paginated jobs list (limit,offset, optionalstatus)GET /jobs/{job_id}β status + counts + batch progressPOST /jobs/{job_id}/terminateβ stop running jobPOST /jobs/{job_id}/resumeβ resume failed/terminated jobGET /jobs/{job_id}/batchesβ batch lifecycle rows
GET /jobs/{job_id}/resultsβ returns current rows (partial=trueuntil completed)GET /jobs/{job_id}/rejectedβ rejected rows for the jobGET /jobs/{job_id}/export?format=csv|jsonβ export completed results
GET /settingsPOST /settings/validatePUT /settingsPOST /settings/activateDELETE /settings/{name}
Configured in config/sources.yaml or via Engine Settings profiles:
inputmapping/validation ruleswebsiteenrichment controlsgoogle_mapsenrichment controlsscoringweights and score behaviorruntimebatching + worker concurrency + provider rate limits
Minimal runtime defaults are included; advanced knobs are optional.
Run backend tests:
python -m unittest discover -s tests -p "test_*.py"Build dashboard:
cd frontend/dashboard
npm run buildCI workflow is included at .github/workflows/ci.yml for backend tests + frontend build.
This runs backend and frontend as separate containers with the frontend calling the backend via VITE_API_BASE_URL.
docker build -f deploy/fly/backend.Dockerfile -t propintel-api:local .
docker build -f deploy/fly/frontend.Dockerfile \
--build-arg VITE_API_BASE_URL=http://localhost:8000 \
-t propintel-web:local .docker run -d \
--name propintel-api \
-p 8000:8000 \
--env-file .env \
-v "$(pwd)/data:/app/data" \
propintel-api:localdocker run -d \
--name propintel-web \
-p 8080:8080 \
propintel-web:localOpen: http://localhost:8080
docker rm -f propintel-web propintel-apiNotes:
VITE_API_BASE_URLis compile-time for Vite; rebuild frontend image when backend URL changes.- Keep
.envlocal only; do not commit secrets.
Deployment is configured for two public Fly.io apps:
propintel-web(public): serves the React dashboardpropintel-api(public): serves FastAPI + SQLite
Frontend uses VITE_API_BASE_URL at build time to call the public backend API.
Deployment assets:
deploy/fly/backend.Dockerfiledeploy/fly/frontend.Dockerfiledeploy/fly/backend.fly.tomldeploy/fly/frontend.fly.toml
# Create app (one-time)
fly apps create propintel-api
# Create persistent volume for SQLite/uploads (one-time)
fly volumes create propintel_data --size 1 --region jnb --app propintel-api
# Set backend secrets
fly secrets set SERPER_API_KEY=... GOOGLE_MAPS_API_KEY=... -a propintel-api
# Deploy backend
fly deploy -c deploy/fly/backend.fly.toml# Create app (one-time)
fly apps create propintel-web
# Optional: set explicit backend URL for this frontend app build
# fly secrets set VITE_API_BASE_URL=https://propintel-api.fly.dev -a propintel-web
# Deploy frontend
fly deploy -c deploy/fly/frontend.fly.tomldeploy/fly/frontend.fly.tomlcurrently builds withVITE_API_BASE_URL=https://propintel-api.fly.dev.- If backend app domain changes, update
build.args.VITE_API_BASE_URLand redeploy frontend. - Restrict backend CORS to your frontend domain(s) in production settings.
- Never commit real API keys or
.envfiles. - Use
.env.exampleas the template. - Rotate any development keys before public release.
Built with love for practical lead intelligence and clean, reliable data workflows.