feat: Cast Iron Scout agent prototype

- Created autonomous cast iron deal scanner
- Scans eBay RSS feeds hourly for cast iron cookware
- Calculates FMV based on brand, type, size
- Sends Telegram alerts for deals ≥50% below FMV
- Identifies Griswold, Wagner, Wapak, Birmingham, Lodge, Victor
- Tracks seen items to avoid duplicate alerts
- Valuation engine with size multipliers
- Configurable preferences in config.json

Known issue: eBay RSS unreliable - next iteration will use proper scraping
This commit is contained in:
2026-04-09 17:45:36 -04:00
parent 674c2c3925
commit 06fb4a243e
10 changed files with 462 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
# Cast Iron Scout 🔥🍳
Autonomous agent that scans for undervalued cast iron cookware deals and alerts you when pieces are priced ≥50% below fair market value.
## Status: 🚧 Prototype
**Built:** April 9, 2026
**Current Sources:** eBay (RSS feeds)
**Next:** Facebook Marketplace, Craigslist integration
## What It Does
1. **Scans** marketplaces every hour for cast iron cookware
2. **Identifies** brands (Griswold, Wagner, Wapak, Birmingham, Lodge, etc.)
3. **Calculates** fair market value based on brand, type, and size
4. **Alerts** you via Telegram when deals ≥50% below FMV are found
5. **Tracks** seen items to avoid duplicate alerts
## Example Alert
```
🔥 CAST IRON DEAL ALERT!
Item: Griswold #8 Skillet - Slant Logo
Price: $45.00
FMV: $180.00
Discount: 75% below FMV! 💰
Source: eBay
Found: 2026-04-09T17:44:23
🔗 [link to item]
Action: Buy now / Bid / Ignore
```
## Architecture
```
cast-iron-scout/
├── scanner.py # Main scanning engine
├── valuation.py # FMV calculator
├── config.json # Your preferences
├── state/ # Track seen items
├── logs/ # Scan logs
└── sources/
├── ebay_scanner.py # eBay scanner (RSS)
├── facebook.py # [TODO]
└── craigslist.py # [TODO]
```
## Configuration
Edit `config.json` to customize:
- **min_discount_percent**: Minimum discount to alert (default: 50%)
- **max_distance_miles**: Max distance for local pickup (default: 500)
- **preferred_brands**: Brands to prioritize
- **item_types**: Types of items to scan for
- **telegram_target**: Where to send alerts
## Current Features
✅ Basic eBay RSS scanning
✅ Brand identification (Griswold, Wagner, Wapak, etc.)
✅ FMV calculation engine
✅ Telegram alerts
✅ Duplicate prevention
✅ Hourly scanning
## Known Issues / TODO
- [ ] eBay RSS feed unreliable - need to implement proper scraping
- [ ] Add Facebook Marketplace scanner
- [ ] Add Craigslist scanner
- [ ] Image recognition for logos/marks
- [ ] Price history tracking
- [ ] Condition assessment from photos
- [ ] Mobile app integration?
## Running Manually
```bash
cd /Users/claw/.openclaw/workspace/agents/cast-iron-scout
python3 scanner.py
```
## Logs
Check `logs/scanner-YYYYMMDD.log` for scan history.
## Next Steps
1. Fix eBay scanner (use proper HTML scraping vs RSS)
2. Add Facebook Marketplace (requires Selenium/Playwright)
3. Add image recognition for logos
4. Build sold items database for better FMV
---
**Maintained by:** Forge (Chris's SaaS Operations Bot)
**Version:** 0.1.0 (Prototype)

View File

@@ -0,0 +1,44 @@
{
"scan_interval_minutes": 30,
"min_discount_percent": 50,
"max_distance_miles": 500,
"preferred_brands": [
"griswold",
"wagner",
"wapak",
"birmingham",
"lodge",
"victor",
"hollowware"
],
"item_types": [
"skillet",
"griddle",
"dutch oven",
"pot",
"pan",
"grill pan",
"waffle iron",
"mold"
],
"condition_keywords": [
"rust",
"restoration",
"as-is",
"vintage",
"antique",
"estate",
"garage sale",
"dirty",
"needs work"
],
"exclude_keywords": [
"new",
"reproduction",
"replica",
"modern",
"calphalon",
"le creuset"
],
"telegram_target": "telegram:8269921691"
}

View File

@@ -0,0 +1,3 @@
[2026-04-09 17:44:23] 🔍 Starting cast iron scan...
[2026-04-09 17:44:28] Found 0 items on eBay
[2026-04-09 17:44:28] Scan complete. Deals found: 0, Total items processed: 0

View File

@@ -0,0 +1,6 @@
requests
beautifulsoup4
lxml
pillow
python-dateutil
feedparser

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Cast Iron Scout - Main Scanner Engine
Continuously scans for cast iron deals and alerts when good deals found
"""
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from sources.ebay_scanner import search_ebay_cast_iron
from valuation import is_good_deal, calculate_fmv
SCRIPT_DIR = Path(__file__).parent
STATE_FILE = SCRIPT_DIR / "state" / "seen_items.json"
CONFIG_FILE = SCRIPT_DIR / "config.json"
LOG_FILE = SCRIPT_DIR / "logs" / f"scanner-{datetime.now().strftime('%Y%m%d')}.log"
def load_config():
"""Load configuration"""
if CONFIG_FILE.exists():
return json.loads(CONFIG_FILE.read_text())
return {}
def load_state():
"""Load previously seen items to avoid duplicates"""
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {"seen_links": [], "last_scan": None}
def save_state(state):
"""Save state to file"""
state['last_scan'] = datetime.now().isoformat()
STATE_FILE.write_text(json.dumps(state, indent=2))
def log(message):
"""Log message"""
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_line = f"[{ts}] {message}"
print(log_line)
with open(LOG_FILE, 'a') as f:
f.write(log_line + '\n')
def send_telegram_alert(item, fmv, discount):
"""Send Telegram alert for a good deal"""
config = load_config()
target = config.get('telegram_target', 'telegram:8269921691')
message = f"""🔥 *CAST IRON DEAL ALERT!*
*Item:* {item['title']}
*Price:* ${item['price']:.2f}
*FMV:* ${fmv:.2f}
*Discount:* {discount:.0f}% below FMV! 💰
*Source:* {item['source']}
*Found:* {item['found_at']}
🔗 {item['link']}
_Action: Buy now / Bid / Ignore_"""
try:
subprocess.run([
'openclaw', 'message', 'send',
'--channel', 'telegram',
'--target', target,
'--message', message
], capture_output=True, timeout=30)
log(f"✅ Alert sent for: {item['title'][:50]}")
except Exception as e:
log(f"❌ Failed to send alert: {e}")
def scan_all_sources():
"""Scan all sources for cast iron items"""
log("🔍 Starting cast iron scan...")
# Load config and state
config = load_config()
state = load_state()
seen_links = set(state.get('seen_links', []))
# Scan eBay
items = search_ebay_cast_iron()
log(f"Found {len(items)} items on eBay")
deals_found = 0
min_discount = config.get('min_discount_percent', 50)
for item in items:
# Skip if already seen
if item['link'] in seen_links:
continue
# Check if it's a good deal
is_deal, discount, fmv = is_good_deal(item['price'], item['title'], min_discount)
if is_deal:
log(f"🎯 DEAL FOUND: {item['title'][:50]} - ${item['price']} ({discount:.0f}% off)")
send_telegram_alert(item, fmv, discount)
deals_found += 1
# Mark as seen
seen_links.add(item['link'])
# Keep only last 1000 seen items to prevent state file from growing forever
if len(seen_links) > 1000:
seen_links = set(list(seen_links)[-1000:])
state['seen_links'] = list(seen_links)
save_state(state)
log(f"Scan complete. Deals found: {deals_found}, Total items processed: {len(items)}")
return deals_found
if __name__ == "__main__":
deals = scan_all_sources()
sys.exit(0)

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
eBay Cast Iron Scanner
Scans eBay for cast iron cookware deals
"""
import requests
import re
from datetime import datetime
from bs4 import BeautifulSoup
def search_ebay_cast_iron():
"""
Search eBay for cast iron items
Returns list of items found
"""
# eBay search URL for cast iron cookware
# Using their REST API would be better but requires API keys
# For now, we'll use RSS feeds which are public
search_terms = [
"griswold skillet",
"wagner cast iron",
"vintage cast iron skillet",
"cast iron restoration",
"wapak skillet",
"birmingham skillet"
]
items = []
for term in search_terms:
# eBay RSS feed (no API key needed!)
rss_url = f"https://www.ebay.com/sch/i.html?_from=R40&_nkw={term.replace(' ', '%20')}&_sacat=0&LH_TitleDesc=0&_rss=1"
try:
response = requests.get(rss_url, timeout=10)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'lxml-xml')
entries = soup.find_all('item')
for entry in entries[:10]: # Top 10 results
try:
title = entry.find('title').text
link = entry.find('link').text
pub_date = entry.find('pubDate').text
# Extract price from description or title
price_match = re.search(r'\$([\d,]+\.?\d*)', title)
price = float(price_match.group(1).replace(',', '')) if price_match else 0
items.append({
'title': title,
'price': price,
'link': link,
'source': 'eBay',
'found_at': datetime.now().isoformat(),
'pub_date': pub_date
})
except Exception as e:
continue
except Exception as e:
print(f"Error scanning eBay for '{term}': {e}")
return items
if __name__ == "__main__":
print("🔍 Scanning eBay for cast iron deals...")
items = search_ebay_cast_iron()
print(f"Found {len(items)} items")
for item in items[:5]:
print(f" - {item['title'][:60]} - ${item['price']}")

View File

@@ -0,0 +1,4 @@
{
"seen_links": [],
"last_scan": "2026-04-09T17:44:28.435221"
}

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Cast Iron Valuation Engine
Determines fair market value and calculates discount percentage
"""
import re
# Base price ranges for common cast iron items (in good condition)
BASE_PRICES = {
'griswold': {'skillet': 150, 'griddle': 200, 'dutch_oven': 250, 'pot': 120, 'pan': 100},
'wagner': {'skillet': 120, 'griddle': 180, 'dutch_oven': 220, 'pot': 100, 'pan': 90},
'wapak': {'skillet': 180, 'griddle': 220, 'dutch_oven': 280, 'pot': 150, 'pan': 130},
'birmingham': {'skillet': 160, 'griddle': 190, 'dutch_oven': 240, 'pot': 130, 'pan': 110},
'lodge': {'skillet': 80, 'griddle': 120, 'dutch_oven': 150, 'pot': 70, 'pan': 60},
'victor': {'skillet': 140, 'griddle': 170, 'dutch_oven': 210, 'pot': 120, 'pan': 100},
'default': {'skillet': 100, 'griddle': 150, 'dutch_oven': 200, 'pot': 80, 'pan': 70}
}
# Size multipliers
SIZE_MULTIPLIERS = {
'#1': 0.5, '#2': 0.6, '#3': 0.7, '#4': 0.8, '#5': 0.9,
'#6': 1.0, '#7': 1.1, '#8': 1.2, '#9': 1.3, '#10': 1.4,
'#11': 1.5, '#12': 1.6, '#13': 1.7, '#14': 1.8
}
def identify_brand(title):
"""Identify the brand from title"""
title_lower = title.lower()
brands = ['griswold', 'wagner', 'wapak', 'birmingham', 'lodge', 'victor', 'hollowware']
for brand in brands:
if brand in title_lower:
return brand
return 'default'
def identify_item_type(title):
"""Identify the type of item from title"""
title_lower = title.lower()
types = ['skillet', 'griddle', 'dutch oven', 'pot', 'pan', 'grill', 'waffle', 'mold']
for item_type in types:
if item_type in title_lower:
return item_type.replace(' ', '_')
return 'skillet' # Default to skillet
def extract_size(title):
"""Extract size number from title"""
# Look for patterns like #8, 8 inch, size 8, etc.
patterns = [
r'#(\d{1,2})',
r'size\s*(\d{1,2})',
r'(\d{1,2})\s*inch',
r'(\d{1,2})\s*"',
]
for pattern in patterns:
match = re.search(pattern, title, re.IGNORECASE)
if match:
size_num = match.group(1)
return f"#{size_num}"
return '#8' # Default to #8
def calculate_fmv(title):
"""
Calculate Fair Market Value for an item
Returns estimated FMV based on brand, type, and size
"""
brand = identify_brand(title)
item_type = identify_item_type(title)
size = extract_size(title)
# Get base price
brand_prices = BASE_PRICES.get(brand, BASE_PRICES['default'])
base_price = brand_prices.get(item_type, brand_prices['skillet'])
# Apply size multiplier
multiplier = SIZE_MULTIPLIERS.get(size, 1.0)
fmv = base_price * multiplier
return round(fmv, 2)
def is_good_deal(price, title, min_discount=50):
"""
Determine if an item is a good deal
Returns (is_deal, discount_percent, fmv)
"""
fmv = calculate_fmv(title)
if price <= 0 or fmv <= 0:
return False, 0, fmv
discount_percent = ((fmv - price) / fmv) * 100
if discount_percent >= min_discount:
return True, discount_percent, fmv
return False, discount_percent, fmv
if __name__ == "__main__":
# Test the valuation engine
test_items = [
"Griswold #8 Skillet - Rusty",
"Wagner Sidney O -AI - Vintage Cast Iron",
"Wapak Funny Face Skillet #10",
"Birmingham Stove & Range Co. #12 Skillet"
]
print("Valuation Engine Test:\n")
for title in test_items:
fmv = calculate_fmv(title)
print(f"{title[:50]}")
print(f" → FMV: ${fmv}\n")