ADR-0011 — La porte KYC du marketplace est la signature de l'APA par l'acheteur
Le pipeline KYC « enhanced » de la V5.1 existe en entier : la table KycVerification, le chiffrement at-rest du rapport provider, le client Stripe Identity, le verdict manuel admin, POST /api/marketplace/kyc/start, et…
Statut
Accepté · 2026-09-06
Piliers : marketplace
Contexte
Le pipeline KYC « enhanced » de la V5.1 existe en entier : la table
KycVerification, le chiffrement at-rest du rapport provider, le client
Stripe Identity, le verdict manuel admin, POST /api/marketplace/kyc/start,
et une banniere KycBanner sur /account/deals/[id].
Il ne conditionnait rien. grep -rn "isKycVerified" src ne rendait que
sa definition ; son commentaire disait « cheap call pour gating » et
l'en-tete du module annoncait « Deals > $10k ou on a besoin d'un check
enhanced ». Aucun chemin de src/ ne comparait un montant a un seuil pour
demander un KYC (marketplace/0441).
L'item d'audit decrivait ca comme « aucune porte, il faut trancher une politique ». Verification faite, la politique existait deja, ecrite dans un endroit ou elle ne pouvait pas s'appliquer :
// src/app/(dashboard)/account/deals/[id]/page.tsx
{!isSeller && (deal.agreedAmount ?? 0) > 1_000_000 ? <KycBanner … /> : null}
Le seuil ($10 000), l'acteur (l'acheteur) et l'etape (le deal) etaient donc tranches depuis la V5.1. Ce qui manquait n'etait pas une decision produit, c'etait un serveur qui l'applique — et la regle du pilier est explicite : une verification de role dans l'UI n'est pas une verification. Un acheteur qui n'a jamais charge cette page, ou qui appelle l'endpoint directement, signait comme un acheteur verifie.
Deux contraintes bornent la reponse :
- Stripe Connect verifie deja le vendeur. L'onboarding Express fait une
verification d'identite, et
cron/marketplace-payoutsrefuse de liberer l'escrow tant queStripeConnectAccount.status !== "ENABLED". La porte du vendeur existe : elle est a la sortie de l'argent, pas ici. - Une porte posee trop tot est une porte que personne ne franchit. Demander un passeport pour lire un NDA arrete la diligence avant que l'acheteur ait une raison de le donner.
Décision
Une seule porte : la signature de l'APA par l'ACHETEUR, sur un deal dont
agreedAmount depasse KYC_REQUIRED_ABOVE_CENTS ($10 000), exige un
KycVerification au statut VERIFIED. Sinon les deux chemins qui posent
apaSignedAt levent kyc-required, que leur route rend en 409.
Le seuil est exporte depuis services/marketplace/kyc.ts et la page le lit
la : la banniere et la porte partagent le meme nombre.
Le vendeur n'est pas gate. Le NDA et la LOI ne sont pas gates.
Pourquoi l'APA et pas ailleurs
| Etape candidate | Pourquoi non |
|---|---|
| L'offre | agreedAmount n'existe pas encore : rien a mesurer. Et gater l'entree du funnel sur un document d'identite tue le funnel. |
| Le NDA | Il ouvre la data room, il ne transfere rien. Trop tot : l'acheteur n'a pas encore de raison de se verifier. |
| La LOI | Non contraignante. Meme argument. |
advanceDealStage (APA_SIGNED → ESCROW_FUNDED) | La transition est ouverte aux deux parties (by: "either"). Gater sur le KYC de l'acheteur y bloquerait aussi le vendeur, et un vendeur qui clique en premier contournerait la porte. Le point est ambigu par construction. |
TRANSFERRING → CLOSED | L'argent a deja bouge. Une porte a la fin n'est pas une porte. |
| Le payout vendeur | Bloquer l'escrow sur un KYC BoostEcom immobiliserait de l'argent deja encaisse chez des vendeurs qui ont deja passe la verification Stripe. C'est un doublon qui cree un incident, pas un controle. |
L'APA est le seul point qui reunit les trois proprietes : le montant est
connu, l'acte est contraignant, et la FSM du deal exige apaSignedAt pour
sortir de DILIGENCE — donc refuser la signature arrete reellement le
pipeline au lieu de le decorer.
Alternatives écartées
| Option | Pourquoi non |
|---|---|
| Gater l'acheteur ET le vendeur | Casse les vendeurs existants sans remede : la KycBanner n'est rendue que cote acheteur, donc un vendeur bloque lit kyc-required dans un toast et n'a aucune surface pour s'y conformer. La verification du payee est par ailleurs deja faite par Connect. A rouvrir le jour ou une banniere vendeur existe (marketplace/0461, ouvert par cette PR). |
| Option 2 de l'item : « c'est un outil manuel d'operateur, on le dit dans la doc » | Ce serait aligner la doc sur le moins-disant alors que le produit affiche deja une garantie a l'acheteur (« required over $10k »). On ne retire pas une promesse affichee, on la tient. |
| Option 3 de l'item : retirer la surface | La chaine est complete et fonctionnelle ; la jeter pour la reconstruire au premier deal a sept chiffres serait du gaspillage pur. |
| Un flag d'environnement pour un deploiement progressif | Un flag qui vaut false par defaut, c'est la porte ouverte avec une ligne de config en plus. Et il n'y a personne a menager : la population gatee est exactement celle a qui la banniere s'affiche deja, sur l'ecran meme ou le bouton « Start verification » se trouve. |
| Mettre la porte dans la route plutot que dans le service | La route est une porte, pas la serrure — meme regle que advanceDealStage. Une seconde entree (webhook provider, action admin) contournerait un controle pose dans le handler HTTP. |
Conséquences
- Un acheteur non verifie sur un deal a plus de $10 000 est bloque a la
signature de l'APA. Le deal reste en
DILIGENCE. Le remede est sur le meme ecran ; ce n'est pas un cul-de-sac. IN_REVIEWne passe pas. « On a demande » n'est pas « on sait » : accepterIN_REVIEWferait de la porte une formalite qu'on franchit en cliquant une fois.- Le code d'erreur
kyc-requiredremonte tel quel dans le toast duSignaturesPanel, commeforbiddenetsignature-not-pending-*avant lui. Un libelle traduit est une amelioration, pas un prealable. - Un deal a exactement $10 000 n'est pas gate : le comparateur est
>, comme la condition de la banniere. Un acheteur a la frontiere n'a jamais ete averti, donc il ne peut pas etre bloque. - Dette assumee : le vendeur n'a pas de KYC enhanced. Le signal qui doit faire revisiter ce choix est un litige ou l'identite du vendeur est en cause, ou une obligation AML qui vise le cedant.
Comment c'est appliqué
Deux chemins ecrivent apaSignedAt, donc la porte est posee sur les deux.
C'est le point qui distingue un controle d'une decoration : signSignature
(V5.1) et markLegalDocSigned (V2, le click-through cable a
POST /api/vendor/marketplace/deals/[id]/sign et sell/deals/[id]/[doc])
posent la meme colonne, et c'est cette colonne que la FSM du deal lit pour
sortir de DILIGENCE. Gater le premier seul aurait livre une porte avec un
bouton a cote.
Que ces deux chemins existent est un defaut a part entiere, suivi par
marketplace/0443 ; cet ADR ne le resout pas, il refuse seulement d'en
dependre.
src/services/marketplace/kyc.ts—KYC_REQUIRED_ABOVE_CENTSetdealRequiresBuyerKyc, la seule definition du seuil.src/services/marketplace/legal-signatures.ts— la porte danssignSignature, avant toute ecriture.src/services/marketplace/seller.ts— la meme porte dansmarkLegalDocSigned, apres les controles de partie et de statut.src/services/marketplace/legal-signatures.test.ts(9 tests) etsrc/services/marketplace/legal-doc-click-through.test.ts(8 tests) — refus sans KYC /IN_REVIEW/REJECTED, passageVERIFIED, et les non-regressions qui rendent bruyant un elargissement de la porte (vendeur, NDA, LOI, montant sous le seuil).src/app/(dashboard)/account/deals/[id]/page.tsx— la banniere litdealRequiresBuyerKyc, plus un litteral local.- Les deux routes rendent
kyc-requireden409.