Backend & data
Service boundaries, the data model, external integrations, and the chain. The persona files describe what users see; this file describes what makes those experiences possible — and where the constraints live.
Architectural principles
- Monolithic backend. The platform runs as a single deployable application against a single Postgres database. The modules below are logical (one codebase, one process); they are not separately deployed services.
- The audit log is the system of record. Every state change writes an audit entry first; module state derives from the audit. Anchored to chain every 10 minutes.
- Tenant isolation is enforced at the database level, not just the application. Every customer-data row carries
tenant_id; no query can cross tenants without a platform-admin context. - Money is integer pence. Never float, never decimal. Currency is always Great British Pound (GBP) unless explicitly USD Coin (USDC) on a stablecoin settlement.
- Idempotency at every external boundary. Every Application Programming Interface (API) mutation requires
FX-Idempotency-Key; replays return the original response. - Append-only audit; mutable state. Domain rows can be updated; audit rows can never be (database-level append-only constraint).
- Compliance fails closed. If a downstream provider (Sumsub, ComplyAdvantage) is unreachable, the gate stays open in pending state — never silently approves.
- Chain compliance modules are the last line. Even if every off-chain check is bypassed, the on-chain Compliance Module on Ethereum Request for Comments 3643 (ERC-3643) will block a non-compliant transfer.
- No secrets in code or config. Provider credentials live in Hardware Security Module (HSM)-backed vault, fetched on boot, rotated quarterly.
Module map
The backend is a single deployable application — one process, one Postgres database. The list below is a logical map: twelve domain modules within that application, each owning a defined set of tables and a clear interface that other modules call in-process. The only network boundaries are the external Representational State Transfer (REST) + webhooks surface, the connections out to providers, and the connection to the FinToken X Network. "Service" is used colloquially below to mean a module's service class, not a separately deployed network service.
auth
Subject creation, sessions, magic-link tokens for buyer one-click, role assignment, MFA. No business state — only "who is this caller right now?"
Owns: subjects, sessions, magic_link_tokens, mfa_factors
kyc
Orchestrates Gates 1–3 across Sumsub + ComplyAdvantage + the internal risk model + Money Laundering Reporting Officer (MLRO) queue. Holds the gate state per subject; emits SUBJECT_ACTIVE when all clear.
Owns: gate_runs, screening_hits, risk_scores, mlro_decisions
monitoring
Gate 4 — continuous. Runs daily deltas against sanctions lists, Financial Conduct Authority (FCA) register, Companies House (CH), Chainalysis. Emits alerts to the compliance console.
Owns: monitoring_runs, alerts
invoice
Receivable lifecycle. Optical Character Recognition (OCR) + vision pass, duplicate detection, buyer-routing, state machine. Spine for everything downstream.
Owns: invoices, invoice_documents, buyer_confirmations, vision_runs, duplicates
tokenisation
Wraps the FinToken X Network. Mints ERC-3643 tokens on confirmation, fractionalises on lender request, transfers via the Compliance Modules, burns on settlement.
Owns: tokens, slices, chain_tx
marketplace
Primary listing for lenders + secondary listing for investors. Filters by lender appetite, buyer concentration, investor classification.
Owns: primary_listings, secondary_listings, listing_filters
settlement
Cash mechanics. Disbursement (lender → seller) at funding; collections (obligor → lender) at maturity; reconciliation via Open Banking Account Information Service (AIS) + bank-statement parsing.
Owns: disbursements, collections, reconciliations, virtual_accounts
secondary
Secondary-market trades. Pre-trade compliance check, trade execution (chain transfer + fiat leg), settlement.
Owns: trades, pre_trade_logs
compliance
MLRO console backend. Queue management, Suspicious Activity Report (SAR) register, decision matrix versioning, retention policy enforcement.
Owns: review_queue, sars, decision_matrix_versions, retention_policies
partner
Tenant management. Theme + DomainKeys Identified Mail (DKIM) + custom domain; Supply Chain Finance (SCF) programme rules; per-tenant API keys + webhooks; usage metering.
Owns: tenants, themes, scf_programmes, api_keys, webhook_endpoints, usage_meters
audit
Append-only audit log + Merkle anchoring service. Hashes every domain event, batches every 10 min, anchors on the FinToken X Network.
Owns: audit_entries, merkle_batches, chain_anchors
notification
Email + webhook delivery. Tenant-aware (Mercia's emails sent from Mercia DKIM), idempotent, retry with backoff.
Owns: email_sends, webhook_deliveries, templates
compliance-ai
Agentic compliance layer. Observes the audit bus, proposes a verdict + SparseScore explanation on every Gate 3 case, Gate 4 alert, and pre-trade entry. Advisory only — MLRO retains decision. Full service spec in AI compliance.
Owns: agent_runs, agent_decisions, sparsescore_explanations, training_runs, model_versions, escalations
Module interaction · single invoice
Data model
Key tables. Column lists are illustrative — the canonical schema lives in migrations.
Core tables
| Table | Owned by | Key columns | Notes |
|---|---|---|---|
subjects |
auth + kyc | id, role, tenant_id, state, created_at, last_kyc_run_at, last_screening_at |
One per legal entity (or representative). Role is immutable post-onboarding. |
onboarding_files |
kyc | id, subject_id, form_version, state, submitted_at, mlro_decided_at, mlro_decided_by, rationale_hash |
One per onboarding attempt. Multiple attempts per subject possible (after a decline). |
gate_runs |
kyc | id, subject_id, gate (1/2/3/4), provider, provider_ref, verdict, raw_payload_ref |
One row per gate execution. raw_payload_ref points to encrypted blob in object storage. |
invoices |
invoice | reference (pk), tenant_id, originator_id, obligor_id, face_value_pence, currency, tenor_days, maturity_date, state, token_reference, created_at |
The spine. token_reference populated post-tokenisation. |
invoice_documents |
invoice | id, invoice_reference, type (original_pdf, ocr_extract, vision_artefacts), storage_ref, hash |
Every artefact hashed; hash forms part of the audit trail. |
buyer_confirmations |
invoice | id, invoice_reference, confirmed_by (subject_id), confirmed_at, page_hash, ip, session_id |
One row at most per invoice. page_hash proves what the buyer saw. |
tokens |
tokenisation | reference (pk, e.g. FXR-INV-23A4F), invoice_reference, contract_address, token_id, state, minted_at, burned_at, burn_tx |
One per receivable; never reused. |
slices |
tokenisation | id, token_reference, index, face_pence, holder_subject_id, holder_wallet |
Created on fractionalisation; sum of face_pence equals the parent token's face. |
primary_listings |
marketplace | id, token_reference, asking_discount_bps, tenant_id, visible_to_lender_subject_ids, created_at |
Hides itself from lenders that breach concentration caps. |
secondary_listings |
marketplace | id, slice_id, asking_price_pence, created_by_subject_id, state |
Lender or investor can list. |
disbursements |
settlement | id, invoice_reference, lender_subject_id, net_advance_pence, discount_bps, state, fps_ref, seller_credit_acked_at |
F3 → F4 lifecycle; closed only on AIS confirmation of seller credit. |
collections |
settlement | id, invoice_reference, narration_match, amount_pence, received_at, onward_payments (jsonb) |
One row per inbound credit; onward_payments records lender + investor splits. |
trades |
secondary | id, slice_id, seller_subject_id, buyer_subject_id, price_pence, pre_trade_log_id, state, chain_tx, fiat_ref |
Pre-trade log linkage is mandatory; trade fails to PRE_TRADE_REQUIRED without it. |
pre_trade_logs |
secondary | id, investor_subject_id, slice_id, answers (7×bool), answered_at, ip |
One row per pre-trade attempt; many per trade if rerun. |
review_queue |
compliance | id, subject_id, resource, reason, opened_at, assigned_to, decided_at, decision, rationale_hash |
The MLRO queue. Closed entries remain queryable forever. |
sars |
compliance | id, subject_hash (not subject_id), nca_ref, filed_at, state |
Subject identity stored hashed in this table; full identity only retrievable via privileged join. |
audit_entries |
audit | id, timestamp, action, actor, actor_role, on_behalf_of, resource, diff, prev_hash, merkle_batch_id |
Append-only at DB level. Every entry hashes prior entry; batches anchor on chain every 10 min. |
tenants |
partner | id, display_name, custom_domain, theme (jsonb), plan, scope_rules |
FinToken X is itself a tenant (tnt_fintokenx); other tenants are partner deployments. |
agent_decisions |
compliance-ai | id, subject_id or trade_id, surface (gate3 / gate4 / pre-trade), verdict, confidence, model_version, policy_version, dataset_signature, explanation_ref, created_at |
One row per agent run. Advisory only. Deterministic replay given the version triple. Linked from the MLRO queue. |
sparsescore_explanations |
compliance-ai | id, agent_decision_id, top_signals (jsonb), counterfactual (jsonb), regulatory_anchors (jsonb), payload_ref, hash |
One per decision. Full payload kept in object storage; hash on this row is the audit anchor. |
Tenant isolation
State machines
Invoice
Subject (any role)
Buyer-specific lifecycle
Token lifecycle
External integrations
| Integration | Used by | Why | Failure mode |
|---|---|---|---|
| Sumsub (Know Your Customer (KYC) + biometric + liveness) | kyc | Gate 1 — identity verification on individuals (Ultimate Beneficial Owners (UBOs), directors, named representatives) and Know Your Business (KYB) on entities. | Webhook delays → onboarding files held in VERIFYING; alert if > 60s. No silent approvals. |
| ComplyAdvantage (sanctions + Politically Exposed Person (PEP) + adverse media) | kyc + monitoring | Gate 2 — name screening at onboarding; daily re-screen (Gate 4). | API down → gate held; daily re-screen has 24h budget before alert. |
| Chainalysis (on-chain attribution) | kyc + monitoring + secondary | Wallet screening for stablecoin-opted lenders + investors; on-chain compliance modules consult before transfer. | Transfers fall back to fiat-only if Chainalysis is unreachable; never silently approve. |
| Companies House API | kyc | KYB on entities; ongoing monitoring for status changes (insolvency, dissolution). | Cached daily; cache > 7d old triggers manual re-check. |
| FCA register API | kyc + monitoring | Lender + broker permission verification; daily re-check (Gate 4). | Cache > 24h triggers a non-blocking alert; subject remains active until cache > 7d. |
| Confirmation of Payee (CoP) | kyc | Bank-account name match for seller and lender designated accounts. | CoP failures route to MLRO queue with the mismatch payload. |
| Open Banking AIS | settlement | Inbound credit reconciliation on the seller's bank (advance landed) and FinToken X collections (obligor paid). | Falls back to scheduled bank-statement parsing; alert if reconciliation > 4h. |
| Faster Payments / Bacs (via banking partner) | settlement | Outbound payments — disbursement leg (lender → seller) and settlement leg (FinToken X → lender, → investor). | Failed payments retry within Faster Payments Service (FPS) rules; persistent failures escalate to ops. |
| FinToken X Network (permissioned Ethereum Virtual Machine (EVM)) | tokenisation + audit | ERC-3643 token mint/transfer/burn; Merkle anchors of audit batches. | Block production stalled → mint queued; tokens marked PENDING_MINT until chain recovers. |
| D&B / Experian (corporate credit) | kyc · risk model | Inputs to the risk model — buyer credit grade, originator credit grade. | Cached weekly; one-off failures fall back to last-known. |
| Simple Email Service (SES) + DKIM (transactional email) | notification | All transactional emails (seller, buyer, lender, investor, broker, partner, MLRO digest). | Tenant DKIM failure → fall back to FinToken X's own DKIM with a banner; alert on first occurrence. |
| National Crime Agency (NCA) reporting portal | compliance | SAR filing destination. | Submission failures retry; Service-Level Agreement (SLA) breach (3 BD) escalates to MLRO board. |
| HSM-backed vault | all | Provider credentials, signing keys (DKIM, webhook signatures, audit Merkle commit). | Vault unavailable → modules that depend on it fail closed (cannot sign, cannot fetch credentials). |
| Graphics Processing Unit (GPU) compute · training and inference | compliance-ai | GPU-accelerated environment for training the agent's policy and running inference on Gate 3, Gate 4, and pre-trade events. Synthetic datasets (fraud, identity, Anti-Money Laundering (AML), consumer behaviour) are used for training so no customer data crosses the production Virtual Private Cloud (VPC) boundary. | GPU node unavailable → compliance-ai stops emitting proposals; MLRO queue continues to operate manually with the existing rule-based score. No customer flow blocked. |
Chain & ERC-3643
The token mechanics, in detail.
Network
- FinToken X Network — permissioned EVM, Hyperledger Besu, Istanbul Byzantine Fault Tolerance (IBFT) 2.0 consensus.
- Validators: FinToken X (3 nodes), Mercia Bank (2 nodes), one future second partner (2 nodes). Quorum: 5 of 7.
- Block time 2s; finality on consensus block (~2s).
- No public bridge. RPC access gated by mutual Transport Layer Security (mTLS)-authenticated relay; explorer access limited per-subject to own holdings.
- Native gas token has no commercial value; metered for spam protection only. FinToken X funds gas for all token operations on behalf of users.
Contracts
| Contract | Purpose |
|---|---|
FXReceivableToken (ERC-3643) | The receivable token. One contract per environment (production / sandbox); per-receivable instance is a tokenId. |
FXIdentityRegistry | Maps wallets ↔ verified subjects. Every wallet eligible to hold an FX token has an entry. |
FXClaimsTopicRegistry | List of claim topics: KYC_VERIFIED, AML_CLEARED, MIFID_PROFESSIONAL, MIFID_ECP, JURISDICTION_UK. |
FXTrustedIssuersRegistry | Single trusted issuer: FinToken X. No third-party claim issuers. |
FXComplianceModularised | Master compliance module — chains SanctionsRule, MiFIDRule, JurisdictionRule, LockupRule. |
Mint flow
invoice.state = CONFIRMED(buyer clicked C2).tokenisation.mint(invoice_reference)called. GeneratestokenId; computesface_pence.- Calls
FXReceivableToken.mint(receiver=fx_minting_wallet, tokenId, face)with FinToken X validator signing. - On confirm,
token.state = MINTEDin domain DB; audit entry written. - Listing event fires; marketplace surfaces it to lenders within filter.
Transfer flow (lender funds)
- Lender calls
POST /v1/paymentswith idempotency key. - Settlement service initiates fiat leg (FPS from lender's designated account → FinToken X collections).
- Tokenisation service calls
FXReceivableToken.transferFrom(fx_minting_wallet, lender_wallet, tokenId). - The chain Compliance Module checks: lender has
KYC_VERIFIED+AML_CLEARED+JURISDICTION_UKclaims. If yes, transfer succeeds. - Audit entry:
FUND_INVOICE, both fiat and chain references attached.
Compliance module logic
Pseudocode, in the order the rules run on every transfer:
function canTransfer(from, to, tokenId, amount) returns (bool, reason) {
// 1. Both wallets must be in the Identity Registry
if (!identityRegistry.isVerified(from) || !identityRegistry.isVerified(to))
return (false, "wallet not verified");
// 2. Receiver must hold required claim topics
for topic in [KYC_VERIFIED, AML_CLEARED, JURISDICTION_UK]:
if (!claimsRegistry.has(to, topic))
return (false, "missing claim: " + topic);
// 3. If transferring to an investor (not the original lender), MiFID classification required
if (isInvestor(to) && !claimsRegistry.hasOneOf(to, [MIFID_PROFESSIONAL, MIFID_ECP]))
return (false, "MiFID classification required");
// 4. Lock-up (post-funding cooldown — none currently active, future-proofing)
if (token.isLocked(tokenId)) return (false, "token locked");
// 5. Sanctions delta — read latest, fail closed
if (sanctionsCheck.isFlagged(from) || sanctionsCheck.isFlagged(to))
return (false, "sanctions");
return (true, "ok");
}
Burn flow
collectionsreconciliation matches obligor inbound credit toinvoice_reference.- Onward payments fire to lender + (if fractionalised) all slice holders.
- On all onward payments confirmed,
FXReceivableToken.burn(tokenId)called. - Token state →
BURNED;burned_at+burn_txrecorded; audit entry anchored.
Audit log & tamper-evident store
The single most regulator-relevant subsystem on the platform.
Architecture
- Append-only Postgres table
audit_entrieswith noUPDATEgrants.DELETEis database-level revoked. - Every entry includes
prev_hash(hash of prior entry) — forms a hash chain. - Every 10 minutes the audit service reads pending entries, builds a Merkle tree, anchors the root on the FinToken X Network in the
FXAuditAnchorcontract. - The chain transaction hash is then written back to the
merkle_batchestable. - Verification: anyone with read access can recompute a Merkle path from a target entry to the anchor and verify against on-chain state.
Action taxonomy (illustrative)
| Action | Customer-facing | Requires on_behalf_of from admin |
|---|---|---|
| SUBJECT_CREATE | yes | yes |
| SUBMIT_INVOICE | yes | yes |
| CONFIRM_INVOICE | yes | yes |
| FUND_INVOICE | yes | yes |
| SECONDARY_TRADE | yes | yes |
| RECEIVE_SETTLEMENT | yes | yes |
| MLRO_APPROVE / MLRO_DECLINE | no | n/a (compliance role) |
| SAR_FILE | no | n/a (compliance role) |
| SUBJECT_FREEZE | no | n/a (compliance role) |
| TOKEN_MINT / TOKEN_BURN | no (system) | no |
| WEBHOOK_DELIVERED | no (system) | no |
| RBAC_BYPASS | no | yes |
| PROVIDER_CRED_ROTATE | no | n/a (admin role) |
| AGENT_PROPOSE_VERDICT | no | n/a (system · advisory only) |
| AGENT_MODEL_DEPLOY / AGENT_MODEL_ROLLBACK | no | n/a (platform-admin role) |
Security boundaries
- Module call boundaries are enforced in code, not on the network. The backend is a single process; there is no internal service mesh. Every module entry point checks the caller's role and tenant scope before doing anything; the unit tests pin those checks.
- External REST surface is JSON Web Token (JWT) + per-tenant API key. JWT signed by the auth module; API key carries tenant scope. Both required for sensitive endpoints.
- Webhook signatures HMAC-SHA256 over
{timestamp}.{body}. Receivers must verify the timestamp is within 5 minutes of now (replay window). - HSM-backed signing keys for DKIM, webhook secrets, audit Merkle commit, chain transaction signing.
- Row-Level Security (RLS) at the database — connection-level tenant pinning; no application-only tenant filtering.
- Object storage encryption at rest — invoice PDFs, Sumsub raw payloads, OCR artefacts, all per-tenant keys.
- Quarterly cross-team red-team of the bypass-marking invariant: try to take a customer action as platform admin without leaving a fingerprint, fail closed.
Environments
| Env | Audience | Chain | Providers |
|---|---|---|---|
| local | Engineering | Anvil dev chain | Mock providers |
| preview | Engineering + design | Sandbox FinToken X Network | Sumsub sandbox, ComplyAdvantage sandbox |
| ai-lab | compliance-ai training + inference | Mirror of sandbox chain · read-only on customer state | Synthetic datasets only · GPU-backed · no production customer data crosses the boundary |
| sandbox | FCA / Bank of England (BoE) Sandbox programme | Sandbox FinToken X Network | Live providers, sandbox keys; live banking rails with real money in capped envelopes |
| production | Live customers | Production FinToken X Network | Live providers, live keys |
Rate limits
| Surface | Default | Tier overrides |
|---|---|---|
| Lender API | 600 req/min · 1m req/month included | +£0.0008 / req beyond included |
| Broker Score API | 120 req/min · 50k req/month included | Plan-gated |
| Partner API | 1,200 req/min · 5m req/month included | Plan-gated |
| Webhook delivery | per-endpoint exponential backoff up to 24h | — |
| Browser Single-Page Application (SPA) | per-IP 600 req/min on read endpoints | — |
tenant_id; (2) the database connection used by an HTTP request has a Postgres-levelrow_securitypolicy that pins all queries to the caller's tenant; (3) cross-tenant queries (used by FinToken X support) require a platform-admin session that itself setsSET LOCAL fx.bypass_rls = TRUEwithactor_role = platform_adminrecorded — and the bypass is itself an audit entry.