Skip to main content
Glama
rbenhaga
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é
```