> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payouts & VAS

> Creator money-out through Duro transfers with OTP-guarded bank accounts and a T+1 settlement balance, plus airtime, data, betting, cable TV, and electricity vended through Duro VAS.

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`):

```mermaid theme={null}
flowchart TD
    subgraph gross["Gross (subscription charges)"]
        SET["settled = SUM(charges) where createdAt < today (date_trunc day)"]
        PEN["pending = SUM(charges) where createdAt >= today"]
    end
    subgraph out["Deductions"]
        PAID["paidOut = SUM(payouts in pending/processing/paid)"]
        SPENT["billSpent = SUM(bill_transactions in success/processing)"]
    end
    SET --> NET["settledNet = settled - 10% fee"]
    NET --> AVAIL["available = max(0, settledNet - paidOut - billSpent)"]
    PAID --> AVAIL
    SPENT --> AVAIL
    PEN --> PENDING["pending (net) = today's charges, not yet withdrawable"]
```

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`:

```mermaid theme={null}
flowchart TD
    OTP1["POST /payouts/accounts/request-otp"] --> MAIL["email a 6-digit code<br/>(two_factor_challenges, purpose 'bank', 10-min TTL)"]
    ADD["POST /payouts/accounts { bankCode, accountNumber, code, makeDefault }"] --> BANK{"bank in Duro bank list?"}
    BANK -->|"no"| E1["400 choose a supported bank"]
    BANK -->|"yes"| RES["resolveAccount(bankCode, accountNumber) via Duro"]
    RES -->|"null"| E2["400 could not resolve account"]
    RES -->|"name"| DUP{"already saved?"}
    DUP -->|"yes"| E3["409 already saved"]
    DUP -->|"no"| VER["verifyBankOtp(code)"]
    VER --> SAVE["insert payout_accounts (resolved accountName, isDefault)"]
```

* **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:

```mermaid theme={null}
flowchart TD
    REQ["POST /payouts { amountKobo, accountId }"] --> BAL{"amount <= available?"}
    BAL -->|"no"| E1["400 exceeds available balance"]
    BAL -->|"yes"| ACC{"saved account?"}
    ACC -->|"no"| E2["400 choose a saved account"]
    ACC -->|"yes"| CALL["createPayout → POST /payouts (Duro)<br/>Idempotency-Key: pout_{QPO ref}"]
    CALL --> ROW["insert payouts row (status from Duro, duroPayoutId)"]
    ROW --> MAIL["email payout notice"]
```

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')`.

| Category    | Purchase endpoint                                            | Pre-purchase lookup                             |
| ----------- | ------------------------------------------------------------ | ----------------------------------------------- |
| Airtime     | `POST /vas/airtime`                                          | none                                            |
| Data        | `POST /vas/data` (plan catalog via `/vas/data-plans/:telco`) | none                                            |
| Betting     | `POST /vas/betting`                                          | `POST /vas/betting/lookup` (verify customer id) |
| Cable TV    | `POST /vas/cabletv`                                          | `POST /vas/cable/lookup`                        |
| Electricity | `POST /vas/electricity`                                      | `POST /vas/electricity/lookup` (meter name)     |

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`):

```mermaid theme={null}
flowchart TD
    BUY["purchase(input)"] --> BAL{"available >= amount?"}
    BAL -->|"no"| E1["400 insufficient balance"]
    BAL -->|"yes"| ROW["insert bill_transactions (status: processing)"]
    ROW --> CALL["duro.vasPurchase(category, body)<br/>POST /vas/{category}, Idempotency-Key: vas_{category}_{ref}"]
    CALL -->|"ok"| UPD["update → success / processing,<br/>store providerReference + meta"]
    CALL -->|"throws"| FAIL["update → failed, record error in meta; rethrow 400"]
```

* 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:

<CardGroup cols={2}>
  <Card title="Subscriptions (in)" icon="arrow-right-to-bracket">
    `/checkout/sessions` + `/subscriptions/*` + webhooks. Recurring reader billing and recovery.
  </Card>

  <Card title="Payouts (out)" icon="arrow-right-from-bracket">
    `/payouts` + `/payouts/banks` + `/payouts/banks/resolve`. Creator withdrawals to a bank account.
  </Card>

  <Card title="Bills (out)" icon="receipt">
    `/vas/{category}` + `/vas/{kind}/lookup`. Airtime, data, betting, cable TV, electricity.
  </Card>

  <Card title="Reconciliation" icon="scale-balanced">
    Duro webhooks (`payout_*`, `subscription_*`) settle the asynchronous outcome of every flow back into Quill's tables.
  </Card>
</CardGroup>

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](/quill/deployment) for how it ships, or return to [Duro Integration](/quill/duro-integration) for the client details.
