# Snipe MVP + circuitforge-core Extraction — Design Spec **Date:** 2026-03-25 **Status:** Approved **Products:** `snipe` (new), `circuitforge-core` (new), `peregrine` (updated) --- ## 1. Overview This spec covers two parallel workstreams: 1. **circuitforge-core extraction** — hoist the shared scaffold from Peregrine into a private, locally-installable Python package. Peregrine becomes the first downstream consumer. All future CF products depend on it. 2. **Snipe MVP** — eBay listing monitor + seller trust scorer, built on top of circuitforge-core. Solves the immediate problem: filtering scam accounts when searching for used GPU listings on eBay. Design principle: *cry once*. Pay the extraction cost now while there are only two products; every product after this benefits for free. --- ## 2. circuitforge-core ### 2.1 Repository - **Repo:** `git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core` (private) - **Local path:** `/Library/Development/CircuitForge/circuitforge-core/` - **Install method:** `pip install -e ../circuitforge-core` (editable local package; graduate to Forgejo Packages private PyPI at product #3) - **License:** BSL 1.1 for AI features, MIT for pipeline/utility layers ### 2.2 Package Structure ``` circuitforge-core/ circuitforge_core/ pipeline/ # SQLite staging DB, status machine, background task runner llm/ # LLM router: fallback chain, BYOK support, vision-aware routing vision/ # Vision model wrapper — moondream2 (local) + Claude vision (cloud) [NET-NEW] wizard/ # First-run onboarding framework, tier gating, crash recovery tiers/ # Tier system (Free/Paid/Premium/Ultra) + Heimdall license client db/ # SQLite base class, migration runner config/ # Settings loader, env validation, secrets management pyproject.toml README.md ``` ### 2.3 Extraction from Peregrine The following Peregrine modules are **extracted** (migrated from Peregrine, not net-new): | Peregrine source | → Core module | Notes | |---|---|---| | `app/wizard/` | `circuitforge_core/wizard/` | | | `scripts/llm_router.py` | `circuitforge_core/llm/router.py` | Path is `scripts/`, not `app/` | | `app/wizard/tiers.py` | `circuitforge_core/tiers/` | | | SQLite pipeline base | `circuitforge_core/pipeline/` | | **`circuitforge_core/vision/`** is **net-new** — no vision module exists in Peregrine to extract. It is built fresh in core. **Peregrine dependency management:** Peregrine uses `requirements.txt`, not `pyproject.toml`. The migration adds `circuitforge-core` to `requirements.txt` as a local path entry: `-e ../circuitforge-core`. Snipe is greenfield and uses `pyproject.toml` from the start. There is no requirement to migrate Peregrine to `pyproject.toml` as part of this work. ### 2.4 Docker Build Strategy Docker build contexts cannot reference paths outside the context directory (`COPY ../` is forbidden). Both Peregrine and Snipe resolve this by setting the compose build context to the parent directory: ```yaml # compose.yml (snipe or peregrine) services: app: build: context: .. # /Library/Development/CircuitForge/ dockerfile: snipe/Dockerfile ``` ```dockerfile # snipe/Dockerfile COPY circuitforge-core/ ./circuitforge-core/ RUN pip install -e ./circuitforge-core COPY snipe/ ./snipe/ RUN pip install -e ./snipe ``` In development, `compose.override.yml` bind-mounts `../circuitforge-core` so local edits to core are immediately live without rebuild. --- ## 3. Snipe MVP ### 3.1 Scope **In (v0.1 MVP):** - eBay listing search (Browse API + Seller API) - Metadata trust scoring (free tier) - Perceptual hash duplicate photo detection within a search result set (free tier) - Faceted filter UI with dynamic, data-driven filter options and sliders - On-demand search only - `SavedSearch` DB schema scaffolded but monitoring not wired up **Out (future versions):** - Background polling / saved search alerts (v0.2) - Photo analysis via vision model — real vs marketing shot, EM bag detection (v0.2, paid) - Serial number consistency check (v0.2, paid) - AI-generated image detection (v0.3, paid) - Reverse image search (v0.4, paid) - Additional platforms: HiBid, CT Bids, AuctionZip (v0.3+) - Bid scheduling / snipe execution (v0.4+) ### 3.2 Repository - **Repo:** `git.opensourcesolarpunk.com/Circuit-Forge/snipe` (public discovery layer) - **Local path:** `/Library/Development/CircuitForge/snipe/` - **License:** MIT (discovery/pipeline), BSL 1.1 (AI features) - **Product code:** `CFG-SNPE` - **Port:** 8506 ### 3.3 Tech Stack Follows Peregrine as the reference implementation: - **UI:** Streamlit (Python) - **DB:** SQLite via `circuitforge_core.db` - **LLM/Vision:** `circuitforge_core.llm` / `circuitforge_core.vision` - **Tiers:** `circuitforge_core.tiers` - **Containerisation:** Docker + `compose.yml`, managed via `manage.sh` - **Python env:** `conda run -n job-seeker` (shared CF env) ### 3.4 Application Structure ``` snipe/ app/ platforms/ __init__.py # PlatformAdapter abstract base class ebay/ adapter.py # eBay Browse API + Seller API client auth.py # OAuth2 client credentials token manager normaliser.py # Raw API response → Listing / Seller schema trust/ __init__.py # TrustScorer orchestrator metadata.py # Account age, feedback, price vs market, category history photo.py # Perceptual hash dedup (free); vision analysis (paid, v0.2+) aggregator.py # Weighted composite score + red flag extraction ui/ Search.py # Main search + results page components/ filters.py # Dynamic faceted filter sidebar listing_row.py # Listing card with trust badge + red flags + error state db/ models.py # Listing, Seller, Search, TrustScore, SavedSearch schemas migrations/ wizard/ # First-run onboarding (thin wrapper on core wizard) snipe/ # Bid engine placeholder (v0.4) manage.sh compose.yml compose.override.yml Dockerfile pyproject.toml ``` ### 3.5 eBay API Credentials eBay Browse API and Seller API require OAuth 2.0 app-level tokens (client credentials flow — no user auth needed, but a registered eBay developer account and app credentials are required). **Token lifecycle:** - App token fetched at startup and cached in memory with expiry - `auth.py` handles refresh automatically on expiry (tokens last 2 hours) - On token fetch failure: search fails with a user-visible error; no silent fallback **Credentials storage:** `.env` file (gitignored), never hardcoded. ``` EBAY_CLIENT_ID=... EBAY_CLIENT_SECRET=... EBAY_ENV=production # or sandbox ``` **Rate limits:** eBay Browse API — 5,000 calls/day (sandbox), higher on production. Completed sales comps results are cached in SQLite with a 6-hour TTL to avoid redundant calls and stay within limits. Cache miss triggers a fresh fetch; fetch failure degrades gracefully (price vs market signal skipped, score noted as partial). **API split:** `get_seller()` uses the eBay Seller API (different endpoint, same app token). Rate limits are tracked separately. The `PlatformAdapter` interface does not expose this distinction; it is an internal concern of the eBay adapter. ### 3.6 Data Model **`Listing`** ``` id, platform, platform_listing_id, title, price, currency, condition, seller_id, url, photo_urls (JSON), listing_age_days, fetched_at, trust_score_id ``` **`Seller`** ``` id, platform, platform_seller_id, username, account_age_days, feedback_count, feedback_ratio, category_history_json, fetched_at ``` **`TrustScore`** ``` id, listing_id, composite_score, account_age_score, feedback_count_score, feedback_ratio_score, price_vs_market_score, category_history_score, photo_hash_duplicate (bool), photo_analysis_json (paid, nullable), red_flags_json, scored_at, score_is_partial (bool) ``` **`MarketComp`** *(price comps cache)* ``` id, platform, query_hash, median_price, sample_count, fetched_at, expires_at ``` **`SavedSearch`** *(schema scaffolded in v0.1; monitoring not wired until v0.2)* ``` id, name, query, platform, filters_json, created_at, last_run_at ``` **`PhotoHash`** *(perceptual hash store for cross-search dedup, v0.2+)* ``` id, listing_id, photo_url, phash, first_seen_at ``` ### 3.7 Platform Adapter Interface ```python class PlatformAdapter: def search(self, query: str, filters: SearchFilters) -> list[Listing]: ... def get_seller(self, seller_id: str) -> Seller: ... def get_completed_sales(self, query: str) -> list[Listing]: ... ``` Adding HiBid or CT Bids later = new adapter, zero changes to trust scorer or UI. ### 3.8 Trust Scorer #### Metadata Signals (Free) Five signals, each scored 0–20, equal weight. Composite = sum (0–100). | Signal | Source | Red flag threshold | Score 0 condition | |---|---|---|---| | Account age | eBay Seller API | < 30 days | < 7 days (also hard-filter) | | Feedback count | eBay Seller API | < 10 | < 3 | | Feedback ratio | eBay Seller API | < 95% | < 80% with count > 20 | | Price vs market | Completed sales comps | > 30% below median | > 50% below median | | Category history | Seller past sales | No prior electronics sales | No prior sales at all | **Hard filters** (auto-hide regardless of composite score): - Account age < 7 days - Feedback ratio < 80% with feedback count > 20 **Partial scores:** If any signal's data source is unavailable (API failure, rate limit), that signal contributes 0 and `score_is_partial = True` is set on the `TrustScore` record. The UI surfaces a "⚠ Partial score" indicator on affected listings. #### Photo Signals — Anti-Gotcha Layer | Signal | Tier | Version | Method | |---|---|---|---| | Perceptual hash dedup within result set | **Free** | v0.1 MVP | Compare phashes across all listings in the current search response; flag duplicates | | Real photo vs marketing shot | **Paid / Local vision** | v0.2 | Vision model classification | | Open box + EM antistatic bag (proof of possession) | **Paid / Local vision** | v0.2 | Vision model classification | | Serial number consistency across photos | **Paid / Local vision** | v0.2 | Vision model OCR + comparison | | AI-generated image detection | **Paid** | v0.3 | Classifier model | | Reverse image search | **Paid** | v0.4 | Google Lens / TinEye API | **v0.1 dedup scope:** Perceptual hash comparison is within the current search result set only (not across historical searches). Cross-session dedup uses the `PhotoHash` table and is a v0.2 feature. Photos are not downloaded to disk in v0.1 — hashes are computed from the image bytes in memory during the search request. ### 3.9 Tier Gating Photo analysis features use `LOCAL_VISION_UNLOCKABLE` (analogous to `BYOK_UNLOCKABLE` in Peregrine's `tiers.py`) — they unlock for free-tier users who have a local vision model (moondream2) configured. This is distinct from BYOK (text LLM key), which does not unlock vision features. | Feature | Free | Paid | Local vision unlock | |---|---|---|---| | Metadata trust scoring | ✓ | ✓ | — | | Perceptual hash dedup (within result set) | ✓ | ✓ | — | | Photo analysis (real/marketing/EM bag) | — | ✓ | ✓ | | Serial number consistency | — | ✓ | ✓ | | AI generation detection | — | ✓ | — | | Reverse image search | — | ✓ | — | | Saved searches + background monitoring | — | ✓ | — | Locked features are shown (disabled) in the filter sidebar so free users see what's available. Clicking a locked filter shows a tier upgrade prompt. ### 3.10 UI — Results Page **Search bar:** keywords, max price, condition selector, search button. Sort: trust score (default), price ↑/↓, listing age. **Filter sidebar** — all options and counts generated dynamically from the result set. Options with 0 results are hidden (not greyed): - Trust score — range slider (min/max from results); colour-band summary (safe/review/skip + counts) - Price — min/max text inputs + market avg/median annotation - Seller account age — min slider - Feedback count — min slider - Positive feedback % — min slider - Condition — checkboxes (options from data: New, Open Box, Used, For Parts) - Photo signals — checkboxes: Real photo, EM bag visible, Open box, No AI-generated (locked, paid) - Hide if flagged — checkboxes: New account (<30d), Marketing photo, >30% below market, Duplicate photo - Shipping — Free shipping, Local pickup - Reset filters button **Listing row (happy path):** thumbnail · title · seller summary (username, feedback count, ratio, tenure) · red flag badges · trust score badge (colour-coded: green 80+, amber 50–79, red <50) · `score_is_partial` indicator if applicable · price · "Open eBay ↗" link. Left border colour matches score band. **Listing row (error states):** - Seller data unavailable: seller summary shows "Seller data unavailable" in muted text; affected signals show "–" and partial score indicator is set - Photo URL 404: thumbnail shows placeholder icon; hash dedup skipped for that photo - Trust scoring failed entirely: listing shown with score "?" badge in neutral grey; error logged; "Could not score this listing" tooltip **Hidden results:** count shown at bottom ("N results hidden by filters · show anyway"). Clicking reveals them in-place at reduced opacity. --- ## 4. Build Order 1. **circuitforge-core** — scaffold repo, extract wizard/llm/tiers/pipeline from Peregrine, build vision module net-new, update Peregrine `requirements.txt` 2. **Snipe scaffold** — repo init, Dockerfile, compose.yml (parent context), manage.sh, DB migrations, wizard first-run, `.env` template 3. **eBay adapter** — OAuth2 token manager, Browse API search, Seller API, completed sales comps with cache 4. **Metadata trust scorer** — all five signals, aggregator, hard filters, partial score handling 5. **Perceptual hash dedup** — in-memory within-result-set comparison 6. **Results UI** — search page, listing rows (happy + error states), dynamic filter sidebar 7. **Tier gating** — lock photo signals, `LOCAL_VISION_UNLOCKABLE` gate, upsell prompts in UI --- ## 5. Documentation Locations - Product spec: `snipe/docs/superpowers/specs/2026-03-25-snipe-circuitforge-core-design.md` *(this file)* - Internal copy: `circuitforge-plans/snipe/2026-03-25-snipe-circuitforge-core-design.md` - Roadmap: `Circuit-Forge/roadmap` issues #14 (snipe) and #21 (circuitforge-core) - Org-level context: `/Library/Development/CircuitForge/CLAUDE.md`