Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,95 @@ REDIS_PORT=6379
# =============================================================================
# Discord Webhook: Server Settings → Integrations → Webhooks
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
# All price-error, resale, and Marketplace alerts route to DISCORD_WEBHOOK_URL (Nova Price Error).
PRICE_BOT_DISCORD_READY_MENTION=
PRICE_BOT_DISCORD_REVIEW_MENTION=
PRICE_BOT_DISCORD_HOT_MENTION=

# Simple Discord price-error bot (no DB, Redis, or LLM calls)
# Run once: npm run worker:discord-price
# Loop mode: PRICE_BOT_LOOP=true npm run worker:discord-price
PRICE_BOT_LOOP=false
PRICE_BOT_INTERVAL_MS=900000
PRICE_BOT_FETCH_TIMEOUT_MS=20000
PRICE_BOT_PLAYWRIGHT_FALLBACK=true
PRICE_BOT_PLAYWRIGHT_TIMEOUT_MS=30000
PRICE_BOT_TARGET_DELAY_MS=1000
PRICE_BOT_MIN_CONFIDENCE=70
PRICE_BOT_MIN_ALERT_DISCOUNT=30
PRICE_BOT_MIN_WATCH_DISCOUNT=35
PRICE_BOT_MIN_HOT_DISCOUNT=50
PRICE_BOT_MIN_ERROR_DISCOUNT=80
PRICE_BOT_MAX_ALERTS=5
# Use pc-retailers-active.json for the productive lean list; pc-retailers.json keeps the broad research list.
PRICE_BOT_TARGETS_FILE=config/pc-retailers-active.json
PRICE_BOT_FEEDS_FILE=config/pc-feeds.json
PRICE_BOT_APIS_FILE=config/pc-apis.json
PRICE_BOT_OPENBOX_TARGETS_FILE=config/resale-openbox-retailers.json
PRICE_BOT_TOOL_TARGETS_FILE=config/resale-tool-retailers.json
PRICE_BOT_CAR_TARGETS_FILE=config/resale-car-retailers.json
PRICE_BOT_RETAILER_RELIABILITY_FILE=config/retailer-reliability.json
PRICE_BOT_USE_SQLITE=true
PRICE_BOT_SQLITE_FILE=.price-bot.sqlite
PRICE_BOT_HISTORY_LIMIT=30
PRICE_BOT_CROSS_STORE_DROP_PERCENT=35
PRICE_BOT_FAMILY_DEDUP_DROP_PERCENT=10
PRICE_BOT_MAX_SANE_PRICE=8000
PRICE_BOT_RESALE_ALERTS=true
PRICE_BOT_RESALE_MIN_PROFIT=60
PRICE_BOT_RESALE_MIN_MARGIN_PERCENT=18
PRICE_BOT_RESALE_MIN_DISCOUNT=35
PRICE_BOT_RESALE_LOCAL_FEE_PERCENT=0
PRICE_BOT_RESALE_FEE_PERCENT=13.25
PRICE_BOT_RESALE_TAX_PERCENT=8.25
PRICE_BOT_RESALE_SHIPPING_BUFFER=25
PRICE_BOT_OPENBOX_RESALE_ALERTS=true
PRICE_BOT_OPENBOX_RESALE_MIN_PROFIT=90
PRICE_BOT_OPENBOX_RESALE_MIN_MARGIN_PERCENT=28
PRICE_BOT_OPENBOX_RESALE_MIN_DISCOUNT=35
PRICE_BOT_TOOL_RESALE_ALERTS=true
PRICE_BOT_TOOL_RESALE_MIN_PROFIT=50
PRICE_BOT_TOOL_RESALE_MIN_MARGIN_PERCENT=22
PRICE_BOT_TOOL_RESALE_MIN_DISCOUNT=35
PRICE_BOT_CAR_RESALE_ALERTS=true
PRICE_BOT_CAR_RESALE_MIN_PROFIT=75
PRICE_BOT_CAR_RESALE_MIN_MARGIN_PERCENT=30
PRICE_BOT_CAR_RESALE_MIN_DISCOUNT=40
PRICE_BOT_BUY_SCORE_MIN=70
PRICE_BOT_EBAY_COMPS_ENABLED=false
PRICE_BOT_EBAY_COMPS_REQUIRE=true
PRICE_BOT_EBAY_COMPS_MIN_COUNT=3
PRICE_BOT_EBAY_COMPS_LIMIT=20
PRICE_BOT_EBAY_COMPS_MAX_AGE_DAYS=90
PRICE_BOT_READY_TO_BUY_SCORE=85
PRICE_BOT_NEEDS_REVIEW_SCORE=70
PRICE_BOT_MAX_READY_TO_BUY_PRICE=750
PRICE_BOT_BUY_AUTOMATION_ENABLED=false
PRICE_BOT_BUY_AUTOMATION_MODE=off
PRICE_BOT_MAX_AUTO_BUY_PRICE=250
PRICE_BOT_MAX_DAILY_AUTO_BUY_SPEND=500
PRICE_BOT_AUTO_BUY_RETAILERS=
PRICE_BOT_TARGET_FAILURE_SKIP_COUNT=3
PRICE_BOT_TARGET_SKIP_MINUTES=60
PRICE_BOT_DRY_RUN=false
PRICE_BOT_TEST_SEND=false
PRICE_BOT_SELF_TEST=false
PRICE_BOT_STATE_FILE=.price-bot-state.json
PRICE_BOT_FACEBOOK_MARKETPLACE_TARGETS_FILE=config/facebook-marketplace-searches.json
PRICE_BOT_FACEBOOK_MARKETPLACE_RAW_LISTINGS_FILE=
PRICE_BOT_FACEBOOK_MARKETPLACE_MAX_ALERTS=8
PRICE_BOT_FACEBOOK_MARKETPLACE_DRY_RUN=false
PRICE_BOT_FACEBOOK_MARKETPLACE_TEST_SEND=false

# Optional source keys
BESTBUY_API_KEY=
EBAY_CLIENT_ID=
EBAY_CLIENT_SECRET=
EBAY_ACCESS_TOKEN=
EBAY_MARKETPLACE_ID=EBAY_US
EBAY_OAUTH_SCOPE=https://api.ebay.com/oauth/api_scope/buy.marketplace.insights
# Optional JSON array overriding the built-in Best Buy and Target targets.
# PRICE_BOT_TARGETS=[{"retailer":"example","url":"https://example.com/deals","category":"electronics","selectors":{"container":".product","title":".title","price":".price","originalPrice":".was","link":"a","image":"img"}}]

# Twitter/X API (for bot posting)
TWITTER_API_KEY=...
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ yarn-error.log*
# local env files
.env*.local
.env
.price-bot-state.json
.price-bot.sqlite
.price-bot.sqlite-*
.argus-*.json
.argus-*.txt
.marketplace-*.json
logs/


# vercel
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,43 @@ npm run worker
# Or run specific workers
npm run worker:validate # Anomaly validator
npm run worker:notify # Notification sender
npm run worker:discord-price # Simple Discord price-error bot
```

### Simple Discord Price Bot

For the lightweight version, skip Postgres, Redis, queues, and AI validation. Set `DISCORD_WEBHOOK_URL`, then run:

```bash
npm run worker:discord-price
```

The bot scrapes the configured retailer pages, feeds, and optional APIs, uses deterministic price-drop detection, sends compact Discord webhook embeds, and stores price history/dedupe state in `.price-bot.sqlite` when available. Use `config/pc-retailers-active.json` for the productive lean list; `config/pc-retailers.json` keeps the broad research list. Feeds come from `config/pc-feeds.json`; optional API targets come from `config/pc-apis.json`. Static HTML is tried first, then Playwright renders pages that fail or return zero products. Repeatedly empty/failing targets are put on temporary cooldown. Use `PRICE_BOT_TARGETS_FILE` / `PRICE_BOT_FEEDS_FILE` / `PRICE_BOT_APIS_FILE` to point at other JSON files or `PRICE_BOT_TARGETS` to provide an inline selector-based list.

Resale alerts are separate from strict price-error alerts. The resale lanes are `RESALE` for new/sealed PC electronics, `OPENBOX_RESALE` for acceptable open-box PC deals, `TOOL_RESALE` for tool kits/batteries/scanners, and `CAR_RESALE` for local-friendly car gear. The default scoring is conservative and local-first: local resale uses `PRICE_BOT_RESALE_LOCAL_FEE_PERCENT=0`, shipped fallback uses `PRICE_BOT_RESALE_FEE_PERCENT=13.25`, tax uses `PRICE_BOT_RESALE_TAX_PERCENT=8.25`, and shipping buffer only applies to shipped estimates. Each lane must clear profit, margin, and a lane-specific minimum discount before alerting. Open-box/clearance can qualify only for resale lanes; used, damaged, as-is, missing, seller-refurbished, and for-parts listings are rejected.

Additional resale sources are loaded from `config/resale-openbox-retailers.json`, `config/resale-tool-retailers.json`, and `config/resale-car-retailers.json`. Override those with `PRICE_BOT_OPENBOX_TARGETS_FILE`, `PRICE_BOT_TOOL_TARGETS_FILE`, and `PRICE_BOT_CAR_TARGETS_FILE`. Retailer trust/penalty rules load from `config/retailer-reliability.json`.

Buy score combines estimated profit, margin, discount, liquidity/path, condition risk, and optional eBay sold comps. Set `PRICE_BOT_EBAY_COMPS_ENABLED=true` with eBay Marketplace Insights credentials (`EBAY_CLIENT_ID`/`EBAY_CLIENT_SECRET` or `EBAY_ACCESS_TOKEN`) to validate candidates against recent sold listings. Marketplace Insights is a limited-release eBay Buy API; if access is missing or forbidden, the bot logs the issue and continues without comp gating. When comps are enabled and available, candidates without enough sold comps are rejected by default via `PRICE_BOT_EBAY_COMPS_REQUIRE=true`.

Buy decisions are computed after buy score. Alerts can be `WATCH`, `NEEDS_REVIEW`, `READY_TO_BUY`, or `REJECTED`, and Discord shows comp confidence, buy-prep max price, preferred resale venue, decision notes, risk flags, local-only/store-pickup-only hints, and tool kit breakout value when detected. All price-error, resale, and Facebook Marketplace alerts route to `DISCORD_WEBHOOK_URL` so they land in Nova Price Error; lanes remain visible as alert fields, not Discord channels. Purchase automation is intentionally off by default: `PRICE_BOT_BUY_AUTOMATION_ENABLED=false` and `PRICE_BOT_BUY_AUTOMATION_MODE=off`. Future retailer-specific adapters can use `prepare`, `auto_cart`, or `auto_buy`, but only under the configured score, price, spend, and retailer allowlist gates.

Facebook Marketplace Discord pushes use `npm run worker:facebook-marketplace:discord` with `PRICE_BOT_FACEBOOK_MARKETPLACE_RAW_LISTINGS_FILE` when the browser extractor writes raw visible cards. For a quick seeded send/dry-run of the current known candidates, use `PRICE_BOT_FACEBOOK_MARKETPLACE_TEST_SEND=true` and optionally `PRICE_BOT_FACEBOOK_MARKETPLACE_DRY_RUN=true`.

Facebook Marketplace monitoring is browser-only because it depends on the user's logged-in Chrome tab. The read-only scan targets live in `config/facebook-marketplace-searches.json`, and listing text is scored by `src/workers/facebook-marketplace-scorer.ts`. It must only browse/search listings and report candidates; it must never message sellers, click contact buttons, save listings, or make offers.

```bash
npm run worker:discord-price:status
npm run worker:discord-price:feedback -- list
npm run worker:discord-price:feedback -- mark <alert_id> good "solid margin"
npm run worker:discord-price:feedback -- summary
npm run worker:facebook-marketplace:self-test
```

```bash
PRICE_BOT_DRY_RUN=true npm run worker:discord-price
PRICE_BOT_TEST_SEND=true npm run worker:discord-price
PRICE_BOT_SELF_TEST=true npm run worker:discord-price
```

---
Expand Down
74 changes: 74 additions & 0 deletions config/argus-procurement-searches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[
{
"id": "camera-basic",
"priority": 1,
"slot": "camera",
"label": "First camera node",
"queries": ["tapo c500", "outdoor rtsp onvif camera", "wifi onvif outdoor camera", "tapo outdoor camera"],
"targetMin": 30,
"targetMax": 45
},
{
"id": "camera-upgraded",
"priority": 1,
"slot": "camera",
"label": "Upgraded camera node",
"queries": ["tapo c520ws", "tapo c520", "outdoor onvif ethernet camera", "rtsp onvif outdoor wifi camera"],
"targetMin": 45,
"targetMax": 70
},
{
"id": "microsd",
"priority": 1,
"slot": "microsd",
"label": "High-endurance microSD",
"queries": ["128gb high endurance microsd", "256gb high endurance microsd", "surveillance microsd card"],
"targetMin": 15,
"targetMax": 30
},
{
"id": "mounting-materials",
"priority": 1,
"slot": "mounting",
"label": "Mounting and weatherproof materials",
"queries": ["weatherproof junction box lot", "camera bracket u bolt lot", "outdoor electrical box cable glands", "satellite dish mount pole"],
"targetMin": 10,
"targetMax": 25
},
{
"id": "technic-electronics",
"priority": 2,
"slot": "technic-electronics",
"label": "Technic hub plus motors",
"queries": ["LEGO Technic hub motor lot", "Powered Up motor lot", "Technic Control+ incomplete set"],
"targetMin": 50,
"targetMax": 75
},
{
"id": "technic-bundle",
"priority": 2,
"slot": "technic-electronics",
"label": "Technic hub, motors, and useful parts",
"queries": ["broken app controlled LEGO Technic", "LEGO Technic hub motors gears", "LEGO robotics parts lot"],
"targetMin": 70,
"targetMax": 110
},
{
"id": "technic-bulk-parts",
"priority": 2,
"slot": "technic-parts",
"label": "Technic bulk gears axles beams",
"queries": ["bulk Technic gears axles beams", "LEGO Technic parts lot gears axles", "Technic liftarms pins connectors lot"],
"targetMin": 20,
"targetMax": 60
},
{
"id": "future-phase",
"priority": 3,
"slot": "future-phase",
"label": "Future ARGUS watchlist",
"queries": ["TP-Link EAP110 Outdoor", "TP-Link CPE210", "RTL-SDR", "Raspberry Pi", "Jetson", "thermal camera", "LiFePO4 battery"],
"targetMin": 1,
"targetMax": 999
}
]
Loading
Loading