Problem Relevance · 20%
Duro is aimed at a real, expensive, local problem most platforms ignore: involuntary churn. A renewal fails because a salary-backed debit card is empty on the wrong day — the customer wanted to stay, but the money didn’t move. It’s 20–40% of all churn, and worse where cards are debit and salaries land once a month. The entire product is organised around recovering that money: a payday-aware retry engine, rail-switching, a live “money at risk → recovered” ledger as the merchant’s hero metric. This isn’t a feature bolted onto a billing CRUD — recovery is the thesis. Read it →Technical Execution · 25% — the heaviest weight
The depth is in the engine, and it’s real, not a stub.A genuine billing loop
A 60-second scanner finds due subscriptions;
BillingService.renew invoices, charges, advances the period, handles trial-to-active, maxCycles, scheduled plan changes, and cancel-at-period-end — with month-end date clamping.An explicit state machine
Nine states, a transition table, illegal transitions that throw. The table is also the source of truth for webhook event names, so state and events can’t drift.
A real worker topology
Eight BullMQ workers, seven repeatable scanners (billing, reminder, dunning, webhook, checkout-recovery, mandate-activation, payout), one consistent scan→enqueue→process pattern, dedup by job id, graceful shutdown.
A typed, OOP monorepo
strict TypeScript, no any, no free functions in business logic, exact version pins — and an integration suite that runs the whole money loop against real Postgres on every push.Security & Reliability · 20%
Isolation by construction, plus the results of a deliberate adversarial audit.- Tenant isolation that can’t be forgotten —
testandliveare separate Postgres schemas, not amodecolumn. Every repository is tenant-scoped at construction; idempotency keys are per-tenant. → - Real findings, fixed — a HIGH-severity webhook SSRF-to-metadata vector, a cross-tenant idempotency replay, an unenforced blacklist, mode-scoping leaks — all found, fixed, and tested. →
- Reliability in the runtime — single-flight caching (no stampede), retries with backoff on every queue, the
in_flightguard that prevents double-charges, graceful shutdown with a hard backstop. - Secrets done right — keys and OTPs hashed at rest, timing-safe compares, HMAC-signed webhooks, real credentials only in gitignored env.
Product UX & Clarity · 15%
Clarity for two audiences — the developer integrating, and the merchant operating.API ergonomics
Prefixed IDs, integer minor-units, one error envelope, keyset pagination (page 500 = page 1), per-tenant idempotency, mode implied by the key — and an interactive playground on every endpoint.
Operator clarity
The recovery command center renders “at risk → recovering → recovered,” a per-failure breakdown, and a per-subscription retry timeline. The number that matters is the first thing the merchant sees.
Brand once, everywhere
A draft/publish appearance builder themes checkout, the popup, the portal, and emails from one token set.
Customer self-service
One WhatsApp login, every subscription across every merchant, pause/cancel/fix-card — with ownership enforced.
Payment Integration Depth · 20%
Payments are modelled deeply, not as a single “charge” call.- Rails are first-class. Card, bank transfer, USSD, virtual account, direct-debit mandate, and wallet are modelled as distinct rails, and recovery relays across them when one fails, in a merchant-configurable order. →
- A real, live gateway, behind a clean boundary. All charging goes through a single
ChargeGatewayinterface (@duro/payments); the worker wires theNombaChargeGateway(@duro/nomba-client), which performs real Nomba tokenised-card charges (/v1/checkout/tokenized-card-payment), direct-debit mandate debits (/v1/direct-debits/debit-mandate), refunds (/v1/checkout/refund), and bank-transfer payouts (/v2/transfers/bank). ASimulatedChargeGatewayimplements the same interface but is used only by the integration test suite. → - Money-in and money-out are both built. Wallet top-ups, per-customer Nomba virtual accounts, merchant payouts to bank accounts with a T+1 available/pending split, and refunds all run against Nomba today. →
- Tokens belong to the customer. A saved card tokenises to an email-keyed global account, reusable across every merchant, deep enough to power a cross-merchant wallet. →
Charging runs live against Nomba through the
ChargeGateway abstraction. Duro runs two merchant-selected environments: test hits Nomba’s sandbox host (sandbox.nomba.com) and live hits production (api.nomba.com), selected by the API-key prefix or the x-duro-mode header. The full loop, renewal, decline, dunning, recovery, webhooks, payouts, is exercisable end to end in both. Only the internal integration test suite swaps in a deterministic SimulatedChargeGateway so CI can assert every branch without external calls; a few operations (refunds, payouts) also short-circuit to a deterministic result in test mode by design.Nomba Integration Depth
Duro integrates 30 distinct Nomba API endpoints through one typed client (packages/nomba-client/src/client.ts) and its token manager. That is the breadth behind “Payment Integration Depth”: tokenized-card charges, direct-debit mandates, transfers/payouts, customer virtual accounts, transaction verification, refunds, and the full bills/VAS surface, all real calls.
The full per-endpoint list, with every method, path, and purpose, is on the Nomba API surface page.
Start with the thesis, or jump straight to the recovery engine where the heaviest-weighted execution lives.