Configure
the providers.
Watch it learn.
The hands-on half DOC leaves out: how to add providers, how amount limits and the business multiplier decide who's eligible, the three ways to pull a provider out of rotation, and the CLI simulator walkthrough the doc folder confusingly files under "testing."
Configure your providers
Everything routing depends on starts here. Each provider is a row in the SQLite
provider_config table, created and edited from
/admin/providers.
Before the simulator below means anything, you need providers. Walk through it once:
-
Start the services
terminaldocker compose up -d dragonfly bun run dev # wait for "Server running on port 3000"
-
Open the admin dashboard
Go to
http://localhost:3000/adminand sign in with Google. Then click Providers in the sidebar (/admin/providers). -
Add three providers
Click Add Provider and fill in each. These are the values the rest of this doc and the simulator assume:
name type min max currencies mult stripe card 100 1,000,000 NGN, USD 1.0 paystack card 100 500,000 NGN 1.0 flutterwave card 500 2,000,000 NGN, USD, GBP 1.0 -
Verify
terminal$ curl http://localhost:3000/api/health {"status":"healthy","providers":["stripe","paystack","flutterwave"]}
The provider config model
ProviderConfiginterface ProviderConfig { name: string; // unique id enabled: boolean; // active without deleting type: string; // "card", "bank_transfer", … currencies: string[]; // ["NGN", "USD"] amountLimits: { // per-currency min/max [currency: string]: { min: number; max?: number }; }; businessMultiplier: number; // 0–10, default 1.0 priority: number; // default 0 }
Try it: who's eligible, and at what share?
This is the eligibility filter and weight blend, live. Move the amount, switch the currency, drag the business multipliers — the bars show which providers can take the transaction and how the router would split traffic among them.
Base weights are illustrative (stripe 0.72 · paystack 0.65 · flutterwave 0.45, from the WIKI's "after 50 transactions" example). Ineligible providers drop to 0% and the rest renormalize.
Amount limits & the business multiplier
Two knobs decide who can take a transaction (amount limits) and how much of the eligible traffic they get (business multiplier).
Amount limits = a hard gate
Eligibility is a simple filter applied before any weighting — fall outside a provider's range and it's skipped entirely:
eligibility filterif (amount < provider.minAmount) continue; // too small if (provider.maxAmount && amount > provider.maxAmount) continue; // too big
With the three providers from chapter 01, that produces:
| amount | eligible providers | why |
|---|---|---|
| ₦50 | none | below every minimum |
| ₦1,000 | all three | within every range |
| ₦750,000 | stripe, flutterwave | exceeds paystack's 500k max |
| ₦1,500,000 | flutterwave | only one reaches to 2M |
Use it to match provider capability to transaction size — fee structures, per-provider caps, and so on. (Scrub the amount slider in the chapter 01 simulator to watch these exact transitions.)
Business multiplier = a soft thumb on the scale
Once a provider is eligible, the multiplier scales its computed weight:
weightingfinalWeight = baseWeight * businessMultiplier;
| value | effect |
|---|---|
| 1.0 | normal weight (default) |
| 2.0 | double the traffic allocation |
| 0.5 | half the traffic allocation |
Reach for it to:
- Favor providers with better commercial terms.
- Gradually shift traffic during a migration.
- A/B test provider performance at different traffic levels.
The multiplier never makes an ineligible provider eligible — the amount filter runs first and is absolute. Multiplier only redistributes traffic among the providers that already passed the gate. The chapter 01 simulator shows both at once: the eligibility badge is the gate, the multiplier slider is the nudge.
Three ways to stop traffic
Open Circuit, Disable, and Delete look similar in the UI but differ in duration and in what they keep. Picking the wrong one is how you lose a provider's metrics by accident.
| action | where | duration | keeps config + metrics? |
|---|---|---|---|
| Open Circuit | dashboard / API | temporary — auto-recovers | yes |
| Disable | Providers page | until re-enabled | yes |
| Delete | Providers page | permanent | no — gone |
Open Circuit — a quick, temporary blip
For a provider having live issues. It auto-recovers through half-open back to closed. Drive it from a dashboard card or the API:
curl$ curl -X POST http://localhost:3000/api/providers/stripe/circuit/open \ -d '{"reason":"provider degraded"}' $ curl -X POST http://localhost:3000/api/providers/stripe/circuit/close
Disable — longer-term, reversible
For maintenance or a contract pause. The provider stays in the database with all config and metrics; re-enable any time and it resumes with its existing weight. This is the safe default for "stop, but I'll want it back."
Delete — permanent
Removes the provider and everything about it. Use with caution; there's a confirmation dialog for a reason.
The CLI simulator walkthrough
This is what the doc folder's vi.TESTING.md actually contains — not a
test-suite guide, but a hands-on walkthrough for driving synthetic traffic at your real
router and watching the weights move.
In the source docs this material is filed under "testing," which is misleading: it's a simulator walkthrough, not the unit-test suite. (For the actual 67-test suite, see DOC chapter 05.) This chapter keeps the content but gives it its real name.
Run it
-
Services up, providers configured
You did this in chapter 01:
docker compose up -d dragonfly,bun run dev, and three providers added. -
Start the simulator
terminalbun run simulate # defaults: 2 TPS, 120s, verbose
-
Watch live in the dashboard
Open
http://localhost:3000/admin. Weights, success rates, and circuit states update over SSE; thanks tohx-boostthe live data survives page navigation.
Options
| flag | default | what it does |
|---|---|---|
--tps |
2 | transactions per second |
--duration |
120 | seconds to run; 0 = forever |
--verbose |
off | log every transaction |
examplesbun run scripts/simulate.ts --tps 10 --duration 60 # high volume bun run scripts/simulate.ts --tps 5 --duration 0 --verbose bun run scripts/simulate.ts --tps 20 --duration 30 # quick burst
What each tick does
Every 1000/TPS ms the simulator generates one transaction (random amount
₦100–₦10,000) and runs it through the real API:
per transaction1. GET /api/route // ask for a provider 2. simulate the provider response // success/latency/async from mock rates 3. POST /api/metrics/report // report the outcome 4. (async) confirm via webhook // after a short delay
The mock provider profiles
Edit scripts/simulate.ts to change how each provider "behaves":
scripts/simulate.tsconst SIMULATED_PROVIDERS = { default: { successRate: 0.85, latencyMs: 500, asyncRate: 0.2 }, stripe: { successRate: 0.95, latencyMs: 300, asyncRate: 0.1 }, paystack: { successRate: 0.9, latencyMs: 400, asyncRate: 0.3 }, flutterwave: { successRate: 0.8, latencyMs: 600, asyncRate: 0.2 }, };
successRate is the probability of success (0–1), latencyMs the
average response time, asyncRate the chance of returning "pending" (needs a
webhook).
What you'll see in the terminal
stdout[Simulator] Starting simulation at 5 TPS... [Tx abc123] Routed to stripe (weight: 0.72) - SUCCESS in 287ms [Tx def456] Routed to paystack (weight: 0.65) - PENDING (webhook) [Tx ghi789] Routed to flutterwave (weight: 0.45) - FAILED in 1203ms [Webhook] def456 confirmed for paystack
Weights converge as data accumulates
| after | stripe | paystack | flutterwave |
|---|---|---|---|
| 0 tx (priors) | 0.60 | 0.60 | 0.60 |
| ~50 tx | 0.72 | 0.65 | 0.45 |
| ~200 tx | 0.78 | 0.62 | 0.38 |
Stripe pulls ahead (best success + lowest latency), but the 5% exploration keeps probing the others so a recovery is never missed.
Three things worth simulating
-
Normal operation —
--tps 5 --duration 60; watch weights stabilize. -
Degradation — drop a provider's
successRatein the script, restart, watch its weight fall. -
Circuit breaker — set a
successRateto 0, watch the circuit open, then raise it and watch recovery.
Prefer a picture? Use the web simulator
bun run simulator starts a self-contained app on
port 3001
with live charts and one-click failure scenarios (baseline, provider failure, high
latency, partial outage, recovery). Same router code, mock providers — the visual way to
see the algorithm work. The CLI tool, by contrast, drives your real
configured providers.
Troubleshooting
-
"No available providers" — add at least one enabled provider at
/admin/providers. -
Simulator won't connect — confirm
bun run devand Dragonfly are up; check/api/health. -
Weights not moving — run longer or raise
--tps; Bayesian learning needs volume.