| Dimension | Weight | Where Duro earns it |
|---|---|---|
| Problem Relevance | 20% | The thesis — recovery-first billing for salary-backed debit cards |
| Technical Execution | 25% | The billing engine + recovery engine |
| Security & Reliability | 20% | Security model + multi-tenancy |
| Product UX & Clarity | 15% | API ergonomics + appearance + portal |
| Payment Integration Depth | 20% | Rails & the gateway abstraction |
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
Five BullMQ queues, four repeatable scanners, 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, and direct-debit mandate are modelled as distinct rails — and the recovery engine relays across them when one fails, in a merchant-configurable order. →
- A clean gateway boundary. All charging goes through a single
ChargeGatewayinterface, so the live rail drops in behind the same callers the simulator runs today. Tokenisation, charge, and transfer surfaces map onto it. - Tokens belong to the customer. A saved card tokenises to a phone-keyed identity, reusable across every merchant — the integration is deep enough to power a cross-merchant wallet. →
- Reconciliation is wired in. A dedicated payouts/reconcile queue is already in the worker topology, ready for the live rail in July.
The live payment rail lands in the July window; everything charges through a deterministic simulator today so the full loop — renewal, decline, dunning, recovery, webhooks — is testable end-to-end right now.
Start with the thesis, or jump straight to the recovery engine where the heaviest-weighted execution lives.