# PFI Architecture and Design <!-- omit in toc --> # Summary <!-- omit in toc --> This design doc covers all of the modules, verbs, and data models needed to facilitate a USD -> MXN remittance via the [tbdex protocol](https://github.com/TBD54566975/tbdex/blob/main/specs/protocol/README.md). A core functionality of our PFI is moving money. For our PFI, considerations around money movements can be divided into two broad areas: 1. The externally accessible [tbdex API](https://github.com/TBD54566975/tbdex/blob/main/specs/http-api/README.md) 2. Payment rails which facilitate funds movements 3. Supporting infrastructure to satisfy requirements from accounting, treasury, and audit An overarching principle is to optimize our design for satisfying our short term requirements while remaining extensible enough to iterate on in the future. > [!NOTE] > While we are supporting just USD -> MXN remittances in the short term, the architecture will support arbitrary future currency pairs > [!NOTE] > FTL and Go principles have informed aspects of this design. In particular, modules communicate only via RPC - there is no async data transmission via pub/sub # Table of Contents <!-- omit in toc --> - [Architecture Summary](#architecture-summary) - [Sequence Diagrams](#sequence-diagrams) - [Institutional Top Up](#institutional-top-up) - [USD -> MXN Remittance](#usd---mxn-remittance) - [MXN Acquisition From USD](#mxn-acquisition-from-usd) - [Modules](#modules) - [`tbdex`](#tbdex) - [Verbs](#verbs) - [HTTP Ingress](#http-ingress) - [`GetOfferings`](#getofferings) - [`CreateExchange`](#createexchange) - [`SubmitMessage`](#submitmessage) - [`GetExchanges`](#getexchanges) - [`GetExchange`](#getexchange) - [`offerings`](#offerings) - [Verbs](#verbs-1) - [Internal](#internal) - [`GetOfferings`](#getofferings-1) - [`GetOffering`](#getoffering) - [`UpdateRate`](#updaterate) - [Database Schemas](#database-schemas) - [`exchanges`](#exchanges) - [Verbs](#verbs-2) - [HTTP Ingress](#http-ingress-1) - [`HandleBitsoWebhook`](#handlebitsowebhook) - [`HandleBankayaWebhook`](#handlebankayawebhook) - [`HandleCircleWebhook`](#handlecirclewebhook) - [Internal](#internal-1) - [`ProcessMessage`](#processmessage) - [Database Schemas](#database-schemas-1) - [`quoting`](#quoting) - [Verbs](#verbs-3) - [Internal](#internal-2) - [`CreateQuote`](#createquote) - [Database Schemas](#database-schemas-2) - [`ledger`](#ledger) - [Verbs](#verbs-4) - [Internal](#internal-3) - [`CreateBalance`](#createbalance) - [`CreateEntry`](#createentry) - [`GetBalances`](#getbalances) - [`CheckBalanceAndReserve`](#checkbalanceandreserve) - [Database Schemas](#database-schemas-3) - [`bankaya`](#bankaya) - [`bitso`](#bitso) - [Verbs](#verbs-5) - [Internal](#internal-4) - [`CreatePayment`](#createpayment) - [`circle`](#circle) - [Verbs](#verbs-6) - [Internal](#internal-5) - [`CreatePayout`](#createpayout) - [`adminbff`](#adminbff) - [Verbs](#verbs-7) - [`treasury`](#treasury) - [Verbs](#verbs-8) - [Internal](#internal-6) - [`CreateBalance`](#createbalance-1) - [`CreateTransfer`](#createtransfer) - [Database Schemas](#database-schemas-4) - [Open Questions](#open-questions) - [Appendix](#appendix) # Architecture Summary ![image](../../images/pfi-modules.png) # Sequence Diagrams ## Institutional Top Up ```mermaid sequenceDiagram participant abff as Admin BFF participant ca as CashApp participant api as tbdex API participant o as Offerings participant e as Exchanges participant q as Quoting participant l as Ledger ca ->> api: GET /offerings api ->> o: GetOfferings() ca ->> api: POST /exchanges (RFQ) api ->> e: ProcessMessage(RFQ) e ->> e: INSERT RFQ e ->> q: CreateQuote() e ->> e: INSERT Quote e ->> ca: Send Quote ca ->> api: PUT /exchanges/:id (Order) api ->> e: ProcessMessage(Order) e ->> e: INSERT Order, INSERT Payin, INSERT Payout e ->> l: +cash SB pending abff ->> e: USD deposit received e ->> l: +cash SB void/settled e ->> e: INSERT OrderStatus, Close e ->> ca: Send OrderStatus, Close ``` ## USD -> MXN Remittance ```mermaid sequenceDiagram participant ca as CashApp participant api as tbdex API participant o as Offerings participant e as Exchanges participant q as Quoting participant l as Ledger participant ba as Bankaya participant bs as Bitso participant bae as Bankaya External ca ->> api: GET /offerings api ->> o: GetOfferings() ca ->> api: POST /exchanges (RFQ) api ->> e: ProcessMessage(RFQ) e ->> e: INSERT RFQ e ->> l: checkBalances(USD, MXN), reserveFunds(USD, MXN) e ->> e: INSERT Order, INSERT Payin, INSERT Payout e ->> q: CreateQuote() q ->> bs: GetRate() e ->> e: INSERT Quote e ->> ca: Send Quote ca ->> api: PUT /exchanges/:id (Order) api ->> e: ProcessMessage(Order) e ->> l: -cash SB void/settled e ->> ba: CreatePayment() bae ->> e: Webhook(payment processing) e ->> e: INSERT OrderStatus e ->> ca: Send OrderStatus bae ->> e: Webhook(payment complete) e ->> l: -tbd MXN void/settled e ->> e: INSERT Close e ->> ca: Send Close ``` ## MXN Acquisition From USD ```mermaid sequenceDiagram participant a as Admin BFF participant t as Treasury participant l as Ledger participant ci as Circle participant cie as Circle External participant bse as Bitso External participant bs as Bitso participant bae as Bankaya External a ->> t: CreateTransfer(tbd USD, tbd USDC) t ->> t: INSERT transfer t ->> l: -tbd USD pending, +tbd USDC pending t ->> ci: send USD to Circle cie ->> t: Webhook(deposit complete) t ->> l: - tbd USD void/settled, +tbd USDC void/settled a ->> t: CreateTransfer(tbd USDC, tbd MXN) t ->> t: INSERT transfer t ->> l: -tbd USDC pending, +tbd MXN pending t ->> ci: send USDC to Bitso cie ->> t: Webhook(payout complete) t ->> l: -tbd USDC void/settle bse ->> t: Webhook(deposit received) t ->> l: +tbd MXN void/settle a ->> t: CreateTransfer(tbd bitso MXN, tbd bankaya MXN) t ->> t: INSERT transfer t ->> l: -tbd bitso MXN pending, +tbd bankaya MXN pending t ->> bs: send MXN to Bankaya t ->> t: -tbd bitso MXN void/settle bae ->> t: Webhook(deposit complete) t ->> l: -tbd bankaya MXN void/settle ``` # Modules ## `tbdex` The tbdex module implements the tbdex HTTP API spec. All incoming requests from an external customer (e.g Cash App) come into the tbdex module. PFI business logic is kept to a minimum in this module - it will simply do high level message validation and verification, returning HTTP errors with a descriptive message if either fail. ### Verbs #### HTTP Ingress > [!NOTE] > For all tbdex HTTP Ingress Verb request and response types, refer to the [tbdex HTTP API spec](https://github.com/TBD54566975/tbdex/blob/main/specs/http-api/README.md) ##### `GetOfferings` Returns a list of `Offering`s to the caller. ###### Biz Logic 1. Call `Offerings` module for list of PFI offerings 2. Return the spec-defined response ##### `CreateExchange` ###### Biz Logic 1. Verify the RFQ contained within the request body 2. Call [`offerings.GetOffering`]() to fetch the associated Offering 3. Validate RFQ against offering 4. Call [`exchanges.ProcessMessage`]() to handle the RFQ 5. Return 202 Accepted, or an error ##### `SubmitMessage` ###### Biz Logic 1. Verify the message contained within the request body 2. Call [`exchanges.ProcessMessage`]() to handle the message 3. Return 202 Accepted, or an error ##### `GetExchanges` ###### Biz Logic 1. Verify the bearer token in the request headers 2. Call [`exchanges.GetExchanges`]() to fetch the customer's exchanges 3. Return a list of exchanges, or an error ##### `GetExchange` ###### Biz Logic 1. Verify the bearer token in the request headers 2. Call [`exchanges.GetExchange`]() to fetch the customer's exchanges 3. Return the exchange, or an error ## `offerings` The offerings module contains all offering related business logic. This includes maintaining the available currency pairs and updating the indicative rates for each currency pair on a regular schedule. > [!NOTE] > Scheduled jobs are not yet implemented on FTL ### Verbs #### Internal ##### `GetOfferings` Used by [`tbdex.GetExchanges`](#getexchanges) to fetch the PFI's offerings. ###### Request Empty ###### Biz Logic 1. SELECT from `offerings` and return ###### Response | field | description | | :-------------- | :----------------------------- | | Offerings | List of offerings | ##### `GetOffering` Used by [`tbdex.CreateExchange`](#createexchange) to fetch the offering from the RFQ. ###### Request | field | description | |:---------- |:-------------------- | | OfferingID | e.g. `offering_1234` | ###### Biz Logic 1. SELECT WHERE from `offerings` and return ###### Response | field | description | |:--------- |:---------------------- | | Offerings | The offering requested | ##### `UpdateRate` > [!WARNING] > TODO: Fill out after scheduled jobs are available ###### Request ###### Biz Logic ###### Response Empty, or error ### Database Schemas #### `offerings` | field | description | |:---------------------- |:--------------------- | | `id` | primary key | | `offering_id` | unique immutable ID | | `payin_currency_code` | source currency | | `payout_currency_code` | target currency | | `offering_json` | full offering as json | | `created_at` | ISO8601 | | `updated_at` | ISO8601 | ## `exchanges` The exchanges module acts as a bridge between the external requests coming from the customer and the PFI's internal business logic. In order to avoid circular module dependencies, it also receives all incoming webhook notifications from external APIs and takes the appropriate action based on the contents of the notification. This may include sending messages to the exchange's callback URL if the PFI has new messages to send to customers (`Quote`, `OrderStatus`, `Close`). In addition, we have an association table which join ledger entries to payments and contain domain data which is additional to the debit/credit amounts. ### Verbs #### HTTP Ingress ##### `HandleBitsoWebhook` Used to handle notifications from Bitso ###### Request | field | description | |:-------------- |:-------------------------------- | | `notification` | The notification as a byte array | ###### Biz Logic 1. Parse into Bitso notification type 2. Handle notification ###### Response 200 OK if processed successfully > [!WARNING] > TODO: confirm bitso's expected response ##### `HandleBankayaWebhook` Used to handle notifications from Bankaya ###### Request | field | description | |:-------------- |:-------------------------------- | | `notification` | The notification as a byte array | ###### Biz Logic 1. Parse into Bankaya notification type 2. Handle notification ###### Response 200 OK if processed successfully > [!WARNING] > TODO: confirm bankaya's expected response ##### `HandleCircleWebhook` Used to handle notifications from Circle ###### Request | field | description | |:-------------- |:-------------------------------- | | `notification` | The notification as a byte array | ###### Biz Logic 1. Parse into Circle notification type 2. Handle notification > [!WARNING] > TODO: **INCOMPLETE** ###### Response 200 OK if processed successfully > [!WARNING] > TODO: confirm circle's expected response #### Internal ##### `ProcessMessage` Used by [`tbdex.SubmitMessage`](#submitmessage) process a new tbdex message. This is the primary business logic router - it will handle each message type appropriately. ###### Request | field | description | |:--------- |:--------------------- | | `message` | The tbdex message | | `replyTo` | Optional callback URL | ###### Biz Logic 1. Store the message 2. Switch based on message type **If RFQ:** 3. Call [`quoting.CreateQuote`](#createquote) to get the rate and pay out amount 4. Call [`CheckBalanceAndReserve`](#checkbalanceandreserve) to ledger pendings against the relevant balances 5. Insert payment rows for the payin and payout with the provider returned by [`quoting.CreateQuote`](#createquote) 6. Create quote message 7. Send a request containing the Quote message to the `replyTo` URL if present **If Order:** 3. Fetch the exchange and associated payment rows 4. Call appropriate provider to initiate/execute money movement 5. Send a request containing an OrderStatus message to the exchange's `replyTo` URL if present **If Close:** 3. **TODO** determine if additional biz logic needed ###### Response Empty, or error ### Database Schemas #### `exchanges` | field | description | |:--------------------- |:---------------------------------------- | | `id` | primary key | | `exchange_id` | unique immutable ID identifying exchange | | `message_id` | unique immutable ID identifying message | | `subject_customer_id` | customer initiating exchange | | `message_kind` | tbdex message kind | | `message_json` | full message as json | | `created_at` | ISO8601 timestamp | #### `payments` | field | description | |:-------------- |:---------------------------------------------- | | `id` | primary key | | `payment_id` | unique immutable ID | | `direction` | PAYIN, PAYOUT | | `provider` | provider which will execute the money movement | | `external_id` | provider reference | | `created_at` | ISO8601 timestamp | | `updated_at` | ISO8601 timestamp | #### `payments_entries` | field | description | |:------------ |:-------------------- | | `id` | primary key | | `payment_id` | payment foreign key | | `entry_id` | entry foreign key | | `type` | FEE, PRINCIPLE, etc. | | `created_at` | ISO8601 timestamp | ## `quoting` The quoting module handles all exchange rate business logic. For now, we will use Bitso's API to fetch the USD -> MXN rate, but we may use a different rate provider in the future. ### Verbs #### Internal ##### `CreateQuote` Used by [`exchanges.ProcessMessage`](#processmessage) when handling an RFQ to create a guaranteed rate for the Quote message. Used by [`offerings.UpdateRate`](#updaterate) to create a new indicative rate for Offerings. ###### Request | field | description | |:-------------------- |:--------------- | | `payinCurrencyCode` | source currency | | `payoutCurrencyCode` | target currency | > [!WARNING] > This will likely change as we determine the biz requirements for adjusting rate based on who the customer is, if we are generating a rate for the offering, etc. ###### Biz Logic For USD -> MXN Offering: 1. Call [`bitso.GetRate`] to fetch the latest midmarket rate 2. Store the rate 3. Return the rate For USD -> MXN RFQ Quote: 1. Call [`bitso.GetRate`] to fetch the latest midmarket rate 2. Add fees if relevant 3. Store the rate 4. Return the rate ###### Response Empty, or error ### Database Schemas #### `quotes` | field | description | |:------------- |:----------------------------------------- | | `id` | primary key | | `quote_id` | unique immutable ID | | `exchange_id` | exchange for which the quote is generated | | `provider` | rate provider (BITSO) | | `rate` | exchange rate | | `fee` | fee additional to rate **REVISIT** | | `created_at` | ISO8601 timestamp | ## `ledger` The ledger module contains our source of truth for all balances (customer stored balances as well as TBD internal treasury balances) and money movements impacting the balances. The core ledger is simply an append only table with debits and credits against balances. ### Verbs #### Internal ##### `CreateBalance` Used by [`treasury.CreateBalance`](#createbalance-1) to create a new balance which will be tracked by the ledger. ###### Request | field | description | |:-------------- |:-------------------------------------------- | | `beneficiary` | customer to whom the balance belongs, or TBD | | `custodian` | who is custodying the funds | | `currencyCode` | currency of the balance | ###### Biz Logic 1. Insert a row into `balances` with the appropriate attributes 2. Return the balance ###### Response | field | description | |:--------- |:------------------- | | `balance` | the created balance | ##### `CreateEntry` Used by `exchanges` and `transfers` to ledger a debit or credit against a balance. ###### Request | field | description | |:----------- |:------------------------- | | `balanceID` | balance to ledger against | | `amount` | debit/credit amount | | `type` | PENDING, SETTLED, VOIDED | ###### Biz Logic 1. Start DB transaction 2. Insert a row into `entries` for the specified amount and state 3. Update the balance's amount 4. Commit 5. Return the entry ###### Response | field | description | |:------- |:----------------- | | `entry` | the created entry | ##### `GetBalances` Used by [`exchanges.ListBalances`](#createcredentialoffer) to return the customer's balance amounts. ###### Request | field | description | |:------------- |:-------------------------------------- | | `beneficiary` | beneficiary for whom to fetch balances | ###### Biz Logic 1. Query the `balances` table for the specified beneficiary. 2. Return the balances or error ###### Response | field | description | |:---------- |:-------------------------- | | `balances` | a list of fetched balances | ##### `CheckBalanceAndReserve` Used by [`exchanges.HandleMessage`](#createcredentialoffer) when processing a USD -> MXN RFQ. > [!WARNING] > TODO: **INCOMPLETE** > Need to determine biz level requirements and FTL db features, this may check and reserve against two balances at once ###### Request | field | description | |:----------- |:------------------------------ | | `balanceID` | balance to perform check for | | `amount` | amount that should be reserved | ###### Biz Logic 1. Start DB transaction 2. Query balance A 3. Insert a row into `entries` that is a pending amount against balance A 4. Update balance A 5. Commit 6. Return the balance and entry ###### Response | field | description | |:--------- |:------------------- | | `balance` | the updated balance | | `entry` | the created entry | ### Database Schemas #### `balances` | field | description | |:------------------ |:----------------------------------------------- | | `id` | primary key | | `balance_id` | unique immutable ID | | `beneficiary` | customer id to whom the balance belongs, or TBD | | `custodian` | who is custodying the funds | | `currency_code` | currency of the balance | | `available_amount` | total amount - pendings | | `amount` | total amount | | `created_at` | ISO8601 timestamp | | `updated_at` | ISO8601 timestamp | #### `entries` | field | description | |:------------ |:----------------------------- | | `id` | primary key | | `entry_id` | unique immutable ID | | `balance_id` | balance impacted by the entry | | `amount` | debit/credit amount | | `type` | PENDING, VOIDED, SETTLED | | `created_at` | ISO8601 timestamp | ## `bankaya` The bankaya module is our integration point with Bankaya's API. > [!WARNING] > TODO: **INCOMPLETE** > Need to determine relevant endpoints from Bankaya API docs ### Verbs #### Internal ## `bitso` The bitso module is our integration point with Bitso's API. In the short term, we will also be using the Bitso API to fetch the midmarket rate for a USD -> MXN currency exchange. > [!WARNING] > TODO: **INCOMPLETE** > Need to determine relevant endpoints from Bitso API docs ### Verbs #### Internal ##### `CreatePayment` Used by [`treasury`](#treasury) module to initiate a payout. > [!WARNING] > **May change depending on business requirements** ###### Request ###### Biz Logic ###### Response ## `circle` The circle module is our integration point with Circle's API. > [!WARNING] > TODO: **INCOMPLETE** > Need to determine relevant endpoints from Circle API docs ### Verbs #### Internal ##### `CreatePayout` Used by [`treasury`](#treasury) to initiate a payout from our Circle wallet ###### Request ###### Biz Logic ###### Response ## `adminbff` The admin BFF module is the backend for our admin UI, which will initially be used by Treasury Ops and ourselves. > [!WARNING] > TODO: **INCOMPLETE** > Need to gather requirements for Treasury Ops and visibility/reporting requirements ### Verbs #### HTTP Ingress ## `treasury` The treasury module acts as a bridge between the admin actions coming from our admin UI and the PFI. In order to avoid circular module dependencies, it also receives all incoming webhook notifications from external APIs and takes the appropriate action based on the contents of the notification (just like the `exchanges` module). In addition, we have an association table which join ledger entries to transfers and contain domain data which is additional to the debit/credit amounts. > [!WARNING] > TODO: **INCOMPLETE** > Need to gather requirements for Treasury Ops ### Verbs #### Internal ##### `CreateBalance` Used by [`adminbff.CreateBalance`](#createcredentialoffer) to create a new balance. `treasury` will perform validation on the balance details and return an error if there is business logic preventing the balance from being created. ###### Request | field | description | |:-------------- |:-------------------------------------------- | | `beneficiary` | customer to whom the balance belongs, or TBD | | `custodian` | who is custodying the funds | | `currencyCode` | currency of the balance | ###### Biz Logic 1. Validate the create request 2. Call [`ledger.CreateBalance`](#createbalance) to create the balance 3. Return the balance > [!IMPORTANT] > Uniqueness ###### Response | field | description | |:--------- |:------------------- | | `Balance` | The created balance | ##### `CreateTransfer` Used by [`adminbff.CreateTransfer`](#createtransfer) to create a treasury transfer between two balances. ###### Request | field | description | |:------------------- |:-------------------------------------------- | | `sourceBalanceID` | customer to whom the balance belongs, or TBD | | `targetBalanceID` | who is custodying the funds | | `sourceAmount` | amount sent | | `destinationAmount` | amount received | | `currencyCode` | currency of the balance | ###### Biz Logic > [!WARNING] > TODO: **INCOMPLETE** > Fill out after we have concrete treasury flows ###### Response | field | description | |:---------- |:-------------------- | | `transfer` | The created transfer | ### Database Schemas #### `transfers` | field | description | |:------------------------ |:--------------------------------- | | `id` | primary key | | `transfer_id` | unique immutable ID | | `source_balance_id` | source of funds (debit) | | `destination_balance_id` | destination of funds (credit) | | `source_amount` | amount sent | | `destination_amount` | amount received | | `state` | state of the transfer **REVISIT** | | `created_at` | ISO8601 timestamp | | `updated_at` | ISO8601 timestamp | #### `transfers_entries` | field | description | |:------------- |:-------------------- | | `id` | primary key | | `transfer_id` | transfer foreign key | | `entry_id` | entry foreign key | | `type` | FEE, PRINCIPLE, etc. | | `created_at` | ISO8601 timestamp | # Open Questions 1. What is the customer creation flow? a. Where should the Customers table live? 3. What is the balance creation flow and how does it relate to the customer creation flow? 4. What are the requirements of the admin UI? (from Treasury Ops and for our own reporting/visibility) 5. What is the Offering creation workflow? # Appendix - [Excalidraw diagram source](https://app.excalidraw.com/s/A7R4E52NON1/800NLratwGQ) - [tbdex protocol](https://github.com/TBD54566975/tbdex/blob/main/specs/protocol/README.md)