feat: infra/devops batch — CI/CD, installer, nginx docs, cf-orch agent (v0.3.0)
Some checks failed
CI / Frontend typecheck + tests (push) Waiting to run
CI / Python tests (push) Waiting to run
Mirror / mirror (push) Has been cancelled
Release / release (push) Has been cancelled

Closes #15, #22, #24, #25. Closes #1 and #27 (already shipped in 0.2.0).

## CI/CD (#22)
- .forgejo/workflows/ci.yml — Python lint (ruff) + pytest + Vue typecheck + vitest
  on every PR/push. Installs cf-core from GitHub mirror for the CI runner.
- .forgejo/workflows/release.yml — Docker build/push (api + web) to Forgejo registry
  on v* tags; git-cliff changelog; multi-arch amd64+arm64.
- .forgejo/workflows/mirror.yml — push to GitHub + Codeberg mirrors.

## Self-hosted installer (#25)
- install.sh rewritten to match CF installer pattern: coloured output, named
  functions, --docker / --bare-metal / --help flags, auto-detect Docker/conda/
  Python/Node/Chromium/Xvfb, license key prompting with format validation.

## Nginx docs (#24)
- docs/nginx-self-hosted.conf — sample nginx config: SPA fallback, SSE proxy
  (proxy_buffering off), long-term asset cache headers.
- docs/getting-started/installation.md — bare-metal install section with nginx
  setup, Chromium/Xvfb note, serve-ui.sh vs nginx trade-off.

## cf-orch agent (#15)
- compose.override.yml — cf-orch-agent sidecar service (profiles: [orch]).
  Starts only with docker compose --profile orch. Registers with coordinator at
  CF_ORCH_COORDINATOR_URL (default 10.1.10.71:7700).
- .env.example — CF_ORCH_URL / CF_ORCH_COORDINATOR_URL comments expanded.

## Docs
- mkdocs.yml + full docs/ tree (getting-started, reference, user-guide) staged
  from prior session work.

Bump version 0.2.0 → 0.3.0.
This commit is contained in:
pyr0ball 2026-04-14 06:19:25 -07:00
parent 6d5ceac0a1
commit 2dda26a911
19 changed files with 1101 additions and 148 deletions

View file

@ -72,9 +72,15 @@ SNIPE_DB=data/snipe.db
# OLLAMA_HOST=http://localhost:11434 # OLLAMA_HOST=http://localhost:11434
# OLLAMA_MODEL=llava:7b # OLLAMA_MODEL=llava:7b
# CF Orchestrator — managed inference for Paid+ cloud users (internal use only). # CF Orchestrator — routes vision/LLM tasks to a cf-orch coordinator for VRAM management.
# Self-hosted users leave this unset; it has no effect without a valid allocation token. # Self-hosted: point at a local cf-orch coordinator if you have one running.
# CF_ORCH_URL=https://orch.circuitforge.tech # Cloud (internal): managed coordinator at orch.circuitforge.tech.
# Leave unset to run vision tasks inline (no VRAM coordination).
# CF_ORCH_URL=http://10.1.10.71:7700
#
# cf-orch agent (compose --profile orch) — coordinator URL for the sidecar agent.
# Defaults to CF_ORCH_URL if unset.
# CF_ORCH_COORDINATOR_URL=http://10.1.10.71:7700
# ── In-app feedback (beta) ──────────────────────────────────────────────────── # ── In-app feedback (beta) ────────────────────────────────────────────────────
# When set, a feedback FAB appears in the UI and routes submissions to Forgejo. # When set, a feedback FAB appears in the UI and routes submissions to Forgejo.

View file

@ -6,6 +6,36 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
--- ---
## [0.3.0] — 2026-04-14
### Added
**Infrastructure and DevOps**
- `.forgejo/workflows/ci.yml` — Python lint (ruff) + pytest + Vue typecheck + vitest on every PR/push to main. Installs circuitforge-core from GitHub mirror so the CI runner doesn't need the sibling directory.
- `.forgejo/workflows/release.yml` — Docker build and push (api + web images) to Forgejo container registry on `v*` tags. Builds both images multi-arch (amd64 + arm64). Creates a Forgejo release with git-cliff changelog notes.
- `.forgejo/workflows/mirror.yml` — Mirror push to GitHub and Codeberg on main/tags.
- `install.sh` — Full rewrite following the CircuitForge installer pattern: colored output, `--docker` / `--bare-metal` / `--help` flags, auto-detection of Docker/conda/Python/Node/Chromium/Xvfb, license key prompting, structured named functions.
- `docs/nginx-self-hosted.conf` — Sample nginx config for bare-metal self-hosted deployments (SPA fallback, SSE proxy settings, long-term asset caching).
- `docs/getting-started/installation.md` — No-Docker install section: bare-metal instructions, nginx setup, Chromium/Xvfb note.
- `compose.override.yml``cf-orch-agent` sidecar service for routing vision tasks to a cf-orch GPU coordinator (`--profile orch` opt-in). `CF_ORCH_COORDINATOR_URL` env var documented.
- `.env.example``CF_ORCH_URL` and `CF_ORCH_COORDINATOR_URL` comments expanded with self-hosted coordinator guidance.
**Screenshots** (post CSS fix)
- Retook all docs screenshots (`01-hero`, `02-results`, `03-steal-badge`, `hero`) after the color-mix token fix so tints match the theme in both dark and light mode.
### Closed
- `#1` SSE live score push — already fully implemented in 0.2.0; closed.
- `#22` Forgejo Actions CI/CD — shipped.
- `#24` nginx config for no-Docker self-hosting — shipped.
- `#25` Self-hosted installer script — shipped.
- `#15` cf-orch agent in compose stack — shipped.
- `#27` MCP server — already shipped in 0.2.0; closed.
---
## [0.2.0] — 2026-04-12 ## [0.2.0] — 2026-04-12
### Added ### Added

View file

@ -4,7 +4,9 @@
# What this adds over compose.yml: # What this adds over compose.yml:
# - Live source mounts so code changes take effect without rebuilding images # - Live source mounts so code changes take effect without rebuilding images
# - RELOAD=true to enable uvicorn --reload for the API # - RELOAD=true to enable uvicorn --reload for the API
# - NOTE: circuitforge-core is NOT mounted here — use `./manage.sh build` to # - cf-orch-agent sidecar for local GPU task routing (opt-in: --profile orch)
#
# NOTE: circuitforge-core is NOT mounted here — use `./manage.sh build` to
# pick up cf-core changes. Mounting it as a bind volume would break self-hosted # pick up cf-core changes. Mounting it as a bind volume would break self-hosted
# installs that don't have the sibling directory. # installs that don't have the sibling directory.
services: services:
@ -15,3 +17,32 @@ services:
- ./tests:/app/snipe/tests - ./tests:/app/snipe/tests
environment: environment:
- RELOAD=true - RELOAD=true
# Point the LLM/vision task scheduler at the local cf-orch coordinator.
# Only has effect when CF_ORCH_URL is set (uncomment in .env, or set inline).
# - CF_ORCH_URL=http://10.1.10.71:7700
# cf-orch agent — routes trust_photo_analysis vision tasks to the GPU coordinator.
# Only starts when you pass --profile orch:
# docker compose --profile orch up
#
# Requires a running cf-orch coordinator. Default: Heimdall at 10.1.10.71:7700.
# Override via CF_ORCH_COORDINATOR_URL in .env.
#
# To use a locally-built cf-orch image instead of the published one:
# build:
# context: ../circuitforge-orch
# dockerfile: Dockerfile
cf-orch-agent:
image: ghcr.io/circuitforgellc/cf-orch:latest
command: >
agent
--coordinator ${CF_ORCH_COORDINATOR_URL:-http://10.1.10.71:7700}
--node-id snipe-dev
--host 0.0.0.0
--port 7701
--advertise-host 127.0.0.1
environment:
CF_COORDINATOR_URL: ${CF_ORCH_COORDINATOR_URL:-http://10.1.10.71:7700}
restart: on-failure
profiles:
- orch

View file

@ -0,0 +1,39 @@
# eBay API Keys (Optional)
Snipe works without any credentials using its Playwright scraper fallback. Adding eBay API credentials unlocks faster searches and higher rate limits.
## What API keys enable
| Feature | Without keys | With keys |
|---------|-------------|-----------|
| Listing search | Playwright scraper | eBay Browse API (faster, higher limits) |
| Market comps (completed sales) | Not available | eBay Marketplace Insights API |
| Seller account data | BTF scraper (Xvfb) | BTF scraper (same — eBay API doesn't expose join date) |
## Getting credentials
1. Create a developer account at [developer.ebay.com](https://developer.ebay.com/my/keys)
2. Create a new application (choose **Production**)
3. Copy your **App ID (Client ID)** and **Cert ID (Client Secret)**
## Configuration
Add your credentials to `.env`:
```bash
EBAY_APP_ID=YourAppID-...
EBAY_CERT_ID=YourCertID-...
```
Then restart:
```bash
./manage.sh restart
```
## Verifying
After restart, the search bar shows **API** as available in the data source selector. The auto mode will use the API by default.
!!! note
The Marketplace Insights API (for completed sales comps) requires an approved eBay developer account. New accounts may not have access. Snipe gracefully falls back to Browse API results when Insights returns 403 or 404.

View file

@ -0,0 +1,102 @@
# Installation
## Requirements
- Docker with Compose plugin
- Git
- No API keys required to get started
## One-line install
```bash
bash <(curl -fsSL https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/raw/branch/main/install.sh)
```
This clones the repo to `~/snipe` and starts the stack. Open **http://localhost:8509** when it completes.
## Manual install
Snipe's API image is built from a context that includes `circuitforge-core`. Both repos must sit as siblings:
```
workspace/
├── snipe/ ← this repo
└── circuitforge-core/ ← required sibling
```
```bash
mkdir snipe-workspace && cd snipe-workspace
git clone https://git.opensourcesolarpunk.com/Circuit-Forge/snipe.git
git clone https://git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core.git
cd snipe
cp .env.example .env
./manage.sh start
```
## Managing the stack
```bash
./manage.sh start # build and start all containers
./manage.sh stop # stop containers
./manage.sh restart # rebuild and restart
./manage.sh status # container health
./manage.sh logs # tail logs
./manage.sh open # open in browser
```
## Updating
```bash
git pull
./manage.sh restart
```
## Ports
| Service | Default port |
|---------|-------------|
| Web UI | 8509 |
| API | 8510 |
Both ports are configurable in `.env`.
---
## No-Docker install (bare metal)
Run `install.sh --bare-metal` to skip Docker and install via conda or venv instead.
This sets up the Python environment, builds the Vue frontend, and writes helper scripts.
**Requirements:** Python 3.11+, Node.js 20+, `xvfb` (for the eBay scraper).
```bash
bash <(curl -fsSL https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/raw/branch/main/install.sh) --bare-metal
```
After install, you get two scripts:
| Script | What it does |
|--------|-------------|
| `./start-local.sh` | Start the FastAPI API on port 8510 |
| `./serve-ui.sh` | Serve the built frontend with `python3 -m http.server 8509` (dev only) |
`serve-ui.sh` is single-threaded and suitable for testing only. For a real deployment, use nginx.
### nginx config (production bare-metal)
Install nginx, copy the sample config, and reload:
```bash
sudo cp docs/nginx-self-hosted.conf /etc/nginx/sites-available/snipe
sudo ln -s /etc/nginx/sites-available/snipe /etc/nginx/sites-enabled/snipe
# Edit the file — update `root` to your actual web/dist path
sudo nginx -t && sudo systemctl reload nginx
```
See [`docs/nginx-self-hosted.conf`](../nginx-self-hosted.conf) for the full config with TLS notes.
### Chromium / Xvfb note
Snipe uses headed Chromium via Xvfb to bypass Kasada (the anti-bot layer on eBay seller profile pages). If Chromium is not detected, the scraper falls back to the eBay Browse API — add `EBAY_APP_ID` / `EBAY_CERT_ID` to `.env` so that fallback has credentials.
The installer detects and installs Xvfb automatically on Debian/Ubuntu/Fedora. Chromium is installed via `playwright install chromium`. macOS is not supported for the scraper path.

View file

@ -0,0 +1,39 @@
# Quick Start
## 1. Run a search
Type a query into the search bar and press **Search** or hit Enter.
!!! tip
Start broad (`vintage camera`) then narrow with keyword filters once you see results. The must-include and must-exclude fields let you refine without re-searching from scratch.
## 2. Read the trust badge
Each listing card shows a trust badge in the top-right corner:
| Badge | Meaning |
|-------|---------|
| Green (70100) | Established seller, no major concerns |
| Yellow (4069) | Some signals missing or marginal |
| Red (039) | Multiple red flags — proceed carefully |
| `STEAL` label | Price significantly below market median |
A spinning indicator below the badge means enrichment is still in progress (account age is being fetched). Scores update automatically when enrichment completes.
## 3. Check red flags
Red flag pills appear below the listing title when Snipe detects a concern. Hover or tap a flag for a plain-language explanation.
## 4. Click through to eBay
Listing titles link directly to eBay. In cloud mode, links include an affiliate code that supports Snipe's development at no cost to you. You can opt out in Settings.
## 5. Filter results
Use the sidebar filters to narrow results without re-running the eBay search:
- **Min trust score** — slider to hide low-confidence listings
- **Min account age / Min feedback** — hide new or low-volume sellers
- **Hide listings checkboxes** — hide new accounts, suspicious prices, duplicate photos, damage mentions, long-on-market, significant price drop
These filters apply instantly to the current result set. Use the search bar to change the underlying eBay query.

33
docs/index.md Normal file
View file

@ -0,0 +1,33 @@
# Snipe
**eBay trust scoring before you bid.**
![Snipe landing hero](screenshots/01-hero.png)
Snipe scores eBay listings and sellers for trustworthiness before you place a bid. Paste a search query, get results with trust scores, and know exactly which listings are worth your time.
## What it catches
- **New accounts** selling high-value items with no track record
- **Suspicious prices** — listings priced far below completed sales
- **Duplicate photos** — images copy-pasted from other listings (perceptual hash deduplication)
- **Damage buried in titles** — scratch, dent, untested, for parts, and similar
- **Known bad actors** — sellers on the community blocklist
## How it works
![Search results with trust scores](screenshots/02-results.png)
Each listing gets a composite trust score from 0100 based on five seller signals: account age, feedback count, feedback ratio, price vs. market, and category history. Red flags are surfaced alongside the score, not buried in it.
## Free, no account required
Search and scoring work without creating an account. Community features (reporting sellers, importing blocklists) require a free account.
## Quick links
- [Installation](getting-started/installation.md)
- [Understanding trust scores](user-guide/trust-scores.md)
- [Red flags reference](user-guide/red-flags.md)
- [Cloud demo](https://menagerie.circuitforge.tech/snipe)
- [Source code](https://git.opensourcesolarpunk.com/Circuit-Forge/snipe)

View file

@ -0,0 +1,58 @@
# nginx config for Snipe — bare-metal self-hosted (no Docker).
#
# Usage:
# sudo cp docs/nginx-self-hosted.conf /etc/nginx/sites-available/snipe
# # Edit: update `root` to your actual web/dist path and `server_name` to your hostname
# sudo ln -s /etc/nginx/sites-available/snipe /etc/nginx/sites-enabled/snipe
# sudo nginx -t && sudo systemctl reload nginx
#
# Assumes:
# - The Snipe FastAPI API is running on 127.0.0.1:8510 (./start-local.sh)
# - The Vue frontend was built by install.sh into web/dist/
# - TLS termination is handled separately (Caddy, certbot, or upstream proxy)
#
# For TLS with Let's Encrypt, run:
# sudo certbot --nginx -d your.domain.com
# Certbot will add the ssl_certificate lines automatically.
server {
listen 80;
server_name your.domain.com; # replace or use _ for catch-all
# Path to the Vue production build — update to match your install directory
root /home/youruser/snipe/snipe/web/dist;
index index.html;
# Proxy all /api/ requests to the FastAPI backend
location /api/ {
proxy_pass http://127.0.0.1:8510;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE (Server-Sent Events) — live trust score updates
# These are long-lived streaming responses; disable buffering.
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 120s;
}
# index.html — never cache; ensures clients always get the latest entry point
# after a deployment (JS/CSS chunks are content-hashed so they cache forever)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri /index.html;
}
# SPA fallback — all unknown paths serve index.html so Vue Router handles routing
location / {
try_files $uri $uri/ /index.html;
}
# Long-term cache for content-hashed static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View file

@ -0,0 +1,66 @@
# Architecture
## Stack
| Layer | Technology |
|-------|-----------|
| Frontend | Vue 3, Pinia, UnoCSS |
| API | FastAPI (Python), host networking |
| Database | SQLite (WAL mode) |
| Scraper | Playwright + Chromium + Xvfb |
| Container | Docker Compose |
## Data flow
```mermaid
graph LR
User -->|search query| VueSPA
VueSPA -->|GET /api/search| FastAPI
FastAPI -->|Browse API or Playwright| eBay
eBay --> FastAPI
FastAPI -->|score_batch| TrustScorer
TrustScorer --> FastAPI
FastAPI -->|BTF enrich queue| XvfbScraper
XvfbScraper -->|seller profile| eBayProfile
eBayProfile --> XvfbScraper
XvfbScraper -->|account_age update| SQLite
FastAPI -->|SSE push| VueSPA
```
## Database layout
Snipe uses two SQLite databases in cloud mode:
| Database | Contents |
|----------|---------|
| `shared.db` | Sellers, listings, market comps, community signals, scammer blocklist |
| `user.db` | Trust scores, saved searches, user preferences, background tasks |
In local (self-hosted) mode, everything uses a single `snipe.db`.
WAL (Write-Ahead Logging) mode is enabled on all connections for concurrent reader safety.
## Seller enrichment pipeline
eBay's Browse API returns listings without seller account ages. Snipe fetches account ages by loading the seller's eBay profile page in a headed Chromium instance via Xvfb.
Each enrichment session uses a unique Xvfb display number (`:200``:299`, cycling) to prevent lock file collisions across parallel sessions. Kasada bot protection blocks headless Chrome and curl-based requests — only a full headed browser session passes.
## Affiliate URL wrapping
All listing URLs are wrapped with an eBay Partner Network (EPN) affiliate code before being returned to the frontend. Resolution order:
1. User opted out → plain URL
2. User has BYOK EPN ID (Premium) → wrap with user's ID
3. CF affiliate ID configured in `.env` → wrap with CF's ID
4. Not configured → plain URL
## Licensing
| Layer | License |
|-------|---------|
| Discovery pipeline (scraper, trust scoring, search) | MIT |
| AI features (photo analysis, description reasoning) | BSL 1.1 |
| Fine-tuned model weights | Proprietary |
BSL 1.1 is free for personal non-commercial self-hosting. SaaS re-hosting requires a commercial license. Converts to MIT after 4 years.

View file

@ -0,0 +1,32 @@
# Tier System
Snipe uses Circuit Forge's standard four-tier model.
## Tiers
| Tier | Price | Key features |
|------|-------|-------------|
| **Free** | Free | Search, trust scoring, red flags, blocklist, market comps, affiliate links, saved searches |
| **Paid** | $4.99/mo or $129 lifetime | Photo analysis, background monitoring (up to 5 searches), serial number check |
| **Premium** | $9.99/mo or $249 lifetime | All Paid features, background monitoring (up to 25), custom affiliate ID (BYOK EPN) |
| **Ultra** | Contact us | Human-in-the-loop assistance |
## Free tier philosophy
Snipe's core trust-scoring pipeline — the part that actually catches scammers — is entirely free and requires no account. This is intentional.
More users = more community blocklist data = better protection for everyone. The free tier drives the network effect that makes the paid features more valuable.
## Self-hosted
Running Snipe yourself? All features are available with no tier gates in local mode. Bring your own LLM (Ollama compatible) to unlock photo analysis and description reasoning on your own hardware.
## BYOK (Bring Your Own Key)
Premium subscribers can supply:
- **Local LLM endpoint** — any OpenAI-compatible server (Ollama, vLLM, LM Studio) unlocks AI features on Free tier
- **eBay Partner Network campaign ID** — your affiliate revenue instead of Snipe's
## Cloud trial
15-day free trial of Paid tier on first signup. No credit card required.

View file

@ -0,0 +1,84 @@
# Trust Score Algorithm
## Signal scoring
Each signal contributes 020 points to the composite score.
### account_age
| Days old | Score |
|----------|-------|
| < 7 | 0 (triggers `new_account` hard flag) |
| 730 | 5 |
| 3090 | 10 |
| 90365 | 15 |
| > 365 | 20 |
Data source: eBay profile page (BTF scraper via headed Chromium + Xvfb — eBay API does not expose account registration date).
### feedback_count
| Count | Score |
|-------|-------|
| 0 | 0 (triggers `zero_feedback` hard flag, score capped at 35) |
| 19 | 5 |
| 1049 | 10 |
| 50199 | 15 |
| 200+ | 20 |
### feedback_ratio
| Ratio | Score |
|-------|-------|
| < 80% (with 20+ reviews) | 0 (triggers `established_bad_actor`) |
| < 90% | 5 |
| 9094% | 10 |
| 9598% | 15 |
| 99100% | 20 |
### price_vs_market
Compares listing price to the median of recent completed sales from eBay Marketplace Insights API.
| Price vs. median | Score |
|-----------------|-------|
| < 40% | 0 (triggers `suspicious_price` flag) |
| 4059% | 5 |
| 6079% | 10 |
| 80120% | 20 (normal range) |
| 121149% | 15 |
| 150%+ | 10 |
`suspicious_price` flag is suppressed when the market price distribution is too wide (standard deviation > 50% of median) — this prevents false positives on heterogeneous search results.
When no market data is available, this signal returns `None` and is excluded from the composite.
### category_history
Derived from the seller's recent listing history (categories of their sold items):
| Result | Score |
|--------|-------|
| Seller has history in this category | 20 |
| Seller sells cross-category (generalist) | 10 |
| No category history available | None (excluded from composite) |
## Composite calculation
```
composite = (sum of available signal scores) / (20 × count of available signals) × 100
```
This ensures missing signals don't penalize a seller — only available signals count toward the denominator.
## Zero-feedback cap
When `feedback_count == 0`, the composite is hard-capped at **35** after the standard calculation. A 0-feedback seller cannot score above 35 regardless of other signals.
## Partial scores
A score is marked **partial** when one or more signals are `None` (not yet available). The score is recalculated and the partial flag is cleared when enrichment completes.
## Red flag override
Red flags are evaluated independently of the composite score. A seller can have a high composite score and still trigger red flags — for example, a long-established seller with a suspicious-priced listing and duplicate photos.

View file

@ -0,0 +1,34 @@
# Community Blocklist
The blocklist is a shared database of sellers flagged by Snipe users. When a blocklisted seller appears in search results, their listing card is marked with an `established_bad_actor` flag.
## Viewing the blocklist
Navigate to **Blocklist** in the sidebar to see all reported sellers, with usernames, platforms, and optional reasons.
## Reporting a seller
On any listing card, click the **Block** button (shield icon) to report the seller. You can optionally add a reason (e.g. "sent counterfeit item", "never shipped").
!!! note
In cloud mode, blocking requires a signed-in account. Anonymous users can view the blocklist but cannot report sellers.
## Importing a blocklist
The Blocklist view has an **Import CSV** button. The accepted format:
```csv
platform,platform_seller_id,username,reason
ebay,seller123,seller123,counterfeit item
ebay,badactor99,badactor99,
```
The `reason` column is optional. `platform` defaults to `ebay` if omitted.
## Exporting the blocklist
Click **Export CSV** in the Blocklist view to download the current blocklist. Use this to back up, share with others, or import into another Snipe instance.
## Blocklist sync (roadmap)
Batch reporting to eBay's Trust & Safety team is on the roadmap (issue #4). This would allow community-flagged sellers to be reported directly to eBay from within Snipe.

View file

@ -0,0 +1,58 @@
# Red Flags
Red flags appear as pills on listing cards when Snipe detects a concern. Each flag is independent — a listing can have multiple flags at once.
## Hard red flags
These override the composite score display with a strong visual warning.
### `zero_feedback`
Seller has received zero feedback. Score is capped at 35.
### `new_account`
Account registered within the last 7 days. Extremely high fraud indicator for high-value listings.
### `established_bad_actor`
Feedback ratio below 80% with 20 or more reviews. A sustained pattern of negative feedback from an established seller.
## Soft flags
Shown as warnings — not automatic disqualifiers, but worth investigating.
### `account_under_30_days`
Account is less than 30 days old. Less severe than `new_account` but worth noting for high-value items.
### `low_feedback_count`
Fewer than 10 feedback ratings total. Seller is new to eBay or rarely transacts.
### `suspicious_price`
Listing price is more than 50% below the market median from recent completed sales.
!!! note
This flag is suppressed automatically when the search returns a heterogeneous price range — for example, a search that mixes laptop generations spanning $200$2,000. In that case, the median is not meaningful and flagging would produce false positives.
### `duplicate_photo`
The same image (by perceptual hash) appears on another listing. Common in scams where photos are lifted from legitimate listings.
### `scratch_dent_mentioned`
The title or description contains keywords indicating cosmetic damage, functional problems, or evasive language:
- Damage: *scratch, dent, crack, chip, broken, damaged*
- Functional: *untested, for parts, parts only, as-is, not working*
- Evasive: *read description, see description, sold as-is*
### `long_on_market`
The listing has been seen 5 or more times over 14 or more days without selling. A listing that isn't moving may be overpriced or have undisclosed problems.
### `significant_price_drop`
The current price is more than 20% below the price when Snipe first saw this listing. Sudden drops can indicate seller desperation — or a motivated seller — depending on context.
## Triple Red
When a listing hits all three of these simultaneously:
- `new_account` OR `account_under_30_days`
- `suspicious_price`
- `duplicate_photo` OR `zero_feedback` OR `established_bad_actor` OR `scratch_dent_mentioned`
The card gets a **pulsing red border glow** to make it impossible to miss in a crowded results grid.

View file

@ -0,0 +1,56 @@
# Searching
## Basic search
Type a query and press **Search**. Snipe fetches listings from eBay and scores each seller in parallel.
Result count depends on the **Pages to fetch** setting (1 page = up to 200 listings). More pages means a more complete picture but a longer wait.
## Keyword modes
The must-include field has three modes:
| Mode | Behavior |
|------|---------|
| **All** | Every term must appear in results (eBay AND search) |
| **Any** | At least one term must appear (eBay OR search) |
| **Groups** | Comma-separated groups, each searched separately and merged |
Groups mode is the most powerful. Use it to search for variations that eBay's relevance ranking might drop:
```
16gb, 32gb
RTX 4090, 4090 founders
```
This sends two separate eBay queries and deduplicates the results by listing ID.
## Must-exclude
Terms in the must-exclude field are forwarded to eBay on re-search. Common uses:
```
broken, parts only, for parts, untested, cracked
```
!!! note
Must-exclude applies on re-search (it goes to eBay). The **Hide listings: Scratch/dent mentioned** sidebar filter applies instantly to current results using Snipe's own detection logic, which is more comprehensive than eBay's keyword exclusion.
## Filters sidebar
The sidebar has two sections:
**eBay Search** — settings forwarded to eBay on re-search:
- Category filter
- Price range (min/max)
- Pages to fetch
- Data source (Auto / API / Scraper)
**Filter Results** — applied instantly to current results:
- Min trust score slider
- Min account age / Min feedback count
- Hide listings checkboxes
## Saved searches
Click the bookmark icon next to the Search button to save a search with its current filter settings. Saved searches appear in the **Saved** view and can be re-run with one click, restoring all filters.

View file

@ -0,0 +1,25 @@
# Settings
Navigate to **Settings** in the sidebar to access preferences.
## Community
### Trust score feedback
Shows "This score looks right / wrong" buttons on each listing card. Your feedback is recorded anonymously and used to improve trust scoring for all users.
This is opt-in and enabled by default.
## Affiliate Links (cloud accounts only)
### Opt out of affiliate links
When enabled, listing links go directly to eBay without an affiliate code. Your purchases won't generate revenue for Snipe's development.
By default, Snipe includes an affiliate code in eBay links at no cost to you — you pay the same price either way.
### Custom affiliate ID (Premium)
Premium subscribers can supply their own eBay Partner Network (EPN) campaign ID. When set, your eBay purchases through Snipe links generate revenue for your own EPN account instead of Snipe's.
This requires an active EPN account at [partnernetwork.ebay.com](https://partnernetwork.ebay.com).

View file

@ -0,0 +1,39 @@
# Trust Scores
## How scoring works
Each listing gets a composite trust score from 0100, built from five signals:
| Signal | Max points | What it measures |
|--------|-----------|-----------------|
| `account_age` | 20 | Days since the seller's eBay account was registered |
| `feedback_count` | 20 | Total feedback received (volume proxy for experience) |
| `feedback_ratio` | 20 | Percentage of positive feedback |
| `price_vs_market` | 20 | How the listing price compares to recent completed sales |
| `category_history` | 20 | Whether the seller has a history in this item category |
The composite score is the sum of available signals divided by the maximum possible from available signals. Missing signals don't penalize the seller — they reduce the max rather than adding a zero.
## Score bands
| Score | Label | Meaning |
|-------|-------|---------|
| 70100 | Green | Established seller, no major concerns |
| 4069 | Yellow | Some signals marginal or missing |
| 039 | Red | Multiple red flags — proceed carefully |
## Zero-feedback cap
A seller with zero feedback is hard-capped at a composite score of **35**, regardless of other signals. Zero feedback is the single strongest indicator of a fraudulent or new account, and it would be misleading to allow such a seller to score higher based on price alignment alone.
## Partial scores
When account age hasn't yet been enriched (the BTF scraper is still running), the score is marked **partial** and shown with a spinning indicator. Partial scores are based on available signals only and update automatically when enrichment completes — typically within 3060 seconds per seller.
## STEAL badge
The **STEAL** badge appears when a listing's price is significantly below the market median from recently completed sales. This is a useful signal for buyers, but it can also indicate a scam — always cross-reference with the trust score and red flags.
## Market comps
Market price data comes from eBay's Marketplace Insights API (completed sales). When this API is unavailable (requires an approved eBay developer account), Snipe falls back to listing prices from the Browse API, which is less accurate. The market price shown in search results reflects whichever source was available.

View file

@ -1,226 +1,384 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Snipe — self-hosted install script # Snipe — self-hosted installer
# #
# Supports two install paths: # Supports two install paths:
# Docker (recommended) — everything in containers, no system Python deps required # Docker (recommended) — everything in containers, no system Python deps required
# No-Docker — conda or venv + direct uvicorn, for machines without Docker # Bare metal — conda or pip venv + uvicorn, for machines without Docker
# #
# Usage: # Usage:
# bash install.sh # installs to ~/snipe # bash install.sh # interactive (auto-detects Docker)
# bash install.sh /opt/snipe # custom install directory # bash install.sh --docker # Docker Compose setup only
# bash install.sh ~/snipe --no-docker # force no-Docker path even if Docker present # bash install.sh --bare-metal # conda or venv + uvicorn
# bash install.sh --help
# #
# Requirements (Docker path): Docker with Compose plugin, Git # No account or API key required. eBay credentials are optional (faster searches).
# Requirements (no-Docker path): Python 3.11+, Node.js 20+, Git, xvfb (system)
set -euo pipefail set -euo pipefail
INSTALL_DIR="${1:-$HOME/snipe}" # ── Terminal colours ───────────────────────────────────────────────────────────
FORCE_NO_DOCKER="${2:-}" RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
info() { echo -e "${BLUE}${NC} $*"; }
ok() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW}${NC} $*"; }
error() { echo -e "${RED}${NC} $*" >&2; }
header() { echo; echo -e "${BOLD}$*${NC}"; printf '%0.s─' {1..60}; echo; }
dim() { echo -e "${DIM}$*${NC}"; }
ask() { echo -e "${CYAN}?${NC} ${BOLD}$*${NC}"; }
fail() { error "$*"; exit 1; }
# ── Paths ──────────────────────────────────────────────────────────────────────
SNIPE_CONFIG_DIR="${HOME}/.config/circuitforge"
SNIPE_ENV_FILE="${SNIPE_CONFIG_DIR}/snipe.env"
SNIPE_VENV_DIR="${SNIPE_CONFIG_DIR}/venv"
FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge" FORGEJO="https://git.opensourcesolarpunk.com/Circuit-Forge"
CONDA_ENV="cf"
info() { echo " [snipe] $*"; } # Default install directory. Overridable:
ok() { echo "$*"; } # SNIPE_DIR=/opt/snipe bash install.sh
warn() { echo "! $*"; } SNIPE_INSTALL_DIR="${SNIPE_DIR:-${HOME}/snipe}"
fail() { echo "$*" >&2; exit 1; }
hr() { echo "────────────────────────────────────────────────────────"; }
echo "" # ── Argument parsing ───────────────────────────────────────────────────────────
echo " Snipe — self-hosted installer" MODE_FORCE=""
echo " Install directory: $INSTALL_DIR" for arg in "$@"; do
echo "" case "$arg" in
--bare-metal) MODE_FORCE="bare-metal" ;;
--docker) MODE_FORCE="docker" ;;
--help|-h)
echo "Usage: bash install.sh [--docker|--bare-metal|--help]"
echo
echo " --docker Docker Compose install (recommended)"
echo " --bare-metal conda or pip venv + uvicorn"
echo " --help Show this message"
echo
echo " Set SNIPE_DIR=/path to change the install directory (default: ~/snipe)"
exit 0
;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
# ── Detect capabilities ────────────────────────────────────────────────────── # ── Banner ─────────────────────────────────────────────────────────────────────
echo
echo -e "${BOLD} 🎯 Snipe — eBay listing intelligence${NC}"
echo -e "${DIM} Bid with confidence. Privacy-first, no account required.${NC}"
echo -e "${DIM} Part of the Circuit Forge LLC suite (BSL 1.1)${NC}"
echo
# ── System checks ──────────────────────────────────────────────────────────────
header "System checks"
HAS_DOCKER=false HAS_DOCKER=false
HAS_CONDA=false HAS_CONDA=false
HAS_CONDA_CMD=""
HAS_PYTHON=false HAS_PYTHON=false
HAS_NODE=false HAS_NODE=false
HAS_CHROMIUM=false
HAS_XVFB=false
command -v git >/dev/null 2>&1 || fail "Git is required. Install: sudo apt-get install git"
ok "Git found"
docker compose version >/dev/null 2>&1 && HAS_DOCKER=true docker compose version >/dev/null 2>&1 && HAS_DOCKER=true
conda --version >/dev/null 2>&1 && HAS_CONDA=true if $HAS_DOCKER; then ok "Docker (Compose plugin) found"; fi
python3 --version >/dev/null 2>&1 && HAS_PYTHON=true
node --version >/dev/null 2>&1 && HAS_NODE=true
command -v git >/dev/null 2>&1 || fail "Git is required. Install with: sudo apt-get install git"
# Honour --no-docker flag # Detect conda / mamba / micromamba in preference order
[[ "$FORCE_NO_DOCKER" == "--no-docker" ]] && HAS_DOCKER=false for _c in conda mamba micromamba; do
if command -v "$_c" >/dev/null 2>&1; then
HAS_CONDA=true
HAS_CONDA_CMD="$_c"
ok "Conda manager found: $_c"
break
fi
done
if $HAS_DOCKER; then # Python 3.11+ check
INSTALL_PATH="docker" if command -v python3 >/dev/null 2>&1; then
ok "Docker found — using Docker install path (recommended)" _py_ok=$(python3 -c "import sys; print(sys.version_info >= (3,11))" 2>/dev/null || echo "False")
if [[ "$_py_ok" == "True" ]]; then
HAS_PYTHON=true
ok "Python 3.11+ found ($(python3 --version))"
else
warn "Python found but version is below 3.11 ($(python3 --version)) — bare-metal path may fail"
fi
fi
command -v node >/dev/null 2>&1 && HAS_NODE=true
if $HAS_NODE; then ok "Node.js found ($(node --version))"; fi
# Chromium / Google Chrome — needed for the Kasada-bypass scraper
for _chrome in google-chrome chromium-browser chromium; do
if command -v "$_chrome" >/dev/null 2>&1; then
HAS_CHROMIUM=true
ok "Chromium/Chrome found: $_chrome"
break
fi
done
if ! $HAS_CHROMIUM; then
warn "Chromium / Google Chrome not found."
warn "Snipe uses headed Chromium + Xvfb to bypass eBay's Kasada anti-bot."
warn "The installer will install Chromium via Playwright. If that fails,"
warn "add eBay API credentials to .env to use the API adapter instead."
fi
# Xvfb — virtual framebuffer for headed Chromium on headless servers
command -v Xvfb >/dev/null 2>&1 && HAS_XVFB=true
if $HAS_XVFB; then ok "Xvfb found"; fi
# ── Mode selection ─────────────────────────────────────────────────────────────
header "Install mode"
INSTALL_MODE=""
if [[ -n "$MODE_FORCE" ]]; then
INSTALL_MODE="$MODE_FORCE"
info "Mode forced: $INSTALL_MODE"
elif $HAS_DOCKER; then
INSTALL_MODE="docker"
ok "Docker available — using Docker install (recommended)"
dim " Pass --bare-metal to override"
elif $HAS_PYTHON; then elif $HAS_PYTHON; then
INSTALL_PATH="python" INSTALL_MODE="bare-metal"
warn "Docker not found — using no-Docker path (conda or venv)" warn "Docker not found — using bare-metal install"
else else
fail "Docker or Python 3.11+ is required. Install Docker: https://docs.docker.com/get-docker/" fail "Docker or Python 3.11+ is required. Install Docker: https://docs.docker.com/get-docker/"
fi fi
# ── Clone repos ────────────────────────────────────────────────────────────── # ── Clone repos ───────────────────────────────────────────────────────────────
header "Clone repositories"
# compose.yml and the Dockerfile both use context: .. (parent directory), so # compose.yml and the Dockerfile both use context: .. (parent directory), so
# snipe/ and circuitforge-core/ must be siblings inside INSTALL_DIR. # snipe/ and circuitforge-core/ must be siblings inside SNIPE_INSTALL_DIR.
SNIPE_DIR="$INSTALL_DIR/snipe" REPO_DIR="$SNIPE_INSTALL_DIR"
CORE_DIR="$INSTALL_DIR/circuitforge-core" SNIPE_DIR_ACTUAL="$REPO_DIR/snipe"
CORE_DIR="$REPO_DIR/circuitforge-core"
if [[ -d "$SNIPE_DIR" ]]; then _clone_or_pull() {
info "Snipe already cloned — pulling latest..." local label="$1" url="$2" dest="$3"
git -C "$SNIPE_DIR" pull --ff-only if [[ -d "$dest/.git" ]]; then
info "$label already cloned — pulling latest..."
git -C "$dest" pull --ff-only
else else
info "Cloning Snipe..." info "Cloning $label..."
mkdir -p "$INSTALL_DIR" mkdir -p "$(dirname "$dest")"
git clone "$FORGEJO/snipe.git" "$SNIPE_DIR" git clone "$url" "$dest"
fi fi
ok "Snipe → $SNIPE_DIR" ok "$label$dest"
}
if [[ -d "$CORE_DIR" ]]; then _clone_or_pull "snipe" "$FORGEJO/snipe.git" "$SNIPE_DIR_ACTUAL"
info "circuitforge-core already cloned — pulling latest..." _clone_or_pull "circuitforge-core" "$FORGEJO/circuitforge-core.git" "$CORE_DIR"
git -C "$CORE_DIR" pull --ff-only
else
info "Cloning circuitforge-core (shared library)..."
git clone "$FORGEJO/circuitforge-core.git" "$CORE_DIR"
fi
ok "circuitforge-core → $CORE_DIR"
# ── Configure environment ──────────────────────────────────────────────────── # ── Config file ────────────────────────────────────────────────────────────────
header "Configuration"
ENV_FILE="$SNIPE_DIR/.env" ENV_FILE="$SNIPE_DIR_ACTUAL/.env"
if [[ ! -f "$ENV_FILE" ]]; then if [[ ! -f "$ENV_FILE" ]]; then
cp "$SNIPE_DIR/.env.example" "$ENV_FILE" cp "$SNIPE_DIR_ACTUAL/.env.example" "$ENV_FILE"
# Safe defaults for local installs — no eBay registration, no Heimdall # Disable webhook signature verification for local installs
# (no production eBay key yet — the endpoint won't be registered)
sed -i 's/^EBAY_WEBHOOK_VERIFY_SIGNATURES=true/EBAY_WEBHOOK_VERIFY_SIGNATURES=false/' "$ENV_FILE" sed -i 's/^EBAY_WEBHOOK_VERIFY_SIGNATURES=true/EBAY_WEBHOOK_VERIFY_SIGNATURES=false/' "$ENV_FILE"
ok ".env created from .env.example" ok ".env created from .env.example"
echo "" echo
info "Snipe works out of the box with no API keys." dim " Snipe works out of the box with no API keys (scraper mode)."
info "Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)." dim " Add EBAY_APP_ID / EBAY_CERT_ID later for faster searches (optional)."
echo "" dim " Edit: $ENV_FILE"
echo
else else
info ".env already exists — skipping (delete it to reset)" info ".env already exists — skipping (delete to reset defaults)"
fi fi
cd "$SNIPE_DIR" # ── License key (optional) ─────────────────────────────────────────────────────
header "CircuitForge license key (optional)"
dim " Snipe is free to self-host. A Paid/Premium key unlocks cloud features"
dim " (photo analysis, eBay OAuth). Skip this if you don't have one."
echo
ask "Enter your license key, or press Enter to skip:"
read -r _license_key || true
# ── Docker install path ─────────────────────────────────────────────────────── if [[ -n "${_license_key:-}" ]]; then
_key_re='^CFG-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
if echo "$_license_key" | grep -qP "$_key_re" 2>/dev/null || \
echo "$_license_key" | grep -qE "$_key_re" 2>/dev/null; then
# Append / uncomment Heimdall vars in .env
if grep -q "^# HEIMDALL_URL=" "$ENV_FILE" 2>/dev/null; then
sed -i "s|^# HEIMDALL_URL=.*|HEIMDALL_URL=https://license.circuitforge.tech|" "$ENV_FILE"
else
echo "HEIMDALL_URL=https://license.circuitforge.tech" >> "$ENV_FILE"
fi
# Write or replace CF_LICENSE_KEY
if grep -q "^CF_LICENSE_KEY=" "$ENV_FILE" 2>/dev/null; then
sed -i "s|^CF_LICENSE_KEY=.*|CF_LICENSE_KEY=${_license_key}|" "$ENV_FILE"
else
echo "CF_LICENSE_KEY=${_license_key}" >> "$ENV_FILE"
fi
ok "License key saved to .env"
else
warn "Key format not recognised (expected CFG-XXXX-XXXX-XXXX-XXXX) — skipping."
warn "Edit $ENV_FILE to add it manually."
fi
else
info "No license key entered — self-hosted free tier."
fi
if [[ "$INSTALL_PATH" == "docker" ]]; then # ── Docker install ─────────────────────────────────────────────────────────────
_install_docker() {
header "Docker install"
cd "$SNIPE_DIR_ACTUAL"
info "Building Docker images (~1 GB download on first run)..." info "Building Docker images (~1 GB download on first run)..."
docker compose build docker compose build
info "Starting Snipe..." info "Starting Snipe..."
docker compose up -d docker compose up -d
echo "" echo
ok "Snipe is running!" ok "Snipe is running!"
hr printf '%0.s─' {1..60}; echo
echo " Web UI: http://localhost:8509" echo -e " ${GREEN}Web UI:${NC} http://localhost:8509"
echo " API: http://localhost:8510/docs" echo -e " ${GREEN}API:${NC} http://localhost:8510/docs"
echo "" echo
echo " Manage: cd $SNIPE_DIR && ./manage.sh {start|stop|restart|logs|test}" echo -e " ${DIM}Manage: cd $SNIPE_DIR_ACTUAL && ./manage.sh {start|stop|restart|logs|test}${NC}"
hr printf '%0.s─' {1..60}; echo
echo "" echo
exit 0 }
fi
# ── No-Docker install path ─────────────────────────────────────────────────── # ── Bare-metal install ─────────────────────────────────────────────────────────
_install_xvfb() {
# System deps: Xvfb is required for Playwright (Kasada bypass via headed Chromium) if $HAS_XVFB; then return; fi
if ! command -v Xvfb >/dev/null 2>&1; then
info "Installing Xvfb (required for eBay scraper)..." info "Installing Xvfb (required for eBay scraper)..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends xvfb sudo apt-get install -y --no-install-recommends xvfb
ok "Xvfb installed"
elif command -v dnf >/dev/null 2>&1; then elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y xorg-x11-server-Xvfb sudo dnf install -y xorg-x11-server-Xvfb
ok "Xvfb installed"
elif command -v brew >/dev/null 2>&1; then elif command -v brew >/dev/null 2>&1; then
warn "macOS: Xvfb not available. The scraper fallback may fail." warn "macOS: Xvfb not available via Homebrew."
warn "The scraper (Kasada bypass) will not work on macOS."
warn "Add eBay API credentials to .env to use the API adapter instead." warn "Add eBay API credentials to .env to use the API adapter instead."
else else
warn "Could not install Xvfb automatically. Install it with your package manager." warn "Could not install Xvfb automatically. Install it with your system package manager."
fi warn " Debian/Ubuntu: sudo apt-get install xvfb"
warn " Fedora/RHEL: sudo dnf install xorg-x11-server-Xvfb"
fi fi
}
# ── Python environment setup ───────────────────────────────────────────────── _setup_python_env() {
if $HAS_CONDA; then if $HAS_CONDA; then
info "Setting up conda environment '$CONDA_ENV'..." info "Setting up conda environment (manager: $HAS_CONDA_CMD)..."
if conda env list | grep -q "^$CONDA_ENV "; then _env_name="cf"
info "Conda env '$CONDA_ENV' already exists — updating..." if "$HAS_CONDA_CMD" env list 2>/dev/null | grep -q "^${_env_name} "; then
conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR" info "Conda env '$_env_name' already exists — updating packages..."
conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR"
else else
conda create -n "$CONDA_ENV" python=3.11 -y "$HAS_CONDA_CMD" create -n "$_env_name" python=3.11 -y
conda run -n "$CONDA_ENV" pip install --quiet -e "$CORE_DIR"
conda run -n "$CONDA_ENV" pip install --quiet -e "$SNIPE_DIR"
fi fi
conda run -n "$CONDA_ENV" playwright install chromium "$HAS_CONDA_CMD" run -n "$_env_name" pip install --quiet -e "$CORE_DIR"
conda run -n "$CONDA_ENV" playwright install-deps chromium "$HAS_CONDA_CMD" run -n "$_env_name" pip install --quiet -e "$SNIPE_DIR_ACTUAL"
PYTHON_RUN="conda run -n $CONDA_ENV" "$HAS_CONDA_CMD" run -n "$_env_name" playwright install chromium
ok "Conda environment '$CONDA_ENV' ready" "$HAS_CONDA_CMD" run -n "$_env_name" playwright install-deps chromium
PYTHON_BIN="$HAS_CONDA_CMD run -n $_env_name"
ok "Conda environment '$_env_name' ready"
else else
info "Setting up Python venv at $SNIPE_DIR/.venv ..." info "Setting up pip venv at $SNIPE_VENV_DIR ..."
python3 -m venv "$SNIPE_DIR/.venv" mkdir -p "$SNIPE_CONFIG_DIR"
"$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$CORE_DIR" python3 -m venv "$SNIPE_VENV_DIR"
"$SNIPE_DIR/.venv/bin/pip" install --quiet -e "$SNIPE_DIR" "$SNIPE_VENV_DIR/bin/pip" install --quiet -e "$CORE_DIR"
"$SNIPE_DIR/.venv/bin/playwright" install chromium "$SNIPE_VENV_DIR/bin/pip" install --quiet -e "$SNIPE_DIR_ACTUAL"
"$SNIPE_DIR/.venv/bin/playwright" install-deps chromium "$SNIPE_VENV_DIR/bin/playwright" install chromium
PYTHON_RUN="$SNIPE_DIR/.venv/bin" "$SNIPE_VENV_DIR/bin/playwright" install-deps chromium
ok "Python venv ready at $SNIPE_DIR/.venv" PYTHON_BIN="$SNIPE_VENV_DIR/bin"
ok "Python venv ready at $SNIPE_VENV_DIR"
fi fi
}
# ── Frontend ───────────────────────────────────────────────────────────────── _build_frontend() {
if ! $HAS_NODE; then
if $HAS_NODE; then warn "Node.js not found — skipping frontend build."
warn "Install Node.js 20+ from https://nodejs.org and re-run install.sh."
warn "Until then, access the API at http://localhost:8510/docs"
return
fi
info "Building Vue frontend..." info "Building Vue frontend..."
cd "$SNIPE_DIR/web" cd "$SNIPE_DIR_ACTUAL/web"
npm ci --prefer-offline --silent npm ci --prefer-offline --silent
npm run build npm run build
cd "$SNIPE_DIR" cd "$SNIPE_DIR_ACTUAL"
ok "Frontend built → web/dist/" ok "Frontend built → web/dist/"
else }
warn "Node.js not found — skipping frontend build."
warn "Install Node.js 20+ from https://nodejs.org and re-run install.sh to build the UI."
warn "Until then, you can access the API directly at http://localhost:8510/docs"
fi
# ── Write start/stop scripts ───────────────────────────────────────────────── _write_start_scripts() {
# start-local.sh — launches the FastAPI server
cat > "$SNIPE_DIR/start-local.sh" << 'STARTSCRIPT' cat > "$SNIPE_DIR_ACTUAL/start-local.sh" << 'STARTSCRIPT'
#!/usr/bin/env bash #!/usr/bin/env bash
# Start Snipe without Docker (API only — run from the snipe/ directory) # Start Snipe API (bare-metal / no-Docker mode)
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [[ -f .venv/bin/uvicorn ]]; then if [[ -f "$HOME/.config/circuitforge/venv/bin/uvicorn" ]]; then
UVICORN=".venv/bin/uvicorn" UVICORN="$HOME/.config/circuitforge/venv/bin/uvicorn"
elif command -v conda >/dev/null 2>&1 && conda env list | grep -q "^cf "; then elif command -v conda >/dev/null 2>&1 && conda env list 2>/dev/null | grep -q "^cf "; then
UVICORN="conda run -n cf uvicorn" UVICORN="conda run -n cf uvicorn"
elif command -v mamba >/dev/null 2>&1 && mamba env list 2>/dev/null | grep -q "^cf "; then
UVICORN="mamba run -n cf uvicorn"
else else
echo "No Python env found. Run install.sh first." >&2; exit 1 echo "No Snipe Python environment found. Run install.sh first." >&2; exit 1
fi fi
mkdir -p data mkdir -p data
echo "Starting Snipe API on http://localhost:8510 ..." echo "Starting Snipe API http://localhost:8510 ..."
$UVICORN api.main:app --host 0.0.0.0 --port 8510 "${@}" exec $UVICORN api.main:app --host 0.0.0.0 --port 8510 "${@}"
STARTSCRIPT STARTSCRIPT
chmod +x "$SNIPE_DIR/start-local.sh" chmod +x "$SNIPE_DIR_ACTUAL/start-local.sh"
# Frontend serving (if built) # serve-ui.sh — serves the built Vue frontend (dev only)
cat > "$SNIPE_DIR/serve-ui.sh" << 'UISCRIPT' cat > "$SNIPE_DIR_ACTUAL/serve-ui.sh" << 'UISCRIPT'
#!/usr/bin/env bash #!/usr/bin/env bash
# Serve the pre-built Vue frontend on port 8509 (dev only — use nginx for production) # Serve the pre-built Vue frontend (dev only — use nginx for production).
# See docs/nginx-self-hosted.conf for a production nginx config.
cd "$(dirname "$0")/web/dist" cd "$(dirname "$0")/web/dist"
python3 -m http.server 8509 echo "Serving Snipe UI → http://localhost:8509 (Ctrl+C to stop)"
exec python3 -m http.server 8509
UISCRIPT UISCRIPT
chmod +x "$SNIPE_DIR/serve-ui.sh" chmod +x "$SNIPE_DIR_ACTUAL/serve-ui.sh"
echo "" ok "Start scripts written"
ok "Snipe installed (no-Docker mode)" }
hr
echo " Start API: cd $SNIPE_DIR && ./start-local.sh" _install_bare_metal() {
echo " Serve UI: cd $SNIPE_DIR && ./serve-ui.sh (separate terminal)" header "Bare-metal install"
echo " API docs: http://localhost:8510/docs" _install_xvfb
echo " Web UI: http://localhost:8509 (after ./serve-ui.sh)" _setup_python_env
echo "" _build_frontend
echo " For production, point nginx at web/dist/ and proxy /api/ to localhost:8510" _write_start_scripts
hr
echo "" echo
ok "Snipe installed (bare-metal mode)"
printf '%0.s─' {1..60}; echo
echo -e " ${GREEN}Start API:${NC} cd $SNIPE_DIR_ACTUAL && ./start-local.sh"
echo -e " ${GREEN}Serve UI:${NC} cd $SNIPE_DIR_ACTUAL && ./serve-ui.sh ${DIM}(separate terminal)${NC}"
echo -e " ${GREEN}API docs:${NC} http://localhost:8510/docs"
echo -e " ${GREEN}Web UI:${NC} http://localhost:8509 ${DIM}(after ./serve-ui.sh)${NC}"
echo
echo -e " ${DIM}For production, configure nginx to proxy /api/ to localhost:8510${NC}"
echo -e " ${DIM}and serve web/dist/ as the document root.${NC}"
echo -e " ${DIM}See: $SNIPE_DIR_ACTUAL/docs/nginx-self-hosted.conf${NC}"
printf '%0.s─' {1..60}; echo
echo
}
# ── Main ───────────────────────────────────────────────────────────────────────
main() {
if [[ "$INSTALL_MODE" == "docker" ]]; then
_install_docker
else
_install_bare_metal
fi
}
main

63
mkdocs.yml Normal file
View file

@ -0,0 +1,63 @@
site_name: Snipe
site_description: eBay trust scoring before you bid — catch scammers, flag suspicious prices, surface duplicate photos.
site_author: Circuit Forge LLC
site_url: https://docs.circuitforge.tech/snipe
repo_url: https://git.opensourcesolarpunk.com/Circuit-Forge/snipe
repo_name: Circuit-Forge/snipe
theme:
name: material
palette:
- scheme: default
primary: deep orange
accent: orange
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: deep orange
accent: orange
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- navigation.top
- search.suggest
- search.highlight
- content.code.copy
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.tabbed:
alternate_style: true
- tables
- toc:
permalink: true
nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/installation.md
- Quick Start: getting-started/quick-start.md
- eBay API Keys (Optional): getting-started/ebay-api.md
- User Guide:
- Searching: user-guide/searching.md
- Trust Scores: user-guide/trust-scores.md
- Red Flags: user-guide/red-flags.md
- Community Blocklist: user-guide/blocklist.md
- Settings: user-guide/settings.md
- Reference:
- Trust Score Algorithm: reference/trust-scoring.md
- Tier System: reference/tier-system.md
- Architecture: reference/architecture.md

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "snipe" name = "snipe"
version = "0.2.0" version = "0.3.0"
description = "Auction listing monitor and trust scorer" description = "Auction listing monitor and trust scorer"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [