shopping-radar
by rbenhaga
README.md
# shopping-radar đđ
Serveur **MCP** qui permet à Claude de récupérer les annonces d'un produit sur
plusieurs places de marché françaises et de les **analyser rigoureusement** :
prix, date, livraison, et **distance au point de référence (Montpellier)** quand
l'article n'est pas livrable.
Pensé pour trouver le **meilleur rapport qualité-prix** (ex. un cadeau), en
comparant l'occasion au prix du neuf.
## Sources (8)
| Source | Type | ClĂ© requise | Ătat |
|---|---|---|---|
| **Leboncoin** (direct) | occasion | â (intermittent, DataDome) | â
actif |
| **Leboncoin (Apify)** | occasion **fiable** | `APIFY_TOKEN` (~1 $/1000 rĂ©sultats) | â
testé, opt-in |
| **Vinted** | occasion | â | â
actif |
| **Dealabs** | deals neuf / erreurs de prix | â | â
actif |
| **eBay** | occasion + reconditionnĂ© | `EBAY_APP_ID` + `EBAY_CERT_ID` (gratuit) | đ sur clĂ© |
| **Google Shopping** | neuf (multi-marchands) | `SERPAPI_KEY` (gratuit limitĂ©) | đ sur clĂ© |
| **Idealo** | comparateur neuf | `APIFY_TOKEN` (crĂ©dit gratuit) | đ sur clĂ© |
| **Keepa** | Amazon neuf + historique | `KEEPA_API_KEY` (payant) | đ sur clĂ© |
| **Back Market** | reconditionnĂ© garanti | `APIFY_TOKEN` + `BACKMARKET_APIFY_ACTOR` | đ sur clĂ© |
> Les 3 premiÚres fonctionnent **sans aucune clé**. Les autres s'activent en
> ajoutant la clé correspondante dans `.env`. **Aucune dépendance Python
> supplĂ©mentaire** n'est nĂ©cessaire pour les activer (sauf Keepa) â tout passe par `httpx`.
### Couverture par usage
- **Occasion :** Leboncoin, Vinted, eBay
- **Reconditionné :** Back Market, eBay
- **Neuf / prix de rĂ©fĂ©rence :** Google Shopping (Amazon, Fnac, Darty, BoulangerâŠ), Idealo, Keepa, Dealabs
## Pipeline d'analyse & qualité des données
`analyze` enchaĂźne : **recherche multi-sources â dĂ©duplication â filtre logistique
(Montpellier) â filtre de pertinence â enrichissement qualitĂ© â tri par score
composite**. Chaque offre gardée est enrichie de :
- `condition_label` â Ă©tat normalisĂ© sur une Ă©chelle standard (neuf, comme neuf,
trÚs bon, bon, correct, reconditionné, pour piÚces).
- `is_bundle` â dĂ©tecte les lots/packs (« + 2 batteries », « pack complet »).
- `risk_level` (`ok`/`faible`/`moyen`/`eleve`) + `risk_flags` : `prix_tres_bas`
(sous mĂ©diane â 30 %), `marque_incoherente` (marque â produit recherchĂ©),
`import_hors_fr` (titre en langue Ă©trangĂšre â transfrontalier), `vendeur_inconnu`.
- `deal_score` â note composite 0-100 oĂč **le risque PRIME sur le prix** : la
valeur-prix est plafonnĂ©e sous le seuil « trop beau pour ĂȘtre vrai » (pas de
bonus pour les prix d'arnaque) et un malus de risque (jusqu'Ă â45) Ă©crase le
bonus prix. Pondération : prix vs neuf (50) + état (20) + proximité (15) +
confiance vendeur (10) â risque.
- **Segmentation par variante** : les stats (médiane, seuil « trop bas ») sont
calculées **par segment** (sous-modÚle à premium/standard), pas sur un pool qui
mélangerait X4 nue / Adventure / Bike⊠Le seuil bas est **auto-calibré** par
z-score robuste (mĂ©diane â k·MAD intra-segment), au lieu d'un ratio fixe.
- **DĂ©tection de mislabel** : une variante premium (« Adventure », « Bike »âŠ)
affichĂ©e **sous** le prix d'une version de base â flag `mislabel_suspect`
(résolu sans référence externe, par comparaison inter-segments).
- Seuils configurables : `RISK_LOW_PRICE_RATIO`, `RISK_MAD_K`, `IMPORT_MARKERS`,
`PREMIUM_KEYWORDS`.
> **Limites encore ouvertes** (assumées) : (1) la *validation* du modÚle de risque
> repose sur des tests unitaires (change-detectors), pas sur un jeu labellisé
> précision/rappel ; (2) les **stats vendeur** (note, ancienneté, derniÚre
> connexion) et la **fraĂźcheur/statut vendu** des annonces ne sont pas encore
> exploitĂ©es â ce sont les signaux anti-fraude les plus forts ; (3) le score
> n'intÚgre pas encore **garantie FR / retour / complétude accessoires**.
- `also_on` â autres sources oĂč l'article a Ă©tĂ© repĂ©rĂ© (dĂ©duplication inter-sources).
`stats` remonte aussi `duplicates_removed`, `suspicious_count`, `new_reference_price`.
Tests : `python tests/test_core.py` (tests déterministes, sans réseau).
**Validation (au-delĂ des tests unitaires) :** `python eval/score.py <gold>` mesure
le **rappel `scam`** (la vraie cible) séparément du `risky`, avec IC95% Wilson
(honnĂȘte sur petit N), + faux positifs sur les `ok`. Protocole de labellisation
figé dans [eval/LABELING.md](eval/LABELING.md) (verdict `ok`/`risky`/`scam`,
labellisation **Ă l'aveugle**).
Workflow gold réel :
1. `python eval/make_batch.py` â `eval/to_label.jsonl` (batch rĂ©el, **aveugle**,
stratifié-pour-faire-mal : imports/bundles/refurb/prix bas/phrases FR ambiguës).
2. Labelliser Ă la main (`verdict` + `reason`) selon `eval/LABELING.md`.
3. `python eval/score.py eval/to_label.jsonl` â vrais prĂ©cision/rappel.
`eval/gold.jsonl` (25 lignes synthétiques) reste comme **détecteur de régression**
(il a chiffré puis verrouillé le fix « prix négociable »). C'est un détecteur de
modes d'échec, pas un go/no-go statistique au pourcent.
**Refonte IA-juge (paradigme cible) :** voir [docs/REDESIGN.md](docs/REDESIGN.md)
â l'outil devient un *assistant qui trie* (jamais de rejet silencieux), avec 3
couches : déterministe *compte/trie* (maths, jamais de jugement fraude), IA
multimodale *juge le sérieux* (nourrie des chiffres déterministes, jamais de
calcul de prix), humain *tranche* (ses décisions deviennent les labels). Le
filtrage par mots-clés comme mécanisme de jugement meurt ; il ne survit que comme
pré-filtre pertinence pas cher.
**Vers un produit (SaaS) :** voir [docs/AUDIT.md](docs/AUDIT.md) (audit du code),
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) (passage statelessâstateful : Postgres
+ file de jobs + précalcul) et [db/schema.sql](db/schema.sql). Le moteur actuel
devient la couche *worker*, on ne le réécrit pas.
## Sécurité free tier (quotas)
Un garde-fou ([core/quota.py](core/quota.py)) compte les appels par provider et
**bloque proprement** avant de dépasser la limite gratuite (pas de facturation
surprise). Usage persisté dans `.cache/usage.json`. Limites par défaut (juin 2026,
surchargeables dans `.env`) :
| Provider | Limite gratuite | FenĂȘtre |
|---|---|---|
| SerpAPI | 250 recherches | mois |
| Apify (runs) | 50 runs (vrai plafond = 5 $ de crédit) | mois |
| eBay Browse | 5 000 appels | jour |
| Base Adresse Nationale | 2 000 (poli) | jour |
- **Sources coûteuses opt-in** : Idealo, Back Market (runs Apify) et Keepa
consomment du crĂ©dit â **exclues des recherches par dĂ©faut**. Pour les utiliser,
les nommer : `analyze(query, sources=["vinted","idealo"])`. Google Shopping
(SerpAPI, gratuit) fournit déjà le prix neuf de référence par défaut.
- Ătat des quotas visible via l'outil `list_sources`.
## Fiabilité Leboncoin & proxy
**Important :** la lib `lbc` fait **dĂ©jĂ ** le plus dur â impersonation TLS via
`curl_cffi` (JA3 d'un vrai navigateur), amorçage du cookie DataDome (GET homepage),
et utilisation de l'**API mobile** avec un User-Agent d'app. La couche TLS n'est
donc PAS le problÚme. Les 503 intermittents viennent de la **réputation de l'IP**
(ton IP perso flaggĂ©e aprĂšs quelques requĂȘtes). Le seul vrai levier = une IP propre.
Du gratuit au plus fiable :
1. **Retries + rotation de fingerprint** (gratuit, activé) : `LEBONCOIN_RETRIES=3`.
2. **Proxy résidentiel FR** : `LEBONCOIN_PROXY=...` ou un **pool** `LEBONCOIN_PROXIES=p1,p2,...`
(rotation aléatoire). Options : `LEBONCOIN_IMPERSONATE`, `LEBONCOIN_DATADOME_COOKIE`.
3. **â
Source `leboncoin_apify` (la plus fiable, recommandée)** : acteur Apify
`fatihtahta/leboncoin-fr-scraper` (**proxy résidentiel inclus** ~1 $/1000 résultats).
Testée, données riches (GPS, code postal, vendeur pro/particulier). Opt-in :
`analyze(query, sources=["leboncoin_apify","vinted","google_shopping"])`.
> â ïž **Pas de proxy gratuit fiable contre DataDome** (les gratuits = IP datacenter,
> filtrées). Voie fiable « sans CB » : la source `leboncoin_apify` (crédit Apify).
## GĂ©nĂ©rique â fonctionne pour n'importe quel article
Rien n'est codĂ© en dur pour un produit donnĂ© : la requĂȘte est un paramĂštre, le
point de référence est dans `.env` (`HOME_LAT/LNG`), et les heuristiques sont
génériques. Pour un domaine précis, on **ajoute** des mots-clés via `.env` :
`ACCESSORY_KEYWORDS=perche,objectif,trepied` (photo) ou `âŠ=pompe,antivol` (vĂ©lo),
et `BUNDLE_KEYWORDS=...`. Le filtre de pertinence (`is_relevant`) s'adapte seul au
modÚle recherché (tokens chiffrés type « x4 », « 13 »).
### RĂšgle livraison / distance (mise Ă jour)
Une annonce est **rejetée uniquement** si elle est **explicitement non livrable
ET trop loin**. Si la livraison est **inconnue** (cas fréquent Leboncoin), l'annonce
est **gardĂ©e avec un drapeau « (!) livraison Ă vĂ©rifier »** plutĂŽt que jetĂ©e â on ne
perd pas une bonne affaire potentiellement livrable.
## Géolocalisation
Si une annonce n'a pas de coordonnées GPS (fréquent sur Leboncoin), le code
postal/ville est géocodé via l'**API officielle Base Adresse Nationale**
(gratuite, sans clé) pour calculer la distance à Montpellier. Cache dans
`.cache/geocode.json`.
## Installation
```bash
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
copy .env.example .env # puis éditer si besoin
```
## Outils exposés à Claude (MCP)
- `list_sources()` â sources disponibles + point de rĂ©fĂ©rence.
- `search_all(query, max_price?, sources?, limit_per_source?)` â rĂ©cupĂšre les
offres normalisées de toutes les sources actives (en parallÚle).
- `analyze(query, max_price?, max_distance_km?, reference_price?, ...)` â
recherche **+ filtrage logistique** (livrable OU à †X km de Montpellier)
**+ tri par prix** et score de valeur vs prix neuf.
- `build_candidates(query, budget?, priorities?, top_k?, ...)` â produit le
**« fichier de candidats » pour un agent décideur** : normalise chaque offre
(schĂ©ma stable, `total_cost_normalized`, `warranty_months` oĂč **null â 0**),
met en **quarantaine** le risque élevé/mislabel (jamais transmis), et
**stratifie** en top-K par (modÚle à état). Renvoie aussi un **brief de
dĂ©cision**. Le dĂ©terministe ne tranche rien de subjectif â c'est le job de l'agent.
## Brancher sur Claude Code / Claude Desktop
Ajouter dans la config MCP (ex. `claude_desktop_config.json`) :
```json
{
"mcpServers": {
"shopping-radar": {
"command": "python",
"args": ["C:/Users/rayan/Desktop/Dev/Leboncoin/server.py"]
}
}
}
```
Puis, dans Claude : _« Analyse les Insta360 X4 d'occasion sous 320 âŹ, livrables ou Ă moins de 50 km de Montpellier. »_
## RÚgle métier « livraison / distance »
Une offre est **gardée** si :
- elle est **livrable**, OU
- sa **distance au point de référence †`MAX_DISTANCE_KM`** (défaut 60 km).
Si livraison et distance sont inconnues, l'offre est gardée mais **marquée « à vérifier »**
(on ne jette jamais une offre faute d'information).
## Avertissements
- Les clients Leboncoin/Vinted reposent sur des **API non-officielles** qui
peuvent casser sans préavis et dont l'usage peut contrevenir aux CGU des sites.
Réservé à un usage personnel et raisonnable.
- L'analyse finale (recommandation) est faite par **Claude** ; ce serveur fournit
la donnée normalisée et un pré-tri déterministe.
## Architecture
```
server.py # serveur MCP (outils list_sources / search_all / analyze)
core/
models.py # Offer normalisée + parsing prix/date
geo.py # distance Montpellier (haversine) + géocodage des CP manquants
geocode.py # géocodage Base Adresse Nationale (gratuit) + cache disque
score.py # filtre logistique/accessoires + scoring qualité-prix
sources/
base.py # interface Source
leboncoin.py vinted.py dealabs.py # gratuit
leboncoin_apify.py # Leboncoin fiable (Apify, opt-in)
ebay.py google_shopping.py idealo.py # sur clé
keepa.py backmarket.py # sur clé
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing