Skip to main content
Subscriptions are money in. Quill also uses Duro for the two money-out flows a creator needs: withdrawing earnings to a bank account, and spending that balance on bills (airtime, data, betting, cable TV, electricity). Both route through DuroService, with the same x-api-key and idempotency discipline as the subscription calls.

The settlement balance

Everything a creator can withdraw or spend is derived from the append-only ledger_entries table, not a mutable balance column. PayoutsService.balanceFor() computes the breakdown with database aggregates (src/payouts/payouts.service.ts): The rules encoded here:
  • T+1 settlement. A charge counts toward available only once its day has passed (ledger.createdAt < date_trunc('day', now())). Same-day charges land in pending and become available the next day. This models a settlement delay without a scheduler.
  • 10% platform fee. netToCreator = gross - platformFee, where platformFee = gross * PLATFORM_FEE_BPS / 10000 and PLATFORM_FEE_BPS = 1000 (src/common/money.ts). Payouts and bills draw from the net settled amount.
  • Reservations. In-flight payouts (pending/processing/paid) and successful/processing bill purchases are subtracted, so a creator cannot double-spend a balance that is already committed.
The same available figure caps both a payout request and a VAS purchase.

Saving a payout bank account (OTP-guarded)

Before withdrawing, a creator saves a destination account. Adding or removing an account is protected by an email OTP, so a hijacked session cannot silently redirect payouts. Flow in PayoutsController + PayoutsService:
  • Bank list + name resolution come from Duro. listBanks calls GET /payouts/banks (falling back to a bundled list of ~50 Nigerian banks if Duro returns nothing), and resolveAccount calls POST /payouts/banks/resolve to confirm the account holder’s name before it is saved.
  • OTP is a 6-digit code emailed to the creator, stored in two_factor_challenges (purpose bank), with a 10-minute TTL and a 5-attempt cap. Both addAccount and deleteAccount require a valid code.
  • Removing the default account promotes the next most recent one.

Requesting a payout

PayoutsService.request() validates the amount against available, confirms the destination is a saved account, then calls Duro: The payouts row stores Duro’s returned status and duroPayoutId. Final status arrives asynchronously: Duro’s payout_completed / payout_failed webhooks call markStatusByDuroPayoutId, flipping the row to paid or failed and stamping processedAt. So Quill records the intent immediately and reconciles the outcome from Duro, exactly as it does for subscriptions. Endpoints are guarded @Roles('creator').

VAS: spending the balance on bills

Quill lets a creator spend settled earnings on Nigerian bills, all vended through Duro’s VAS API. VasController exposes five purchase categories plus catalog and lookup endpoints (src/vas/vas.controller.ts), all @Roles('creator'). Catalog data (data plans per telco, betting providers, cable packages, DISCOs, meter types) is served from a static VasCatalog (src/vas/vas-catalog.ts); the supported networks are MTN, AIRTEL, GLO, 9MOBILE, and DISCOs cover the ten major distributors (Ikeja, Eko, Abuja, Ibadan, PHED, and others). Customer-id and meter lookups call DuroService.vasLookup (POST /vas/{kind}/lookup).

The purchase lifecycle

VasService.purchase() records a bill_transactions row, calls Duro, and reconciles the result (src/vas/vas.service.ts):
  • The purchase is charged against the same available settlement balance as payouts, so bill spend and withdrawals draw from one pot.
  • In mock mode, vasPurchase returns deterministic results, including a fabricated prepaid-electricity token for PREPAID meters, so the flow demos end to end offline.
  • Every purchase is idempotency-keyed off the Quill reference (vas_{category}_{reference}), so a retried vend does not double-charge on Duro’s side.
  • History is queryable and searchable via GET /vas/history (paginated, filter by category, search recipient/provider/reference).

One processor, four money flows

Stepping back, Quill routes all four of its money movements through Duro:

Subscriptions (in)

/checkout/sessions + /subscriptions/* + webhooks. Recurring reader billing and recovery.

Payouts (out)

/payouts + /payouts/banks + /payouts/banks/resolve. Creator withdrawals to a bank account.

Bills (out)

/vas/{category} + /vas/{kind}/lookup. Airtime, data, betting, cable TV, electricity.

Reconciliation

Duro webhooks (payout_*, subscription_*) settle the asynchronous outcome of every flow back into Quill’s tables.
That breadth is what makes Quill a complete reference: it is not just “recurring billing on Duro,” it is a product that treats Duro as its entire money layer. See Deployment for how it ships, or return to Duro Integration for the client details.