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 deblo

Le Step Zero ne suffisait pas : comment valider un constructeur sans valider le runtime a fait tomber toutes les sessions vocales de Déblo l’heure où nous avons livré le streaming caméra temps réel

La phase 14 a livré Déblo Eyes — streaming caméra temps réel via LiveKit vers Gemini Live native audio. Le premier deploy a fait tomber toutes les sessions vocales en production en quatre-vingt-dix secondes parce que notre Step 0 avait validé le constructeur sans exercer le runtime. Le build log de comment Déblo a eu des yeux, ce qu’un pré-vol incomplet a coûté, et quels points de polish ont été livrés ou reportés.

33 min May 20, 2026
debloclaude-opus-4.7claude-codegemini-live +25
Thales & Claude deblo

Le tiret cadratin qui a tué la production : comment un slogan marketing dans un header HTTP a fait tomber le chat de Déblo pendant 24 heures

Deux jours avant la soumission App Store, tout le produit chat de Déblo s’est cassé silencieusement. Pas de spinner, pas de toast, aucune erreur dans l’UI — juste un silence radio. L’incident de 24 heures se résumait à un seul « é » dans la valeur d’un header HTTP qui levait une UnicodeEncodeError avant qu’aucune requête vers OpenRouter ne quitte le backend. Post-mortem d’une fausse hypothèse, d’une trace Sentry, et d’un fix de six lignes qui a débloqué le lancement.

30 min May 19, 2026
debloclaude-opus-4.7claude-codeincident +19
Thales & Claude deblo

Six heures, d’une page blanche à la review Apple — Comment nous avons soumis Déblo à l’App Store, en direct

Marche à marche en direct de la soumission de Déblo à l’App Store iOS en six heures : ce que les validateurs d’Apple ont rejeté (un superscript Unicode), ce que nous avons corrigé (un Promotional Text gaspillé sur des marques tierces), et les rouages de l’ASO iOS que presque tout le monde rate.

30 min May 13, 2026
debloclaude-opus-4.7claude-codeapp-store +16