Address Validator API
API HTTP payante à la requête (protocole x402, prix par défaut
0.02 USDC) qui prend une adresse de livraison brute et renvoie une adresse
standardisée, ses coordonnées, un score de livrabilité et une clé de cache.
1. Présentation
- Entrée unique payante :
POST /validate — refuse en 402 Payment Required
tant qu'aucune preuve de paiement valide n'est fournie.
- Sortie : adresse standardisée (
line1, line2, city, state, zip,
country), corrigée par le fournisseur quand l'adresse est reconnue (D84 :
orthographe, ville, code postal ajouté), latitude/longitude (géocodage Nominatim), deliverability,
confidence, degraded et cache_key.
- Découverte :
GET /manifest décrit le service et son schéma pour un
annuaire x402 / MCP.
- Résilience : si le fournisseur est injoignable, l'API répond quand même
200 avec degraded: true — jamais une erreur qui laisserait croire à une
adresse invalide certaine.
Critères de succès de la spec, et où ils sont vérifiés automatiquement :
Ce que ce service ne fait pas : aucun rate limiting applicatif (D21) —
la limite est tenue au niveau edge par Nginx (nginx-hardening.conf : 10
requêtes/s par IP, rafale 20, 429 au-delà). Le paiement hérité (preuve HMAC à
secret partagé, D15) n'est plus accepté sur sa seule signature : depuis le
durcissement W1 (12/10/2026), la confirmation on-chain (D27) est active par
défaut et exige, sur Base, un transfert USDC confirmé vers le portefeuille
d'encaissement. Une preuve HMAC synthétique n'obtient un 200 que dans une
recette déclarée (TEST_MODE=true), et la ligne écrite est alors marquée
test_mode, donc exclue du revenu réel (D40). Tout est documenté (README.md
§7 et §9, SECURITY.md §3 et §6, AUDIT_TRAFIC_VPS.md §5) plutôt que passé
sous silence.
2. Démarrage rapide
Prérequis : Python 3.12 (le venv du dépôt tourne en 3.12.10, comme l'image
python:3.12-slim du Dockerfile). PostgreSQL et Redis sont facultatifs :
sans eux, l'API démarre et se dégrade proprement (/health le dit).
Démarrage conteneurisé : docker compose up -d (voir docker-compose.yml et
Dockerfile).
Déploiement en production (VPS Debian 12, Nginx + HTTPS, paiement x402) :
DEPLOIEMENT_VPS.md — compte rendu réel, écarts assumés par rapport au plan
et points de vigilance d'exploitation.
3. Configuration
Toutes les valeurs viennent de l'environnement ou de .env (pydantic-settings,
app/config.py). Une variable inconnue est ignorée (décision D8).
4. Endpoints et contrat de données
Chaque requête refusée en 402 est journalisée sous forme de métadonnées
(horodatage, endpoint, User-Agent, IP pseudonymisée) : jamais l'adresse
validée. Réglages et rétention : PRIVACY.md §3, DISCOVERY.md §4.
4.1 Revenu réel contre revenu du registre (D40)
/metrics/revenue publie trois lectures du même registre, jamais mélangées :
Les alias validations_paid et revenue_usdc (D31) pointent sur le réel.
Une transaction est marquée test_mode de deux façons, et de deux seulement :
TEST_MODE=true au moment où le service l'enregistre (recette en cours), ou
scripts/migrate_test_mode.py pour les lignes déjà écrites — seuil
TEST_MODE_CUTOFF_UTC (défaut 2026-10-11T12:00:00+00:00, les deux essais de
recette de ce jour-là). Rien n'est déduit du montant ni de l'adresse : une ligne
non marquée reste du revenu réel. /dashboard affiche le réel en avant et la
recette dans sa propre carte.
Sur un registre déjà écrit, le rattrapage se fait sans redéploiement :
venv/bin/python scripts/migrate_test_mode.py (simulation, ligne par ligne) puis
--apply. Le rejeu n'écrit plus rien (idempotent) ; le résultat se contrôle avec
curl -s http://127.0.0.1:8000/metrics/revenue.
Requête (app/schemas.py:11-16) : {"address": {"street", "city", "state", "zip", "country"}} — country vaut US par défaut, les autres champs sont
obligatoires (un champ manquant donne 422).
Réponse (app/schemas.py:19-37) : {"result": {"validated", "standardized", "latitude", "longitude", "deliverability", "confidence", "degraded", "cache_key"}, "price_paid", "transaction_id"}.
Pipeline de validation (app/validation/__init__.py:26-41) :
parse → standardize → verify (fournisseur) → geocode → score. Depuis D84, une
adresse reconnue repasse par parse → standardize sous la forme corrigée par le
fournisseur (corrige) avant le géocodage.
4.2 Deux façons de payer la même validation (D45)
POST /validate sert deux clients : la boutique abonnée, qui présente sa clé API,
et l'agent autonome, qui paie à l'appel (§5). La clé a la priorité : si les deux
sont envoyés, l'abonnement tranche et l'appel n'est pas encaissé en USDC.
La réponse porte X-Payment-Mode (api_key, x402 ou hmac) : sans lui, un client
ne peut pas distinguer un appel couvert par son abonnement d'un appel encaissé. En
mode clé, deux en-têtes disent aussi l'état du compte — X-Plan et
X-Quota-Remaining (absent pour un plan illimité, l'absence dit « illimité »).
Le quota est mensuel, comme publié sur /pricing : la fenêtre est le mois
calendaire UTC (accounts.month_start()), le compteur à vie reste affiché dans
/compte mais ne ferme aucune porte. Les corps de refus sont identifiables
({"mode": "api_key", "reason": "cle_inconnue" | "compte_expire" | "quota_atteint"}),
ne contiennent que le préfixe de la clé (sk_live_ab12) et indiquent la sortie
(upgrade → /pricing).
Un même appel sans en-tête X-API-Key reste facturé en x402 : le mode clé n'a rien
retiré aux agents autonomes. Compteurs publiés par /metrics : api_key_requests,
api_key_validations, api_key_denied, api_key_unavailable.
4.3 Essai gratuit public (D46)
GET /validate-form sert une page publique — indexable, cachable 5 minutes, et
autonome : aucun script, aucune ressource externe, la feuille de style
partagée (app/web.py). C'est la première page qu'un visiteur voit ; la CSP
(form-action 'self') rend vérifiable par le navigateur ce que le code promet.
Compteurs du plafond visibles dans /metrics : un refus de plafond compté est la
seule façon de distinguer « personne n'essaie » de « tout le monde est bloqué ».
4.4 Guide d'intégration public (D48)
GET /guide répond à la question que se pose un client qui vient de recevoir sa
clé : authentification (X-API-Key ou x402), contrat JSON exact, les huit
codes de retour avec l'action à mener, ce qui est mis en cache et ce qui est
plafonné, puis quatre cas pratiques (appel direct PowerShell, import CSV en
Python, Shopify, WooCommerce).
4.5 Email de bienvenue d'une boutique (D49)
Pas une page, un message : celui qu'on envoie à la boutique qui vient de créer
son compte. Il vit dans EMAIL_BIENVENUE.md, sous deux formes — le gabarit à
charger dans n8n (variables {{…}}, expliquées dans une table) et un exemplaire
prêt à envoyer pour le premier client (Tiitoune Art).
4.6 Suivi des clients (D50)
Un tableau de suivi ne vaut que par son accord avec ce qui est réellement servi.
Ce que la table des comptes contient déjà répond à la seule question qui décide du
revenu : quels comptes existent, lesquels se perdent en silence, et que faut-il
leur dire ? Les règles vivent dans app/suivi.py (états, seuils, ordre
d'urgence), l'exécution dans scripts/suivi_clients.py.
Vérification : .\.venv\Scripts\python.exe scripts\decisions_check.py D50 — la
porte installe une base réelle et compare l'état publié au refus que la route
applique, y compris le cas du client payant dont la date d'essai est passée.
4.7 Page d'accueil (D53)
GET / sert la vitrine à un navigateur et garde le JSON d'avant pour un programme
(Vary: Accept sur les deux). Le dessin vit dans app/templates/accueil/fr.html
(maquette d'origine : docs/maquettes/page-accueil/) ; ses chiffres (essai, quotas,
prix, prix x402, contact) sont injectés par app/routes/accueil.py depuis le code et
la configuration.
Vérification : .\.venv\Scripts\python.exe scripts\decisions_check.py D53.
4.8 Six langues (D57)
Le français reste à la racine ; les cinq autres langues ont leur préfixe. Toutes les
pages publiques existent dans les six : la vitrine, le guide (D58), l'essai gratuit (D59),
les tarifs (D60), l'inscription et l'espace client (D61), chacune à son adresse française
précédée du préfixe de la langue (/en/guide, /ar/signup…).
app/i18n.py fabrique les adresses (chemin(lang, page)), les nombres et les prix de
chaque langue (prix, mention_euros, taux de septembre 2026) et rend les gabarits
(rendre). Les montants en euros viennent d'accounts.OFFRES (aussi lisibles sous
pricing.OFFRES) : changer un prix sans
refaire la table convertie fait échouer tests/test_i18n.py et la porte D57. Les
photos sont servies par l'application sous /images/ : la CSP publique n'ouvre que
img-src 'self'.
Vérification : .\.venv\Scripts\python.exe scripts\decisions_check.py D57.
4.9 Agents IA et visites : mesurer au lieu de bloquer (D72, D73)
Le service classe chaque appelant en six familles (agent_ia,
annuaire_mcp, moteur, outil, navigateur, inconnu), journalise les
agents IA et les annuaires MCP dans le journal JSONL de D30 — jamais les visites
humaines — et publie cinq mesures (agents, top_ip, top_agents,
top_pages, conversion). Aucun agent n'est bloqué : un agent qui lit
/pricing est un prospect sans navigateur. La liste blanche des 25 agents IA
publiés par leurs éditeurs est dans docs/AGENTS-IA.md et dans la réponse de
/metrics/agents.
scripts/analyser_visites.py rend sept sections — chiffres clés, temps, top
pages, top IP, codes de statut, clients, anomalies (sondes, chemins sensibles,
fuites). Les adresses y sont pseudonymisées (SHA-256 + sel, D30) sauf
--ip-en-clair demandé explicitement ; une fuite est un chemin sensible servi en
2xx (un 301 renvoie ailleurs, il n'expose rien). Les deux analyseurs lisent la
même table de classification : l'endpoint et les rapports ne peuvent pas se
contredire sur ce qu'est un agent.
Vérification : .\\.venv\\Scripts\\python.exe scripts\\run_tests.py tests\\test_agents_ia.py tests\\test_visites.py.
4.10 Adresses françaises : Base Adresse Nationale (D79)
Pour un pays de PAYS_BAN (France métropolitaine et outre-mer : FR, RE,
GP, MQ, GF, YT, BL, MF, PM), la vérification passe par la Base
Adresse Nationale (app/validation/ban.py, https://data.geopf.fr/geocodage)
et jamais par SmartyStreets, qui ne sait pas juger une adresse française :
- score BAN ≥ 0,8, code postal retourné identique au code postal saisi,
ville identique (accents, casse, tirets et ponctuation normalisés) ;
- types
housenumber (0,99) et street (0,95) seulement — les autres
(municipality, locality) n'ont pas de voie reconnue ;
- une panne BAN rend
degraded: true, sans repli sur SmartyStreets ;
- côté américain, seul un code DPV
Y/S/D valide une adresse ; X et N
ne valident pas, et un candidat sans DPV rend degraded: true.
Les résultats France/outre-mer calculés avant ce correctif (30 jours de cache)
se purgent avec scripts/purger_cache_ban.py (simulation par défaut,
--apply pour supprimer). Mesure du 17/10/2026 : « 8 rue madelaine, Lyon,
75005 » passe de validated: true (cache) à validated: false, tandis que
Paris 75002, Lyon 69001 et Saint-Denis 97400 restent validées.
5. Exemple d'appel payant
- Construire une preuve (le client et l'API partagent
API_SECRET_KEY) :
- Appeler l'API :
- Sans preuve, ou avec une preuve périmée / malformée / falsifiée, la réponse
est
402 (app/payment.py:77-85) :
Le corps du 402 publié ci-dessus est un extrait : il porte aussi
x402Version, network, accepts[] (offre x402 v1) et désormais
accepts[0].outputSchema, le schéma Pydantic de ValidationResponse (D80).
Mesuré le 17/10/2026 : 1 788 octets de corps, en-têtes sous le tampon de 4 Ko
de nginx, et la même offre en v2 dans l'en-tête PAYMENT-REQUIRED.
- Avec une preuve valide, la réponse est
200. Exemple mesuré en recette
le 10/10/2026 sur ce poste (fournisseur injoignable, donc degraded) :
transaction_id reflète simplement l'en-tête X-Transaction-Id reçu
(app/routes/validate.py:67).
Même appel en production le 11/10/2026 (https://kder-ai.com, fournisseur
réel actif, preuve HMAC valide) :
deliverability est dérivé du code DPV renvoyé par le fournisseur (Y →
residential, scorer.py:17-23) et confidence intègre le bonus de géocodage
(0,99 + 0,05, borné à 1,0). Le metadata.rdi du fournisseur (Commercial sur
cette adresse) reste dans la charge brute, non exposée par l'API.
6. Cache, résilience et budget de latence
Latences mesurées sur ce poste le 10/10/2026 (PostgreSQL et Redis éteints,
réseau sortant actif, identifiants fournisseur vides donc réponse 401) :
Ces chiffres sont honnêtes et non flatteurs : la latence dégradée est dominée par
le fournisseur, dont les erreurs 4xx sont rejouées par le back-off (voir
TEST_REPORT.md, « Limites connues »). Les valeurs de référence enregistrées
dans le code (15,2 s avant D23, 6,03 s avant D22) n'ont pas été
re-mesurées aujourd'hui : elles sont citées comme historique dans
TEST_REPORT.md.
7. Paiement x402 : ce qui est vérifié, réglé, et ce qui reste hérité
Le service parle x402 v1 (spécification specs/transports-v1/http.md du dépôt
coinbase/x402) et règle réellement les paiements en USDC sur Base. Le corps du
402 est un document PaymentRequired :
Le client renvoie sa signature dans X-PAYMENT (base64 d'un PaymentPayload
{signature, authorization:{from,to,value,validAfter,validBefore,nonce}}) et
reçoit X-PAYMENT-RESPONSE (base64 de {success, transaction, network, payer}).
x402 v2 aussi (D77). Le même 402 porte l'offre en v2 dans l'en-tête
PAYMENT-REQUIRED (réseau eip155:8453, champ amount, extension bazaar du
catalogue Coinbase). Un client v2 paie par PAYMENT-SIGNATURE et reçoit
PAYMENT-RESPONSE. Sans aucun moyen de paiement, /validate répond 402 même
si le corps est vide : c'est ce que le contrôle du catalogue Coinbase attend.
Deux contrôles, dans cet ordre (app/x402.py) :
- local, hors ligne — le digest EIP-712 (
TransferWithAuthorization,
EIP-3009) est recalculé explicitement (eth_abi, eth_hash, eth_keys), la
signature récupérée est comparée à authorization.from, et destination,
montant, jeton, réseau et fenêtre de validité sont confrontés à l'offre
publiée. Rien ne sort du serveur avant que ce contrôle passe ;
- règlement —
POST /verify puis POST /settle au facilitateur (défaut :
celui de Coinbase, authentifié par un JWT CDP signé avec CDP_API_KEY_ID /
CDP_API_KEY_SECRET). Le hash du règlement alimente le registre durable
(D29) ; tout échec (refus explicite, panne, hash absent, nonce déjà réglé)
vaut un refus.
Le domaine EIP-712 annoncé (extra.name / extra.version) est celui du contrat :
scripts/test_x402_payment.py --check-domain le vérifie on-chain — mesuré le
11/10/2026 sur Base : name() = "USD Coin", version() = "2", decimals() = 6.
Ce qui reste hérité (D15). L'en-tête X-Payment-Proof
(tx_hash:timestamp:hmac_sha256) reste accepté pour l'outillage de l'exploitant
et les clients déjà intégrés : fenêtre symétrique de 300 s, HMAC comparé en temps
constant, et confirmation obligatoire du reçu sur Base (D27, active par défaut ;
seule une recette déclarée TEST_MODE=true s'en passe) par lecture JSON-RPC
(app/onchain.py, sans paquet web3 ni paquet de magasin de clés). Un paiement HMAC n'est jamais présenté comme un règlement x402 : il ne
porte pas X-PAYMENT-RESPONSE.
MCP. Le même outil est exposé en MCP sur POST /mcp (JSON-RPC 2.0, sans état,
sans flux SSE). Depuis D78, le paiement suit le transport MCP x402 v2 : un
tools/call sans paiement rend un résultat d'outil isError: true dont
structuredContent est le PaymentRequired v2 (et content[0].text le même
document, réseau CAIP-2, ressource mcp://tool/validate_address) ; le client met
alors son PaymentPayload v2 dans params._meta["x402/payment"] et reçoit le
règlement dans result._meta["x402/payment-response"]. Un 402 JSON-RPC n'est
plus émis par ce transport : les clients x402 MCP lisent un résultat, pas un
statut HTTP. La compatibilité D34/D37 est gardée (un client v1 paie par
X-PAYMENT et garde ses en-têtes de règlement), et settle_payment est l'unique
chemin de vérification/encaissement des deux transports. La carte
GET /.well-known/mcp-server est servie par l'application, donc elle ne peut pas
décrire un endpoint éteint. Publication annuaire : server.json,
.mcp.json, smithery.yaml (voir DISCOVERY.md §1).
Recette réelle. scripts/test_x402_payment.py a trois modes : plan (vérifie
que /manifest, le corps 402 et l'offre du transport MCP publient la même
offre), --check-domain (compare le domaine annoncé au contrat on-chain) et
--pay (paiement réel ; exige un portefeuille acheteur financé en USDC sur Base et
dépense 0,02 USDC).
8. Tests et vérification
Résultats mesurés le 10/10/2026 sur ce poste : 46 tests collectés — 45 passés, 1 ignoré (le test ignoré est un test d'intégration qui exige de vrais
identifiants SmartyStreets et le réseau) ; scripts/e2e_check.py →
22/22 vérifications OK. Détail complet, méthode et limites dans
TEST_REPORT.md.
Comment jouer le ledger, depuis la racine du projet :
9. Déploiement
deploy.sh cible un VPS Debian/Ubuntu et enchaîne : installation des paquets
(python3.12, PostgreSQL, Redis, Nginx, Certbot), clone dans /opt,
environnement virtuel, pip install -r requirements.txt, création de la base et
de l'utilisateur PostgreSQL, activation de Redis, copie de nginx.conf, obtention
du certificat (certbot --nginx), puis installation et démarrage du service
systemd. Le script est en set -euo pipefail (D20) : il s'arrête à la première
erreur.
Point d'attention : deploy.sh ne crée aucun fichier .env. Sans étape
manuelle, le service démarre avec les valeurs par défaut — donc
API_SECRET_KEY=change_me, le mot de passe PostgreSQL change_me, et aucun
identifiant SmartyStreets (le fournisseur répond alors 401 et toutes les
réponses sont dégradées). À faire immédiatement après le déploiement :
Variante conteneurisée : docker compose up -d (Dockerfile basé sur
python:3.12-slim, docker-compose.yml pour l'API et ses dépendances).
Déploiement du VPS de production (kder-ai.com) : scripts/deploy-kder-ai.sh
— le script réellement exécuté sur le serveur, versionné ici (avant le
11/10/2026 il ne vivait que sur le poste de travail et dans /opt, donc hors du
contrôle de version). Il reprend les 11 étapes ci-dessus et ajoute :
- le refus de déployer si
API_SECRET_KEY vaut encore change_me ou fait moins
de 32 caractères, ou si le .env est en CRLF ;
- l'installation de
nginx-hardening.conf dans /etc/nginx/conf.d/ : HSTS
(1 an, includeSubDomains), X-Content-Type-Options: nosniff,
Referrer-Policy: no-referrer, limit_req 10 req/s par IP (rafale 20) et
corps de requête borné à 16 ko. C'est volontairement hors du vhost, que
certbot --nginx réécrit à chaque renouvellement ;
- des contrôles qui échouent (ou signalent) si le durcissement a disparu :
écoute de
127.0.0.1:8000 uniquement (10.6), en-têtes de sécurité (10.7),
429 sur 60 appels simultanés (10.8) ;
- la compilation de Python 3.12 en repli quand le dépôt Debian n'en fournit pas.
Le compte rendu du déploiement réel (versions, journaux, écarts assumés,
correctifs du 11/10/2026) est dans DEPLOIEMENT_VPS.md.
Après mise en production : restreindre /metrics et /docs, activer un mot de
passe Redis, et vérifier que Nginx a bien installé la redirection HTTPS
(SECURITY.md §4 et §6).
10. Limites connues et suite
Suite recommandée, par ordre de valeur :
- Ne retenter que les erreurs réseau et 5xx du fournisseur : supprime le
back-off sur
401 et ramène la latence dégradée sous ~3 s.
- Consommer les preuves de paiement (anti-rejeu) et refuser le démarrage si
API_SECRET_KEY est resté à sa valeur par défaut.
- Brancher les métriques sur une supervision réelle et générer le
.env au
déploiement.