diff --git a/agents/cast-iron-scout/README.md b/agents/cast-iron-scout/README.md new file mode 100644 index 0000000..777d965 --- /dev/null +++ b/agents/cast-iron-scout/README.md @@ -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) diff --git a/agents/cast-iron-scout/__pycache__/valuation.cpython-314.pyc b/agents/cast-iron-scout/__pycache__/valuation.cpython-314.pyc new file mode 100644 index 0000000..8cb7d43 Binary files /dev/null and b/agents/cast-iron-scout/__pycache__/valuation.cpython-314.pyc differ diff --git a/agents/cast-iron-scout/config.json b/agents/cast-iron-scout/config.json new file mode 100644 index 0000000..fe1445d --- /dev/null +++ b/agents/cast-iron-scout/config.json @@ -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" +} diff --git a/agents/cast-iron-scout/logs/scanner-20260409.log b/agents/cast-iron-scout/logs/scanner-20260409.log new file mode 100644 index 0000000..486c87a --- /dev/null +++ b/agents/cast-iron-scout/logs/scanner-20260409.log @@ -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 diff --git a/agents/cast-iron-scout/requirements.txt b/agents/cast-iron-scout/requirements.txt new file mode 100644 index 0000000..6d71ee5 --- /dev/null +++ b/agents/cast-iron-scout/requirements.txt @@ -0,0 +1,6 @@ +requests +beautifulsoup4 +lxml +pillow +python-dateutil +feedparser diff --git a/agents/cast-iron-scout/scanner.py b/agents/cast-iron-scout/scanner.py new file mode 100644 index 0000000..734fe3a --- /dev/null +++ b/agents/cast-iron-scout/scanner.py @@ -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) diff --git a/agents/cast-iron-scout/sources/__pycache__/ebay_scanner.cpython-314.pyc b/agents/cast-iron-scout/sources/__pycache__/ebay_scanner.cpython-314.pyc new file mode 100644 index 0000000..888a374 Binary files /dev/null and b/agents/cast-iron-scout/sources/__pycache__/ebay_scanner.cpython-314.pyc differ diff --git a/agents/cast-iron-scout/sources/ebay_scanner.py b/agents/cast-iron-scout/sources/ebay_scanner.py new file mode 100644 index 0000000..ee01dca --- /dev/null +++ b/agents/cast-iron-scout/sources/ebay_scanner.py @@ -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']}") diff --git a/agents/cast-iron-scout/state/seen_items.json b/agents/cast-iron-scout/state/seen_items.json new file mode 100644 index 0000000..1add391 --- /dev/null +++ b/agents/cast-iron-scout/state/seen_items.json @@ -0,0 +1,4 @@ +{ + "seen_links": [], + "last_scan": "2026-04-09T17:44:28.435221" +} \ No newline at end of file diff --git a/agents/cast-iron-scout/valuation.py b/agents/cast-iron-scout/valuation.py new file mode 100644 index 0000000..150c549 --- /dev/null +++ b/agents/cast-iron-scout/valuation.py @@ -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")