Architecture
How the pieces fit together before you start configuring.
The daemon is a single Python process that runs scrapers on a schedule, scores listings through the rule engine + Claude, dispatches emails via Gmail, and serves a FastAPI backend. The Telegram bot and React dashboard are your control surfaces.
0 Prerequisites
What you need installed before starting.
| Requirement | Version | Check |
|---|---|---|
| Python | 3.11+ | python --version |
| Node.js | 18+ | node --version |
| Git | any | git --version |
| pip | latest | pip --version |
Clone & Install
# Clone the repo (or copy the apt-hunter directory to your server)
cd ~/
git clone <your-repo-url> apt-hunter
cd apt-hunter
# Create virtual environment and install Python deps
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Install frontend deps
cd frontend
npm install
cd ..
1 Create a Telegram Bot
The bot is your mobile control plane — scan, track, approve, and monitor from your phone.
Open Telegram and search for @BotFather. Send /newbot.
Choose a name (e.g. Apt Hunter) and a username (e.g. apt_hunter_nyc_bot). BotFather will respond with your bot token — a string like 7123456789:AAH....
Start a conversation with your new bot — open it and press Start. Then get your chat ID by visiting:
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
Look for "chat":{"id": XXXXXXXX} in the response. That number is your chat ID.
Save both values — you'll put them in config.json in step 3.
Available Commands
Once running, the bot registers these commands automatically:
| Command | What it does |
|---|---|
| /status | Source health, tier counts, active listings |
| /scan | Trigger an immediate scan cycle |
| /pause | Pause the scan loop |
| /resume | Resume scanning |
| /top N | Show top N listings by score |
| /rules | List active rules |
| /track <id> | Start tracking a listing for price changes |
| /untrack <id> | Stop tracking |
| /approve <id> | Approve and send a queued email draft |
| /dismiss <id> | Dismiss a listing |
| /digest | Generate and view today's digest |
2 Anthropic API Key
Claude scores every listing for quality, scam risk, and value — and composes outreach emails.
Go to console.anthropic.com and sign in (or create an account).
Navigate to API Keys and create a new key. Copy it immediately — it won't be shown again.
Add billing. Apt Hunter uses approximately $1–3/day at typical NYC listing volume:
- Haiku for listing scoring (~$0.001/listing, ~500/day)
- Sonnet for email composition (~$0.01/email, ~5-20/day)
- Haiku for NL queries (ad-hoc)
3 Configure config.json
The central configuration file. Located at the project root.
{
"telegram_bot_token": "7123456789:AAH...",
"telegram_chat_id": "8426763327",
"anthropic_api_key": "sk-ant-...",
"claude_scoring_model": "claude-haiku-4-5-20251001",
"claude_email_model": "claude-sonnet-4-6-20260320",
"api_port": 8080,
"db_path": "apt_hunter.db",
"digest_time_et": "09:00",
"scoring": {
"rule_weight": 0.6, // 60% deterministic rules
"claude_weight": 0.4, // 40% Claude assessment
"auto_send_threshold": 85.0, // auto-email above this
"review_threshold": 65.0, // draft for review
"flag_threshold": 40.0 // just flag in dashboard
},
"sources": {
"streeteasy": { "enabled": true, "interval_hours": 2.0 },
"craigslist": { "enabled": true, "interval_hours": 2.0 },
"zillow": { "enabled": false, "interval_hours": 4.0 },
"apartments_com": { "enabled": false, "interval_hours": 4.0 },
"fb_marketplace": { "enabled": false, "interval_hours": 6.0 },
"fb_groups": { "enabled": false, "interval_hours": 6.0 },
"leasebreak": { "enabled": false, "interval_hours": 24.0 },
"listings_project": { "enabled": false, "interval_hours": 168.0}
}
}
Key Settings
| Field | What it controls |
|---|---|
| auto_send_threshold | Listings scoring above this get emailed automatically. Set to 95+ (or disable source) while calibrating. |
| rule_weight / claude_weight | How much deterministic rules vs Claude assessment influence the composite score. Must sum to 1.0. |
| interval_hours | How often each source is scanned. The main loop sleeps for the minimum enabled interval. |
| claude_scoring_model | Haiku for scoring (cheap, fast). Sonnet for email composition (better writing). |
4 Gmail OAuth Setup
Optional — enables automated email outreach from your real Gmail address. Skip this to use the system without email.
Go to console.cloud.google.com. Select or create a project.
Enable the Gmail API: navigate to APIs & Services → Library → search "Gmail API" → Enable.
Create OAuth credentials: APIs & Services → Credentials → Create Credentials → OAuth client ID. Choose Desktop app. Download the JSON file.
Run the initial auth flow to generate a token. Create this script and run it once:
# generate_gmail_token.py
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = [
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.readonly",
]
flow = InstalledAppFlow.from_client_secrets_file(
"client_secret.json", # the file you downloaded
scopes=SCOPES,
)
creds = flow.run_local_server(port=0)
# Save the token
import json, os
token_dir = os.path.expanduser("~/.config/apt-hunter")
os.makedirs(token_dir, exist_ok=True)
token_path = os.path.join(token_dir, "gmail_token.json")
with open(token_path, "w") as f:
json.dump(json.loads(creds.to_json()), f, indent=2)
print(f"Token saved to {token_path}")
Run it: python generate_gmail_token.py. A browser window opens — sign in with the Gmail account you want to send from. Authorize the app.
~/.config/apt-hunter/gmail_token.json contains your Gmail credentials. Keep it out of version control. The daemon checks for this file on startup — if missing, email features are disabled gracefully.5 Seed Default Rules
Pre-populate the rule engine with sensible defaults. You'll customize these from the dashboard or Telegram.
# From the project root, with venv active:
python seed_rules.py
# Output:
Seeded 11 rules (2 hard, 9 soft).
Default Hard Filters
| Rule | Filter |
|---|---|
| Max price | price < $5,000 |
| Min bedrooms | bedrooms ≥ 1 |
Default Soft Scoring
| Rule | Weight | Logic |
|---|---|---|
| Price sweetness | 25 | Linear scale — cheaper is better |
| Neighborhoods | 20 | Tier 1/2/3 scoring by area |
| Laundry | 15 | In-unit > in-building > none |
| Floor | 10 | Floors 2-5 preferred |
| No-fee bonus | 10 | No broker fee = full score |
| Dishwasher | 5 | Binary bonus |
| Outdoor space | 5 | Binary bonus |
| Photo count | 5 | 8+ photos = full score |
| Owner listing | 5 | Owner > mgmt > broker |
Modify these anytime via the Rules page in the dashboard, the /rules Telegram command, or the NL search ("save as rule").
6 Run the Daemon
Start scanning.
# Activate venv and run
source .venv/bin/activate
python main.py
On startup, you'll see:
Claude API connectivity OK (claude-haiku-4-5-20251001)
Telegram bot started in background thread
Telegram bot online
Gmail integration active # only if token exists
Price tracker and digest builder active
NL query engine active
API listening on port 8080
apt-hunter started — 2 sources enabled: streeteasy, craigslist
=== scan cycle starting ===
scanning streeteasy...
/status to your Telegram bot to verify it's running. You should see source health, tier counts, and active listing count.Set Up Your Applicant Profile
Before the auto-emailer composes messages, fill in your profile. Use the Settings page in the dashboard, or via API:
# Set your profile fields
curl -X POST http://localhost:8080/api/profile \
-H "Content-Type: application/json" \
-d '{"key": "name", "value": "Your Name"}'
curl -X POST http://localhost:8080/api/profile \
-d '{"key": "move_in_date", "value": "August 1"}' \
-H "Content-Type: application/json"
# Repeat for: current_neighborhood, employment,
# income_summary, household_size, pets, credit_score
7 Deploy the Dashboard
The React dashboard gives you a visual interface for browsing, tracking, and managing listings.
Local Development
cd frontend
npm run dev
# Opens at http://localhost:3000
# API calls proxy to localhost:8080
Production Build
cd frontend
npm run build
# Output in frontend/dist/
Serve via FastAPI
For production, serve the built frontend directly from the FastAPI backend. Add this to api/app.py:
# Add static file serving for the dashboard
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
# Mount after all /api routes
app.mount("/assets", StaticFiles(
directory="frontend/dist/assets"
), name="assets")
@app.get("/{path:path}")
async def spa_fallback(path: str):
return FileResponse("frontend/dist/index.html")
Expose via Cloudflare Tunnel
If running on a server behind a tunnel (e.g. apartments.slurbbot.com), point the tunnel at port 8080. Add Google OAuth middleware for authentication.
8 Systemd Service
Run as an always-on daemon that survives reboots.
# /etc/systemd/system/apt-hunter.service
[Unit]
Description=Apt Hunter - NYC Apartment Search
After=network.target
[Service]
Type=simple
User=seb
WorkingDirectory=/home/seb/apt-hunter
ExecStart=/home/seb/apt-hunter/.venv/bin/python main.py
Restart=always
RestartSec=10
EnvironmentFile=/home/seb/.secrets/secrets.env
[Install]
WantedBy=multi-user.target
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable apt-hunter
sudo systemctl start apt-hunter
# Check status
sudo systemctl status apt-hunter
journalctl -u apt-hunter -f
9 Troubleshooting
Common issues and how to fix them.
"Claude API probe failed" on startup
The API key is invalid or the model ID doesn't exist. Check anthropic_api_key in config.json. Verify your account has API credits at console.anthropic.com. Ensure the model ID matches an available model.
Telegram bot doesn't respond
Check that telegram_bot_token is correct. Make sure you started a conversation with the bot (press /start). Verify the telegram_chat_id matches the account you're messaging from. Check logs for "Telegram bot started in background thread".
Scraper returns 0 listings
CSS selectors may need calibration against the live site. StreetEasy and Zillow are the most aggressive about bot detection. Check the daemon logs for HTTP status codes. If you see 403/429, reduce rate_limit_rpm in config. Try enabling one source at a time.
"Gmail credentials not found"
Expected if you haven't set up Gmail OAuth (step 4). Email features are disabled gracefully — the daemon still runs. The token file must be at ~/.config/apt-hunter/gmail_token.json.
Dashboard shows no data
The dashboard reads from the API at port 8080. Ensure the daemon is running and the API is accessible. Check the browser console for CORS or network errors. For local dev, the Vite proxy handles CORS — make sure the daemon is on port 8080.
Tests failing after changes
# Run the full test suite
python -m pytest tests/ -v
# Run a specific test file
python -m pytest tests/test_rules.py -v
# Run with output for debugging
python -m pytest tests/ -v -s
Database is corrupted
Delete apt_hunter.db and restart the daemon — it will recreate the schema via migrate(). You'll lose historical data but the rules survive in seed_rules.py. For production, back up the DB file regularly.
scrapers/<source>.py to match the site's current HTML structure.