_Docs/
Get StartedModulesPlatformDeployCookbookChangelogReference
_Cookbook
  • Introduction
    • RideShare Tutorial
    • Omnibus Account Management
    • Payment Card Acceptance Processing
    • Card Issuing & Financial Host
    • Stablecoin On-Ramp & Off-Ramp Operations
    • Stablecoin Issuer
    • Neobank (FBO)
    • Marketplace Payouts
    • Crypto Custody
    • BNPL & Lending
  1. Examples
  2. Advanced Recipes
  3. Neobank (FBO)
Cookbook

Neobank (FBO)

This example shows how to model a neobank built on a single FBO ("For Benefit Of") bank account in Formance using a declarative ledger schema. The neobank holds all customer money commingled in one omnibus account at a sponsor bank, while the ledger tracks each customer's slice as a separate liability balance. Every customer movement — deposits, card spend, peer-to-peer transfers, withdrawals, and early-access advances — flows through the same pooled cash position, and a daily FBO reconciliation proves the ledger's view of pooled cash against the sponsor's reported statement.

Common use cases:

  • Neobanks and BaaS programs operating customer accounts on top of a sponsor-bank FBO
  • Earned-wage / early-access products that front advances against an expected deposit
  • Card programs managing authorization holds, captures, and refunds against pooled cash

This is an illustrative example. Adapt the schema to your specific business requirements, regulatory obligations, and financial practices.

Key Concepts#

  • FBO Pooling — All customer funds sit commingled in one bank account (platform:banks:sponsor:fbo:settled). The ledger is the system of record for who owns what inside that pool; the bank only ever sees the aggregate.
  • Per-Customer Liability Balances — Each customer has an available balance plus subtrees for authorization holds, in-flight withdrawals, and advances. These are liabilities — the credit balance is what the platform owes that customer.
  • Daily FBO Reconciliation — The single asset-side account (platform:banks:sponsor:fbo:settled) is reconciled every day against the sponsor's reported FBO statement. It runs negative; its absolute value is the ledger's view of settled cash in the pool, and total positive claims against it form an internal solvency check.
  • In-Flight Isolation — Authorization holds, pending ACH withdrawals, and outstanding advances each get their own per-reference account, so funds that are earmarked, settling, or fronted are never confused with spendable balance.
  • Platform Float Separation — The platform's own money (the FBO buffer and the corporate operating account) lives outside the customers root, giving structural proof that platform funds are distinct from customer funds.

The Complete Schema#

This is the full ledger schema for a neobank FBO system. The sections below explain each part.

├─ availableaccount
│ └─ {}$authId
^[a-zA-Z0-9_-]+$
account
│ └─ pendingaccount
└─ outstandingaccount
│ │ ├─ settledaccount
│ │ └─ bufferaccount
│ ├─ settledaccount
│ └─ operatingaccount
│ └─ interestaccount
└─ advanceLossaccount
Open in Studio

Edit this template in the Formance Studio editor.

Open in Studio

Chart of Accounts#

The chart section defines two top-level groups: customers (the per-customer liability subtree) and platform (banks, revenue, and expense accounts the platform controls).

Customers#

├─ availableaccount
│ └─ {}$authId
^[a-zA-Z0-9_-]+$
account
│ └─ pendingaccount
└─ outstandingaccount

Normal credit accounts — these represent your liabilities to customers. Each $customerId has an available balance (spendable funds), a holds subtree (one account per card authorization, by $authId), a withdrawals subtree (one pending account per outbound ACH, by $withdrawalId), and an advances subtree (one outstanding receivable per advance, by $advanceId). The advance outstanding accounts run negative while open — they are receivables the platform fronts — and drain to zero when the expected deposit settles.

Platform#

│ │ ├─ settledaccount
│ │ └─ bufferaccount
│ ├─ settledaccount
│ └─ operatingaccount
│ └─ interestaccount
└─ advanceLossaccount

Mixed nature accounts controlled by the platform. The sponsor FBO settled account is the asset/nostro position: it represents the pooled cash held at the sponsor bank and runs negative as the ledger books credits to customers ahead of bank settlement. The FBO buffer and corporate operating/settled accounts hold platform float. revenue:interest captures FBO interest recognized as platform revenue, and expense:advanceLoss absorbs unrecoverable advances.

Deposit Flow#

Inbound funds settle into the FBO and are credited to the customer's available balance. The allowing unbounded overdraft clause on the FBO settled account lets it go negative — this is intentional, reflecting that the ledger books the customer credit before bank reconciliation.

ACH Direct Deposit

ACH_DIRECT_DEPOSIT settles an inbound ACH direct deposit into the FBO and credits the customer's available balance.

ACH_DIRECT_DEPOSIT
experimental
account interpolation
Inbound ACH direct deposit settled into the FBO and credited to the customer
vars {
  account $customer_id
  monetary $amount
  string $deposit_id
  string $originator
}

send $amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "ach_direct_deposit")
set_tx_meta("deposit_id", $deposit_id)
set_tx_meta("originator", $originator)

Incoming Wire

INCOMING_WIRE does the same for an inbound wire — funds settle into the FBO and credit the customer.

INCOMING_WIRE
experimental
account interpolation
Inbound wire settled into the FBO and credited to the customer
vars {
  account $customer_id
  monetary $amount
  string $wire_id
  string $originator
}

send $amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "incoming_wire")
set_tx_meta("wire_id", $wire_id)
set_tx_meta("originator", $originator)

Deposit Return

ACH_DEPOSIT_RETURN reverses a settled ACH deposit returned by the originating bank, debiting the customer's available balance back to the FBO.

ACH_DEPOSIT_RETURN
experimental
account interpolation
Reversal of a settled ACH deposit returned by the originating bank
vars {
  account $customer_id
  monetary $amount
  string $deposit_id
}

send $amount (
  source = @customers:$customer_id:available allowing unbounded overdraft
  destination = @platform:banks:sponsor:fbo:settled
)

set_tx_meta("event_type", "ach_deposit_return")
set_tx_meta("deposit_id", $deposit_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $deposit_id)

Card Flow#

Card spend follows the authorization-then-capture lifecycle. An authorization places a hold on available balance; the hold is later captured, reversed, or expired.

Authorization

CARD_AUTH places a hold on the customer's available balance, moving funds into a per-authorization holds:$authId account.

CARD_AUTH
experimental
account interpolation
Debit-card authorization places a hold on the customer's available balance
vars {
  account $customer_id
  string $auth_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:available
  destination = @customers:$customer_id:holds:$auth_id
)

set_tx_meta("event_type", "card_auth")
set_tx_meta("auth_id", $auth_id)

Capture

CARD_CAPTURE settles against an authorization: the captured amount leaves the FBO and the remainder of the hold is restored to available.

CARD_CAPTURE
experimental
account interpolation
mid-script function call
Capture against an authorization; captured amount leaves the FBO, remainder restored
vars {
  account $customer_id
  string $auth_id
  monetary $capture_amount
  monetary $hold_balance = balance(@customers:$customer_id:holds:$auth_id, USD/2)
}

send $hold_balance (
  source = @customers:$customer_id:holds:$auth_id
  destination = {
    max $capture_amount to @platform:banks:sponsor:fbo:settled
    remaining to @customers:$customer_id:available
  }
)

set_tx_meta("event_type", "card_capture")
set_tx_meta("auth_id", $auth_id)

Reversal

CARD_AUTH_REVERSE is a merchant-initiated reversal of an authorization — the hold returns to available.

CARD_AUTH_REVERSE
experimental
account interpolation
Merchant-initiated reversal of a card authorization; hold returns to available
vars {
  account $customer_id
  string $auth_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:holds:$auth_id
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "card_auth_reverse")
set_tx_meta("auth_id", $auth_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $auth_id)

Expiry

CARD_AUTH_EXPIRE releases an unused authorization: the remaining hold balance returns to available.

CARD_AUTH_EXPIRE
experimental
account interpolation
mid-script function call
Authorization hold expires unused; remaining hold returns to available
vars {
  account $customer_id
  string $auth_id
  monetary $hold_balance = balance(@customers:$customer_id:holds:$auth_id, USD/2)
}

send $hold_balance (
  source = @customers:$customer_id:holds:$auth_id
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "card_auth_expire")
set_tx_meta("auth_id", $auth_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $auth_id)

A merchant refund of a prior capture is credited back through CARD_REFUND, restoring funds from the FBO to the customer.

CARD_REFUND
experimental
account interpolation
Merchant refund of a prior capture credited to the customer
vars {
  account $customer_id
  monetary $amount
  string $refund_id
  string $original_capture_id
}

send $amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "card_refund")
set_tx_meta("refund_id", $refund_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $original_capture_id)

Peer-to-Peer Transfer#

P2P_TRANSFER moves funds instantly between two customers' available balances. Because both customers share the same FBO pool, the transfer is purely in-ledger and never touches the bank.

P2P_TRANSFER
experimental
account interpolation
Instant in-ledger transfer between two customers; never touches the bank
vars {
  account $from_customer_id
  account $to_customer_id
  monetary $amount
  string $transfer_id
}

send $amount (
  source = @customers:$from_customer_id:available
  destination = @customers:$to_customer_id:available
)

set_tx_meta("event_type", "p2p_transfer")
set_tx_meta("transfer_id", $transfer_id)

Withdrawal Flow#

Outbound ACH withdrawals are a two-step process so that funds being withdrawn can't be spent while in flight.

Reserve

ACH_WITHDRAWAL_RESERVE earmarks funds from the customer's available balance into a per-withdrawal withdrawals:$withdrawalId:pending account.

ACH_WITHDRAWAL_RESERVE
experimental
account interpolation
Customer-requested outbound ACH withdrawal earmarked from available to pending
vars {
  account $customer_id
  string $withdrawal_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:available
  destination = @customers:$customer_id:withdrawals:$withdrawal_id:pending
)

set_tx_meta("event_type", "ach_withdrawal_reserve")
set_tx_meta("withdrawal_id", $withdrawal_id)

Settle

ACH_WITHDRAWAL_SETTLE completes the cycle once the outbound ACH settles — pending funds leave the FBO.

ACH_WITHDRAWAL_SETTLE
experimental
account interpolation
Outbound ACH settles; pending funds leave the FBO
vars {
  account $customer_id
  string $withdrawal_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:withdrawals:$withdrawal_id:pending
  destination = @platform:banks:sponsor:fbo:settled
)

set_tx_meta("event_type", "ach_withdrawal_settle")
set_tx_meta("withdrawal_id", $withdrawal_id)

If the outbound ACH fails or is returned, ACH_WITHDRAWAL_RETURN restores the pending funds to the customer's available balance.

ACH_WITHDRAWAL_RETURN
experimental
account interpolation
Outbound ACH failed or returned; pending funds restored to available
vars {
  account $customer_id
  string $withdrawal_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:withdrawals:$withdrawal_id:pending
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "ach_withdrawal_return")
set_tx_meta("withdrawal_id", $withdrawal_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $withdrawal_id)

Advance Flow#

Early-access advances let the platform front money against an expected deposit, booked as a receivable on a per-advance advances:$advanceId:outstanding account.

Origination

ADVANCE_ORIGINATION fronts the advance: the outstanding receivable goes negative and the customer's available balance is credited.

ADVANCE_ORIGINATION
experimental
account interpolation
Platform fronts an early-access advance; credited to available, booked as a receivable
vars {
  account $customer_id
  string $advance_id
  monetary $amount
}

send $amount (
  source = @customers:$customer_id:advances:$advance_id:outstanding allowing unbounded overdraft
  destination = @customers:$customer_id:available
)

set_tx_meta("event_type", "advance_origination")
set_tx_meta("advance_id", $advance_id)

Settlement

ADVANCE_SETTLEMENT applies the expected deposit when it settles, repaying the advance first and crediting any remainder to available.

ADVANCE_SETTLEMENT
experimental
overdraft()
account interpolation
mid-script function call
Expected deposit settles; repays the advance first, remainder to available
vars {
  account $customer_id
  string $advance_id
  monetary $deposit_amount
  monetary $advance_outstanding = overdraft(@customers:$customer_id:advances:$advance_id:outstanding, USD/2)
}

send $deposit_amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = {
    max $advance_outstanding to @customers:$customer_id:advances:$advance_id:outstanding
    remaining to @customers:$customer_id:available
  }
)

set_tx_meta("event_type", "advance_settlement")
set_tx_meta("advance_id", $advance_id)

Write-Off

ADVANCE_WRITEOFF recognizes an unrecoverable advance as a loss against platform:expense:advanceLoss and funds the FBO from the corporate bank.

ADVANCE_WRITEOFF
experimental
overdraft()
account interpolation
mid-script function call
Unrecoverable advance written off to loss; platform funds the FBO from corporate
vars {
  account $customer_id
  string $advance_id
  monetary $advance_outstanding = overdraft(@customers:$customer_id:advances:$advance_id:outstanding, USD/2)
}

send $advance_outstanding (
  source = @platform:expense:advanceLoss allowing unbounded overdraft
  destination = @customers:$customer_id:advances:$advance_id:outstanding
)

send $advance_outstanding (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @platform:banks:corporate:settled
)

set_tx_meta("event_type", "advance_writeoff")
set_tx_meta("advance_id", $advance_id)
set_tx_meta("adjustment_flag", "true")
set_tx_meta("adjusted_posting_event_id", $advance_id)

Platform Operations#

Two recurring platform movements run on a machine interpreter. FBO_INTEREST_SWEEP recognizes monthly sponsor interest on the FBO as platform revenue.

FBO_INTEREST_SWEEP
machine
Monthly sponsor interest on the FBO recognized as platform revenue
vars {
  monetary $amount
  string $period_id
}

send $amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @platform:revenue:interest
)

set_tx_meta("event_type", "fbo_interest_sweep")
set_tx_meta("period_id", $period_id)

OPERATING_MOVEMENT_TO_CORPORATE and OPERATING_MOVEMENT_TO_FBO sweep platform float between the FBO buffer and the corporate bank, keeping operating cash topped up in either direction.

OPERATING_MOVEMENT_TO_CORPORATE
machine
Weekly sweep of platform float from the FBO buffer to the corporate bank
vars {
  monetary $amount
  string $movement_id
}

send $amount (
  source = @platform:banks:sponsor:fbo:buffer
  destination = @platform:banks:sponsor:fbo:settled
)

send $amount (
  source = @platform:banks:corporate:settled allowing unbounded overdraft
  destination = @platform:banks:corporate:operating
)

set_tx_meta("event_type", "operating_movement_to_corporate")
set_tx_meta("movement_id", $movement_id)
OPERATING_MOVEMENT_TO_FBO
machine
Weekly top-up of platform float from the corporate bank into the FBO buffer
vars {
  monetary $amount
  string $movement_id
}

send $amount (
  source = @platform:banks:corporate:operating
  destination = @platform:banks:corporate:settled
)

send $amount (
  source = @platform:banks:sponsor:fbo:settled allowing unbounded overdraft
  destination = @platform:banks:sponsor:fbo:buffer
)

set_tx_meta("event_type", "operating_movement_to_fbo")
set_tx_meta("movement_id", $movement_id)

The Daily FBO Reconciliation#

The FBO settled account is the heart of the model. Because the ledger books customer credits before the bank confirms settlement, platform:banks:sponsor:fbo:settled runs negative — its absolute value is the ledger's view of settled cash in the pool. Reconciliation compares two figures every day:

  • The asset side — the magnitude of the FBO settled account, matched against the sponsor's reported FBO statement balance.
  • The claims side — the sum of every positive claim against that cash: all customer available, held, and pending-withdrawal balances, plus the platform's FBO float and accrued interest.

The claims against the FBO, plus outstanding advances, should equal the FBO backing. A mismatch signals an unreconciled movement or a missing posting — making the FBO settled account a key health indicator to monitor daily.

Queries#

The queries section defines reusable lookups for reconciliation, per-customer reads, and operational monitoring:

Reconciliation and solvency

  • daily_fbo_reconciliation_invariant_1 — the FBO settled account to reconcile against the sponsor statement
  • total_claims_against_the_fbo_internal_solvency_check — every positive claim against FBO cash, for the internal solvency check

Per-customer reads

  • customer_available_balance — one customer's spendable balance right now
  • customer_total_holds — all current authorization holds for one customer
  • customer_full_position — every account in one customer's subtree in a single read

Platform liability and float

  • total_customer_spendable_and_held_liability — the headline platform liability across all customers
  • total_platform_float — the platform's own money, provably separate from customer funds

Advances

  • outstanding_advances — every advance still carrying a balance
  • aging_advances — advances still open after the expected-deposit window
  • total_advance_exposure — the platform's total early-access advance exposure

In-flight and volumes

  • pending_withdrawals_in_flight — outbound ACH earmarked but not yet settled or returned
  • card_capture_volume — capture cash leaving the FBO over a window
  • p2p_transfer_volume — peer-to-peer volume through customer available balances over a window
  • monthly_interest_revenue — FBO interest recognized to revenue over the month
  • advance_loss_to_date — realized loss on unrecovered advances

Audit

  • customer_transaction_audit — every transaction touching a customer's subtree, for drill-down
  • authorization_lifecycle_audit — every transaction for one card authorization

These leverage the hierarchical account structure — filtering on customers::available matches every customer's spendable balance, and customers::advances::outstanding matches every advance receivable.

Stablecoin IssuerMarketplace Payouts
On This Page
  • Key Concepts
  • The Complete Schema
  • Chart of Accounts
  • Customers
  • Platform
  • Deposit Flow
  • Card Flow
  • Peer-to-Peer Transfer
  • Withdrawal Flow
  • Advance Flow
  • Platform Operations
  • The Daily FBO Reconciliation
  • Queries