chineur
Enables pricing an entire project at the best delivered price by grouping purchases across multiple merchants, new and used, while minimizing total cost including shipping through a single MCP call.
README
chineur
Chiffrage d'un projet entier au meilleur prix livraison comprise, neuf et occasion séparés, avec un plan de commande groupé qui minimise le total réellement payé.
Le moins cher article par article n'est pas la bonne réponse : 10 articles chez 10 marchands, c'est 10 frais de port. Le moteur de panier groupe les achats chez 2 ou 3 marchands, seuils de franco compris, et affiche l'écart avec ce total naïf.
Installation
python -m venv .venv && ./.venv/bin/pip install -e .
./.venv/bin/pytest # 74 tests hors ligne, aucun réseau
Python 3.11 ou plus. Le projet tourne sans aucune clé : LeDénicheur, AliExpress et
Leboncoin ne demandent rien. Les sources à clé (eBay, Mouser) restent simplement éteintes,
et list_sources() le dit.
Clés
Aucun secret n'est lu dans un fichier versionné. bin/chineur-mcp.sh charge un .env
local s'il existe (ignoré par git), sinon il prend l'environnement tel quel, ce qui permet
de les injecter depuis un gestionnaire de secrets par simple substitution, la valeur ne
transitant alors que par l'environnement du processus :
export EBAY_CERT_ID=$(votre-cli-de-coffre get password 'eBay - API Browse')
| Variable | Rôle | Sans elle |
|---|---|---|
EBAY_APP_ID / EBAY_CERT_ID |
keyset eBay Browse | source eBay éteinte |
MOUSER_API_KEY |
clé Mouser Search API | source Mouser éteinte |
FLARESOLVERR_URL |
recours navigateur sur mur anti-bot | recours éteint, le reste fonctionne |
CHINEUR_VAR |
répertoire d'état (cache, quotas, cookies, journal) | <dépôt>/var |
Utilisation
Exposé en MCP stdio (bin/chineur-mcp.sh), trois outils :
| Outil | Rôle |
|---|---|
price_project(articles, zipcode?, used_threshold?, detail?) |
chiffre une liste complète en un seul appel |
best_price(article, …) |
le cas à un seul article, en detail="full" |
list_sources() |
état, quota horaire restant et coupure en cours de chaque source |
Chaque article accepte une quantité ou un plafond : "LD2410C x3", "ESP32-S3 <=12€".
En ligne de commande :
python -c "
import asyncio; from chineur.models import Project
from chineur.search import Orchestrator; from chineur.report import render
async def m():
o = Orchestrator(); print(render(await o.run(Project.build(['LD2410C x2','BME280'])), failures=o.failures))
asyncio.run(m())"
Sources et ce qu'elles valent
| Source | Volet | Port | Remarque |
|---|---|---|---|
| LeDénicheur | neuf + occasion | exact | agrégateur : apporte Amazon, Cdiscount, Fnac, Rue du Commerce, Back Market, Rakuten en une requête |
| AliExpress | neuf | estimé | seule source réaliste pour les modules chinois de niche |
| Leboncoin | occasion | estimé | 10 recherches/heure maximum, non négociable |
| eBay | neuf + occasion | exact | API Browse officielle, active depuis le 2026-08-06 |
| Mouser | neuf | estimé | distributeur pro, API officielle. Couvre ce qu'AliExpress rate (étain, tresse, résistances, transistors, connectique). Inutile sur les modules chinois : absent, ou 3 à 5x le prix |
Limites connues, mesurées le 2026-08-05 et non contournables sans Playwright (V2) :
- AliExpress ne donne ni le vendeur ni le port sur la page de recherche, et la fiche produit en HTTP simple est vide. Le groupage se fait donc au niveau de la plateforme, avec son seuil de franco. Après plusieurs requêtes rapprochées, AliExpress répond 200 avec une page de 2 Ko : c'est détecté et traité comme un blocage, pas comme une absence d'offre.
- Le budget AliExpress est d'environ 6 requêtes par IP fraîche, mesuré le 2026-08-10 après 46 h sans aucune sollicitation : 6 articles servis (133 offres), blocage au 7e, FlareSolverr mis au mur lui aussi. Une liste de 17 articles ne peut donc pas être chiffrée en une fois par scraping, quelle que soit l'attente respectée. C'est la limite structurelle qui justifie l'API affiliée officielle (portals.aliexpress.com), et non un réglage de patience à trouver.
- En occasion, le prix est indicatif. Les annonces Leboncoin sont du texte libre : un résultat peut être un lot, une pièce cassée ou un accessoire. Neuf et occasion ne sont jamais fusionnés en un prix unique.
- Le port est marqué
~quand il est estimé (tableshipping.py) et?quand il est inconnu. Un total contenant une estimation est signalé comme tel.
Pertinence : le vrai facteur limitant
Mesuré le 2026-08-08 sur la liste type réelle (17 articles, composants) : les « introuvables » ne venaient pas d'un manque de sources mais du libellé. Deux causes distinctes, séparées en comptant les titres bruts rendus par eBay avant filtrage.
La requête est trop longue. « etain soudure 60/40 flux 0.8mm » fait renvoyer 0 résultat
par eBay ; « etain soudure » en renvoie 11. Chaque mot descriptif ajouté rétrécit le
résultat jusqu'au vide. D'où reduced_query et la seconde chance de guarded_search :
quand une source ne rend rien, on retente une fois sur un libellé réduit aux références
et aux deux premiers mots porteurs, les qualifiants jetés. Une requête de plus uniquement en
cas d'échec, et jamais sur le dernier crédit du quota.
Le filtre jetait du bon. « cables dupont assortiment » contre « Câble Dupont …
Assortiment 5 à 100pcs » tombait à 0.67 pour un seuil de 0.70 : le score ne comparait que
par inclusion de chaîne, donc le pluriel cassait tout. _apparie rapproche désormais sur le
préfixe commun (60 % du mot), réservé aux mots d'au moins 4 lettres — jamais aux nombres
ni aux références courtes, pour que « 2410 » et « 2420 » restent deux capteurs différents.
Résultat sur la même liste : eBay passe de 10 à 14 articles couverts sur 17, articles sans aucune offre de 5 à 3. Restent trois vrais introuvables, à toutes les sources : la plaque pastillée, le Diese 2201M et la Sugon 8620DX.
Le prix à payer, et il est réel. Une recherche élargie répond à une question plus large que celle posée : « ESP32-WROOM-32 DevKit 38 broches » élargi en « esp32 wroom 32 » remonte un modèle 30 broches. Toute offre issue d'un élargissement porte donc la réserve « recherche élargie à « … » », qui doit rester visible jusqu'à l'affichage.
Recours navigateur (FlareSolverr)
Le point faible du projet : AliExpress est la seule source des modules chinois, et elle se
fait bloquer au bout de 5 à 9 recherches. Le README annonçait ce trou comme « non
contournable sans Playwright (V2) ». Il l'est, sans dépendance supplémentaire côté projet :
une instance FlareSolverr pilote un vrai Chrome, exécute le JavaScript et porte une
empreinte de navigateur complète. Elle se déclare par FLARESOLVERR_URL ; non renseignée,
le recours est simplement éteint.
Mesuré le 2026-08-08, alors que le connecteur direct était bloqué et son disjoncteur ouvert :
fr.aliexpress.com/w/wholesale-LD2410C.html rend HTTP 200, 832 Ko, aucun marqueur anti-bot,
et le parseur HTML existant y décode 60 articles avec prix en EUR. Aucune modification du
parsing n'a été nécessaire.
Branchement dans AliExpress._items : sur mur anti-bot ou page tronquée, on tente le
recours avant de lever Blocked. Points de conception :
- Recours, pas régime. Chaque appel démarre un Chrome : c'est lent et lourd. On n'y passe que sur un blocage avéré.
Noneet pas[]quand le recours ne donne rien. Une liste vide passerait pour « aucun résultat », le disjoncteur ne s'ouvrirait pas, et le connecteur retaperait la source bloquée à l'article suivant en aggravant le bridage.- Jamais bloquant.
flaresolverr.fetchne lève pas : une panne du solveur masquerait la vraie cause derrière un incident d'infrastructure.FLARESOLVERR_URL=l'éteint. - Abandon après 2 échecs consécutifs. Voir la limite ci-dessous : sans ça, chaque article suivant paierait encore des dizaines de secondes d'attente pour rien.
Sa limite, mesurée le 2026-08-08. Le recours n'est pas un contournement, c'est un
second budget. Sur huit recherches AliExpress enchaînées : six servies à 60 articles
chacune en 4 à 6 s, la septième en erreur 500 après 101 s d'attente, la huitième renvoyant
le mur anti-bot (208 Ko, marqueur RGV587). L'IP de sortie ne change pas, donc le même mur
finit par tomber. Deux conséquences dans le code : maxTimeout ramené de 90 à 45 s, et
un mur rendu en HTTP 200 compte comme un échec, sinon le compteur d'abandon ne se
déclencherait jamais.
En pratique le budget AliExpress passe donc d'environ 9 recherches à environ 15, avec 60 articles par page au lieu de 20. C'est un gain net, ce n'est pas l'illimité.
Garde-fous anti-ban
L'IP de sortie est unique et résidentielle : un ban la coupe entièrement. Tout le module est écrit pour ne jamais en arriver là.
-
Quotas horaires persistés en SQLite (
var/chineur.db) : Leboncoin 10/h, LeDénicheur 30/h, AliExpress 20/h, eBay 200/h, Mouser 60/h (quotas API, aucun risque IP). -
Espacement sérialisé par source, pas un simple
sleep: l'orchestrateur lançant tous les articles en parallèle, des délais concurrents s'écouleraient en même temps et les requêtes partiraient quand même en rafale. Chaque départ attend le précédent. -
Disjoncteur sur 403 / captcha / page tronquée : 1 h, 2 h pour AliExpress, dont la pénalité persiste au-delà de la rafale qui l'a déclenchée.
-
Le blocage AliExpress n'est ni un quota ni une question de cadence. Trois recettes du 2026-08-06, libellés jamais mis en cache :
espacement transport servies avant blocage 6-12 s requêtes nues 5 (blocage à 46 s) 6-12 s session à cookies 6 (blocage à 56 s) 45-60 s session à cookies 2 (blocage à 108 s) 2,5 s session à cookies, endpoint JSON 9 (blocage à 30 s) Ralentir dégrade le rendement, donc la cadence n'est pas le levier : le meilleur rendement s'obtient en allant vite. Le blocage dure ~40 min, mesuré deux fois, et se signale par
FAIL_SYS_USER_VALIDATE/RGV587_ERROR::SM, l'anti-bot d'Alibaba qui exige une validation. -
Les articles sont lus sur l'endpoint interne
POST www.aliexpress.com/fn/search-pc/index(celui que la page de recherche appelle elle-même), charge utile de formemods, depuis une session déjà ouverte sur l'accueil. Il rend exactement les mêmes objets article que la page HTML, en.data.result.mods.itemList.content, pour 390 Ko au lieu de 600. Piège : plusieurs blocs de la réponse portent une clécontent, celui des filtres de recherche arrive avantitemList. La page HTML reste en repli automatique, mais uniquement si la réponse n'a plus la forme attendue : sur un mur anti-bot, retenter en HTML depuis la même IP ne ferait que gaspiller une requête. -
Session persistante (
transport.Session) conservée pour AliExpress : page d'accueil visitée une fois, cookies rejoués ensuite, pot dansvar/cookies/pour survivre à un redémarrage, jeté dès qu'une page revient tronquée. Aucun gain mesuré sur le blocage, gardée parce qu'elle rapproche le trafic de celui d'un navigateur. -
Piste non tranchée : si l'endpoint interne se referme un jour, sous-traiter la source à un service hébergé (omkar.cloud, 5 000 requêtes/mois gratuites), au prix d'un tiers qui voit nos recherches.
-
Cache consulté avant le rate limiter : rechiffrer un projet ne retape rien. TTL 1 h par défaut, mais jamais plus court que la coupure du disjoncteur (
Connector.effective_ttl, 6 h pour AliExpress). Sans ce plancher, un projet plus large que le budget d'une source ne peut pas se terminer : la relance d'après-coupure retrouve un cache vide, refait les mêmes premières recherches, rebrûle le budget et se fait rebloquer, indéfiniment. Sauf eBay, exclu du cache pour rester conforme à l'exemption (voir plus bas). -
Un projet dépassant le quota Leboncoin est traité par ordre d'enjeu décroissant, et les articles non traités sont nommés dans la restitution.
Économie de tokens
Le calcul reste en Python, seul le verdict remonte : sortie en texte tabulaire dense, une
meilleure offre neuve et une occasion par article, titres tronqués, URL nettoyées, notes
regroupées, diagnostics dans var/chineur.log. Mesuré : 10 articles ≈ 650 tokens.
Tests
pytest # 74 tests hors ligne, aucun réseau
pytest -m network # 5 tests d'intégration réels (consomment du quota)
Le moteur de panier est entièrement testable hors ligne, y compris ses cas piégeux (seuil de franco déclenché par le groupage, marchand unique, article sans offre, égalités). La propriété « total optimisé ≤ total naïf » est vérifiée sur 50 jeux d'offres aléatoires.
Mouser
Une seule variable : MOUSER_API_KEY. Clé gratuite en self-service sur mouser.com/api-hub
(onglet Search API).
La clé doit être créée depuis mouser.fr. La recherche par mot-clé n'accepte aucun
paramètre de devise (vérifié dans le swagger api.mouser.com/api/docs/V1) : la devise est
celle du site d'inscription. Une clé créée sur mouser.com renvoie des dollars, que le
connecteur écarte plutôt que de les convertir à un taux inventé — la source paraîtrait
simplement vide.
Quotas annoncés par Mouser : 50 pièces par appel, 30 appels/minute, 1 000 appels/jour.
D'où limit_per_hour = 60 et 2 s d'espacement : ce n'est pas de l'anti-ban, c'est le
respect du quota.
Trois choix de fond :
mouserPaysCustomsAndDuties: true— Mouser expédie du Texas. Sans ce drapeau, les droits de douane arrivent après coup et un prix « moins cher » sur le papier devient plus cher à la livraison.searchOptions: InStock— une référence à 12 semaines de délai n'a rien à faire dans une comparaison de prix pour un projet en cours.- Paliers de prix suivis à la quantité demandée. Retenir le palier 1 surestime tout achat groupé de composants passifs.
Les deux pièges qui fausseraient une comparaison, tous deux signalés en réserve sur l'offre : le minimum de commande (0,10 € l'unité par sachet de 100, soit 10 € réels à la caisse) et le format de prix (« 0,096 € » est un prix à trois décimales, « $1,250 » vaut mille deux cent cinquante — un parseur naïf se trompe d'un facteur mille dans les deux sens).
Port : non vérifié. Le franco de 50 € HT et les ~20 € en dessous sont recoupés sur le
web (2026-08-07), pas sur une facture. Ils vivent donc dans shipping.py et ressortent en
ESTIMATED, jamais en fait établi. À corriger dès la première commande réelle : sous le
franco, le port écrase le gain, et c'est ce seuil qui décide si la source sert à quelque chose.
eBay
EBAY_APP_ID (App ID) et EBAY_CERT_ID (Cert ID), pour un keyset Production — le seul
exploitable. Un keyset Sandbox se branche avec EBAY_ENV=sandbox, qui bascule sur
api.sandbox.ebay.com. En sandbox le connecteur reste éteint : l'inventaire est factice, les prix entreraient dans le panier sans que
rien ne le signale. EBAY_SANDBOX_OK=1 le rallume pour tester la plomberie uniquement.
Vérifié le 2026-08-06 sur un keyset Production : jeton client_credentials obtenu,
recherches réelles (29 offres sur « cable hdmi 2.1 2m », port exact lu dans
shippingOptions).
L'état de l'article se lit sur conditionId, jamais sur condition : ce dernier est
traduit par eBay (« Neuf », « Occasion », « Ouvert (jamais utilisé) » sur EBAY_FR), et la
table à clés anglaises d'origine ne matchait rien, donc tout ressortait en occasion.
Les articles 7000 (« pour pièces ») sont écartés : à 3 €, un article cassé gagnerait la
comparaison sans que rien ne le signale.
Conformité (« Your Keyset is currently disabled »). eBay n'active un keyset Production
qu'après abonnement aux notifications de suppression de compte, ou exemption. Le projet
prend l'exemption « je ne conserve pas de données eBay », et le code la rend littéralement
vraie : EBay.cacheable = False, donc aucune réponse eBay n'est écrite dans
var/chineur.db (elles contiennent des pseudos vendeurs). Un test le verrouille. Si un
jour on remet eBay en cache, l'exemption devient fausse : il faudra héberger l'endpoint de
notification à la place.
Licence
MIT, voir LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.