Back to 0fee
0fee

Le moteur de routage intelligent des paiements

Comment 0fee.dev route les paiements à travers 117 méthodes et 53+ fournisseurs grâce à une sélection par priorité avec repli automatique. Par Juste A. Gnimavo et Claude.

Juste A. Gnimavo (Thales) & Claude | March 27, 2026 6 min 0fee
EN/ FR/ ES
routingpaymentsafricamobile-moneyarchitecture

Quand un marchand envoie une demande de paiement à 0fee.dev, quelque chose doit décider quel fournisseur la traite. Pas « Stripe ou PayPal » -- c'est un simple choix binaire. La vraie question est : pour un paiement Orange Money en Côte d'Ivoire, le système doit-il utiliser PaiementPro (priorité 1), PawaPay (priorité 2) ou Hub2 (priorité 3) ? Et si PaiementPro est en panne, doit-il automatiquement basculer vers PawaPay ? C'est le moteur de routage.

Le problème central

0fee.dev couvre 53+ fournisseurs dans 200+ pays. Pour de nombreuses méthodes de paiement, plusieurs fournisseurs peuvent traiter la même transaction. En Afrique francophone, un paiement Orange Money en Côte d'Ivoire peut être traité par PaiementPro, PawaPay, Hub2 ou BUI. Chacun a des frais différents, des historiques de fiabilité différents et des lacunes de couverture différentes.

Le moteur de routage doit :

  1. Découvrir quelles méthodes de paiement sont disponibles pour un pays donné.
  2. Sélectionner le meilleur fournisseur pour une méthode de paiement donnée.
  3. Basculer vers le fournisseur suivant si le premier échoue.
  4. Respecter les fournisseurs configurés et les identifiants du marchand.

Découverte des méthodes de paiement par pays

La première étape de tout paiement est de découvrir les méthodes de paiement disponibles. Cela est piloté par la table payin_methods :

sqlCREATE TABLE payin_methods (
    id TEXT PRIMARY KEY,
    code TEXT UNIQUE NOT NULL,     -- ex. "PAYIN_ORANGE_CI"
    name TEXT NOT NULL,            -- ex. "Orange Money Ivory Coast"
    country_code TEXT NOT NULL,    -- ex. "CI"
    currency TEXT NOT NULL,        -- ex. "XOF"
    type TEXT NOT NULL,            -- ex. "mobile_money"
    operator TEXT,                 -- ex. "Orange"
    is_active INTEGER DEFAULT 1,
    min_amount INTEGER,
    max_amount INTEGER,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

Pour la Côte d'Ivoire (CI), cela renvoie :

CodeNomTypeOpérateurDevise
PAYIN_ORANGE_CIOrange Money Ivory Coastmobile_moneyOrangeXOF
PAYIN_MTN_CIMTN Mobile Money Ivory Coastmobile_moneyMTNXOF
PAYIN_WAVE_CIWave Ivory Coastmobile_moneyWaveXOF
PAYIN_MOOV_CIMoov Money Ivory Coastmobile_moneyMoovXOF
PAYIN_CARD_GLOBALCard Paymentcard-Multiple

Sélection de fournisseur par priorité

Une fois la méthode de paiement choisie, le moteur de routage détermine quel fournisseur la traite. Le système de priorité à trois niveaux fonctionne ainsi :

PrioritéSignificationExemple
1Fournisseur principal -- utilisé en premierPaiementPro pour PAYIN_ORANGE_CI
2Fournisseur secondaire -- utilisé si la priorité 1 échouePawaPay pour PAYIN_ORANGE_CI
3Fournisseur tertiaire -- dernier recoursHub2 pour PAYIN_ORANGE_CI

La colonne provider_method_code est essentielle. Chaque fournisseur utilise ses propres codes internes pour la même méthode de paiement. PaiementPro appelle Orange Money en Côte d'Ivoire « OMCIV2 ». PawaPay l'appelle « ORANGE_CIV ». Hub2 l'appelle simplement « Orange » avec un paramètre pays. La table de routage mappe le code unifié (PAYIN_ORANGE_CI) vers le code interne de chaque fournisseur.

L'algorithme de routage

La fonction de routage principale interroge la table provider_routing, ordonne par priorité et renvoie le meilleur fournisseur disponible :

pythonasync def get_provider_for_unified_method(
    payment_method: str,
    app_id: str,
    environment: str = "production",
    skip_providers: list[str] = None
) -> dict | None:
    skip_providers = skip_providers or []

    with get_db() as conn:
        routes = conn.execute(
            """
            SELECT pr.provider_id, pr.provider_method_code, pr.priority
            FROM provider_routing pr
            WHERE pr.payin_method_code = ?
              AND pr.environment = ?
              AND pr.is_active = 1
              AND pr.provider_id NOT IN ({})
            ORDER BY pr.priority ASC
            """.format(",".join("?" * len(skip_providers))),
            (payment_method, environment, *skip_providers)
        ).fetchall()

        if not routes:
            return None

        for route in routes:
            has_creds = conn.execute(
                """
                SELECT 1 FROM provider_credentials
                WHERE app_id = ? AND provider_id = ?
                  AND environment = ? AND is_active = 1
                """,
                (app_id, route["provider_id"], environment)
            ).fetchone()

            if has_creds:
                return {
                    "provider_id": route["provider_id"],
                    "provider_method_code": route["provider_method_code"],
                    "priority": route["priority"],
                }

        return None

Logique de repli

Quand un paiement échoue au niveau du fournisseur, le système peut réessayer avec le fournisseur suivant dans la chaîne :

pythonasync def initiate_payment_with_fallback(
    payment_method: str,
    payment_data: dict,
    app_id: str,
    environment: str,
    max_attempts: int = 3
) -> tuple[InitPaymentResult, str]:
    skip_providers = []

    for attempt in range(max_attempts):
        route = await get_provider_for_unified_method(
            payment_method, app_id, environment,
            skip_providers=skip_providers
        )

        if not route:
            break

        provider_id = route["provider_id"]
        credentials = await get_decrypted_credentials(
            app_id, provider_id, environment
        )
        provider = provider_registry.get_instance(
            provider_id, credentials, app_id
        )

        payment_data["provider_method_code"] = route["provider_method_code"]
        result = await provider.initiate_payment(payment_data)

        if result.status != "failed":
            return result, provider_id

        skip_providers.append(provider_id)

    return InitPaymentResult(
        provider_ref="",
        status="failed",
        instructions="Aucun fournisseur disponible n'a pu traiter ce paiement"
    ), ""

Considérez le repli en pratique pour un paiement PAYIN_ORANGE_CI :

  1. Tentative 1 : Essayer PaiementPro (priorité 1). PaiementPro renvoie une erreur de timeout.
  2. Tentative 2 : Ignorer PaiementPro, essayer PawaPay (priorité 2). PawaPay initie le paiement avec succès.
  3. Le marchand et le client ne savent jamais que PaiementPro a été tenté en premier.

La table de routage : 117 méthodes de paiement

La table de routage complète mappe 117 méthodes de paiement dans 30+ pays vers leurs configurations de fournisseurs. Le pattern est cohérent : pour l'Afrique de l'Ouest francophone (zone UEMOA), PaiementPro est généralement en priorité 1 car il offre les meilleurs tarifs pour le mobile money local. PawaPay couvre le plus large éventail de pays comme option secondaire. Hub2 sert de repli tertiaire.

Le moteur de routage comme levier commercial

Au-delà du routage technique, le système de priorité est un outil commercial. Si 0fee.dev négocie un meilleur tarif avec un nouveau fournisseur, ajuster la table de routage change quel fournisseur gère le trafic -- sans modification de code, sans intervention du marchand et sans temps d'arrêt. Une seule instruction UPDATE déplace le volume de paiements d'un fournisseur à un autre.

C'est la puissance d'un moteur de routage dans un orchestrateur de paiement. Il transforme la sélection de fournisseur d'une décision codée en dur en un processus configurable et piloté par les données, optimisable en continu.


Cet article fait partie de la série « Comment nous avons construit 0fee.dev ». 0fee.dev est un orchestrateur de paiement couvrant 53+ fournisseurs dans 200+ pays, construit par Juste A. GNIMAVO et Claude depuis Abidjan sans aucun ingénieur humain. Suivez la série pour l'histoire complète de la construction.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude thales

Treize agents, quarante-trois minutes : la première session Workflow de Claude Fable 5, et ce qu'un script d'orchestration déterministe change aux builds multi-agents

Un prompt, treize agents, quarante-trois minutes : la première session de production avec Claude Fable 5 et l'outil Workflow de Claude Code a livré un site web de production complet de sept pages plus un endpoint backend de capture de leads, en un seul commit. Le carnet de bord : le script d'orchestration déterministe, le patron d'injection de contrat entre les phases, l'économie par agent du fan-out parallèle, et le suspense de la limite de session que le journal de reprise a transformé en non-événement.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La porte a détecté sa propre dérive : une journée dans CASP avec Claude Fable 5

Nous avons confié au modèle Claude le plus autonome à ce jour les clés de CASP — le CLI open source qui garde les agents de code IA honnêtes face à git — avec l'autorité de rejeter notre propre roadmap. Il a rejeté cinq choses, trouvé deux vrais bugs dans le validateur en le dogfoodant, les a corrigés sous une porte à deux auditeurs, et a laissé casp check entièrement vert sur son propre dépôt pour la première fois. CASP 0.3.0 en est le résultat.

16 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9
Thales & Claude zerosuite

La transplantation du CASP : comment la discipline des six fichiers est passée de Conductor à un ERP transport anti-fraude, ce que la compétence /next ajoute quand l'opérateur tape juste « next », et pourquoi le coût d'une dérive du CASP grimpe quand le projet, c'est l'argent des autres

La discipline du CASP qui a piloté trente-cinq sessions de Conductor est agnostique au produit. Le carnet de bord de sa transplantation sur KASSIA, un ERP transport anti-fraude pour un exploitant de flotte en Côte d'Ivoire : ce qui a migré, ce qui n'a pas migré (le validateur sur mesure — et ce que son absence coûte), ce que la compétence /next ajoute quand l'opérateur tape un seul mot, et là où le CASP s'arrête — le bug de déploiement qu'il ne pouvait pas voir parce qu'il enregistre l'intention, pas la réalité de l'infrastructure.

23 min Jun 8, 2026
kassiaerp-kassia-transport-logistiquezerosuiteCASP +15