FinToken XArchitectureBackend & data

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

  1. 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.
  2. 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.
  3. 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.
  4. Money is integer pence. Never float, never decimal. Currency is always Great British Pound (GBP) unless explicitly USD Coin (USDC) on a stablecoin settlement.
  5. Idempotency at every external boundary. Every Application Programming Interface (API) mutation requires FX-Idempotency-Key; replays return the original response.
  6. Append-only audit; mutable state. Domain rows can be updated; audit rows can never be (database-level append-only constraint).
  7. Compliance fails closed. If a downstream provider (Sumsub, ComplyAdvantage) is unreachable, the gate stays open in pending state — never silently approves.
  8. 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.
  9. 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

authsession invoicesubmit auditlog
invoice notificationbuyer email authmagic link
invoicebuyer confirms tokenisationmint marketplacelist
marketplacelender funds tokenisationtransfer settlementdisburse auditanchored

Data model

Key tables. Column lists are illustrative — the canonical schema lives in migrations.

Core tables

TableOwned byKey columnsNotes
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

Tenant isolation is enforced at three layers: (1) every domain row carries tenant_id; (2) the database connection used by an HTTP request has a Postgres-level row_security policy 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 sets SET LOCAL fx.bypass_rls = TRUE with actor_role = platform_admin recorded — and the bypass is itself an audit entry.

State machines

Invoice

DRAFT SUBMITTED VERIFYING VERIFIED
VERIFIED AWAITING_BUYER CONFIRMED TOKENISED LISTED
LISTED FUNDED DISBURSED SETTLED
FUNDED PAST_DUE DEFAULT
VERIFYING REJECTED_VISION
VERIFYING REJECTED_DUPLICATE
AWAITING_BUYER DISPUTED VOID
AWAITING_BUYER EXPIRED5 Business Days (BD) no buyer reply

Subject (any role)

CREATED ONBOARDING_SUBMITTED KYC_PASSED SCREENING_PASSED RISK_PASSED ACTIVE
SCREENING_PASSED MLRO_REVIEW RISK_PASSED
MLRO_REVIEW DECLINED
ACTIVE FROZEN ACTIVE
FROZEN DECLINED

Buyer-specific lifecycle

INVITEDFrom a supplier's invoice ONBOARDING_LITE ACTIVE
ACTIVEper-invoice loop NOTIFIEDC1 email CONFIRMEDC2 click INSTRUCTEDC4 reminder PAIDC5

Token lifecycle

PENDING_MINT MINTEDwhole token TRANSFERRED_TO_LENDER
TRANSFERRED_TO_LENDER FRACTIONALISEDN slices SLICE_TRANSFERREDto investor
TRANSFERRED_TO_LENDER BURNEDon settlement
SLICE_TRANSFERRED SLICE_TRANSFERREDresale BURNED

External integrations

IntegrationUsed byWhyFailure 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

ContractPurpose
FXReceivableToken (ERC-3643)The receivable token. One contract per environment (production / sandbox); per-receivable instance is a tokenId.
FXIdentityRegistryMaps wallets ↔ verified subjects. Every wallet eligible to hold an FX token has an entry.
FXClaimsTopicRegistryList of claim topics: KYC_VERIFIED, AML_CLEARED, MIFID_PROFESSIONAL, MIFID_ECP, JURISDICTION_UK.
FXTrustedIssuersRegistrySingle trusted issuer: FinToken X. No third-party claim issuers.
FXComplianceModularisedMaster compliance module — chains SanctionsRule, MiFIDRule, JurisdictionRule, LockupRule.

Mint flow

  1. invoice.state = CONFIRMED (buyer clicked C2).
  2. tokenisation.mint(invoice_reference) called. Generates tokenId; computes face_pence.
  3. Calls FXReceivableToken.mint(receiver=fx_minting_wallet, tokenId, face) with FinToken X validator signing.
  4. On confirm, token.state = MINTED in domain DB; audit entry written.
  5. Listing event fires; marketplace surfaces it to lenders within filter.

Transfer flow (lender funds)

  1. Lender calls POST /v1/payments with idempotency key.
  2. Settlement service initiates fiat leg (FPS from lender's designated account → FinToken X collections).
  3. Tokenisation service calls FXReceivableToken.transferFrom(fx_minting_wallet, lender_wallet, tokenId).
  4. The chain Compliance Module checks: lender has KYC_VERIFIED + AML_CLEARED + JURISDICTION_UK claims. If yes, transfer succeeds.
  5. 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

  1. collections reconciliation matches obligor inbound credit to invoice_reference.
  2. Onward payments fire to lender + (if fractionalised) all slice holders.
  3. On all onward payments confirmed, FXReceivableToken.burn(tokenId) called.
  4. Token state → BURNED; burned_at + burn_tx recorded; audit entry anchored.

Audit log & tamper-evident store

The single most regulator-relevant subsystem on the platform.

Architecture

  • Append-only Postgres table audit_entries with no UPDATE grants. DELETE is 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 FXAuditAnchor contract.
  • The chain transaction hash is then written back to the merkle_batches table.
  • 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)

ActionCustomer-facingRequires on_behalf_of from admin
SUBJECT_CREATEyesyes
SUBMIT_INVOICEyesyes
CONFIRM_INVOICEyesyes
FUND_INVOICEyesyes
SECONDARY_TRADEyesyes
RECEIVE_SETTLEMENTyesyes
MLRO_APPROVE / MLRO_DECLINEnon/a (compliance role)
SAR_FILEnon/a (compliance role)
SUBJECT_FREEZEnon/a (compliance role)
TOKEN_MINT / TOKEN_BURNno (system)no
WEBHOOK_DELIVEREDno (system)no
RBAC_BYPASSnoyes
PROVIDER_CRED_ROTATEnon/a (admin role)
AGENT_PROPOSE_VERDICTnon/a (system · advisory only)
AGENT_MODEL_DEPLOY / AGENT_MODEL_ROLLBACKnon/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

EnvAudienceChainProviders
localEngineeringAnvil dev chainMock providers
previewEngineering + designSandbox FinToken X NetworkSumsub sandbox, ComplyAdvantage sandbox
ai-labcompliance-ai training + inferenceMirror of sandbox chain · read-only on customer stateSynthetic datasets only · GPU-backed · no production customer data crosses the boundary
sandboxFCA / Bank of England (BoE) Sandbox programmeSandbox FinToken X NetworkLive providers, sandbox keys; live banking rails with real money in capped envelopes
productionLive customersProduction FinToken X NetworkLive providers, live keys
Sandbox is the load-bearing environment for the first 12–18 months. The FCA / BoE Digital Securities Sandbox (DSS) is a live regulatory regime with real money — not a test environment in the engineering sense. Promotion from sandbox to full production requires the full Head of Compliance hire and FCA sign-off on the Sandbox exit memo.

Rate limits

SurfaceDefaultTier overrides
Lender API600 req/min · 1m req/month included+£0.0008 / req beyond included
Broker Score API120 req/min · 50k req/month includedPlan-gated
Partner API1,200 req/min · 5m req/month includedPlan-gated
Webhook deliveryper-endpoint exponential backoff up to 24h
Browser Single-Page Application (SPA)per-IP 600 req/min on read endpoints