An end-to-end demo of the technology stack behind a short-duration business-credit fintech — a company that lends working capital to small businesses and funds those loans by distributing them to institutional investors as on-chain vault shares. Every component runs end to end against a real local Postgres database and a real local Hardhat blockchain node — no mocked-up UI over fake data.
CreditRail is an end-to-end demo of the technology stack behind a short-duration business-credit fintech: a company that lends working capital to small businesses and funds those loans by distributing them to institutional investors as on-chain vault shares. It was built as a portfolio project to demonstrate the exact skill set a fractional CTO / technical-lead role for such a company requires — architecture, ML-based risk scoring, lending business logic, tokenization, security/audit posture, and the judgment to know what to build versus what to buy.
Every component is real and runs end to end against a local Postgres database and a local Hardhat blockchain node — there is no mocked-up UI sitting on top of fake data.
A pre-pilot fintech lending to small businesses has to solve five different problems at once, and they don't compose cleanly:
Decide, from messy real-world data (bank statements), whether a business is a safe credit risk, without a credit bureau history to lean on.
"Will this business repay" and "is this business/its documents even real" are different questions. Conflating them into one model is exactly the kind of black-box design a regulator or auditor pushes back on.
Once approved, someone has to track amortization, payments, missed payments, and defaults correctly, because this is money, not a demo toy.
The company doesn't have its own balance sheet deep enough to fund loans itself; it needs to package approved loans into a pool and raise capital from institutional investors against that pool.
Institutional investors expect a transparent, auditable way to hold and value their position (which points toward tokenization), but also expect KYC enforcement that doesn't disappear the moment a token changes hands — and won't accept "one private key controls everything" as a custody answer.
The hard part isn't any one of these in isolation — it's that they have to be wired together (ingestion feeds ML feeds underwriting feeds loans feeds pools feeds on-chain accounting) without the seams silently drifting apart, while keeping every state change auditable.
CreditRail implements the full pipeline as nine cooperating pieces, every arrow below a real HTTP call or on-chain transaction — reproducible via the Streamlit dashboard, pytest, or hardhat test, not just a diagram.
Bank statement CSV / invoice image
│
▼
Ingestion (parse + OCR)
│
▼
Feature engineering (shared by training AND inference)
│
▼
XGBoost risk model + independent fraud rule engine
│
▼
Underwriting decision → Loan issuance → amortized repayment schedule
│
▼
Pool aggregation (off-chain grouping of approved loans)
│
▼
CreditVault.sol (ERC-4626, on-chain) ← institutional investors
│ permissioned deposit / withdraw / transfer (Allowlist-gated)
▼
Gasless deposits for KYC'd investors (ERC-4337 account abstraction)
| Layer | Technology |
|---|---|
| Backend | Python, FastAPI, SQLAlchemy, PostgreSQL, Alembic |
| ML / Risk | scikit-learn, XGBoost, pandas |
| Chain | Solidity 0.8.24, OpenZeppelin 5, Hardhat, eth-infinitism ERC-4337 reference contracts |
| Chain Bridge | web3.py |
| Dashboard | Streamlit |
| Infra | Docker Compose |
The build was sequenced in nine phases, each one deliberately laying a foundation the next phase depends on — from backend auth through data ingestion, ML scoring, loan servicing, smart contracts, the chain bridge, and finally gasless account abstraction for investors.
FastAPI + PostgreSQL + SQLAlchemy + Alembic, JWT auth with bcrypt password hashing, and role-based access control (admin / underwriter / investor) enforced per-endpoint. Every state-changing action writes to an AuditLog table.
A from-scratch bank-statement CSV parser and categorizer, plus a Tesseract-OCR-based invoice/receipt field extractor (vendor, invoice number, date, amount) for document-extraction demo purposes.
Application creation, scoring-gated underwriting decisions, amortized repayment-schedule generation, and payment/missed-payment/default tracking.
A synthetic-data generator (since no real loan performance data exists for a pre-pilot company), an XGBoost risk classifier, and a rule-based fraud engine kept deliberately separate from the ML score — so a regulator can ask "why was this flagged" independently for each question.
Once issued, loans move into servicing — an amortized repayment schedule with due dates, amounts due/paid, and status, tracked per loan so payments, missed payments, and defaults are never lost in a spreadsheet.
Off-chain grouping of approved loans into a pool — loans in pool, total principal, and weighted average rate — the step that packages individual loans into something an institutional investor can fund as a single position.
CreditVault.sol (ERC-4626), Allowlist.sol (KYC registry), and MockUSDC.sol (test stablecoin), built on OpenZeppelin bases and tested with Hardhat/Mocha/Chai — total assets, deployed capital, cash on hand, and live share price, plus an investor wallet lookup.
web3client/vault_client.py, called from a small set of explicit, admin-triggered endpoints — allow an investor onto the KYC allowlist, disburse loan capital to a borrower, record a repayment (principal + yield), or record a default write-off — rather than inlined into loan servicing.
An ERC-4337 paymaster so KYC'd institutional investors can deposit into the vault without ever holding ETH for gas — a real institutional-onboarding problem solved with account abstraction rather than asking every investor to manage a funded wallet.
ml/features.py has no FastAPI or training-script imports — it operates on anything shaped like a transaction, whether that's a live database row or synthetic training data run through the same parser. This closes off a common, silent class of bug: a model trained on one feature computation and served on a slightly different one, with no error, just quietly wrong scores.
Recording a payment or a default in the database never directly calls the blockchain. The database stays the system of record for loan status (which has regulatory/contractual timelines that can't wait on chain congestion), and a handful of admin-triggered endpoints reconcile confirmed off-chain events onto the vault as a separate, auditable step — mirroring how production fintech-to-chain bridges are actually built.
A KYC allowlist that only gates the initial deposit() call is a compliance hole — a holder could freely transfer shares to an unapproved wallet afterward. CreditVault overrides three separate hooks (_deposit, _withdraw, _update) because OpenZeppelin's ERC-4626/ERC-20 base contracts don't expose one hook that covers deposit, withdraw, and peer-to-peer transfer at once. Each path has its own test so this doesn't regress silently.
"Will this business repay" (a probabilistic credit judgment) and "is this business/its documents legitimate" (closer to a compliance check) are answered by two independent systems — an XGBoost risk score and a rule-based fraud engine (NSF-fee frequency, round-number deposits, revenue spikes, insufficient history). A single black-box model that mixes both doesn't give a defensible answer to an auditor.
While building the gasless-deposit flow, SimpleAccountFactory.getAddress(owner, salt) (the CREATE2 address prediction) returned a different address than what createAccount(owner, salt) actually deployed to, in the reference contracts pulled in for this project. Fix: use the address createAccount actually returns (captured via a .staticCall before sending the real transaction) instead of trusting the predicted address — "counterfactual address" tricks need to be verified against the actual factory version in use, not assumed correct from the spec.
It would have been easy to present the vault's NAV accounting as fully trustless. It isn't — a single admin key currently asserts that a repayment or default happened off-chain. Rather than hide that, the design docs (RWA_TOKENIZATION.md, BUILD_VS_BUY.md) state it plainly and describe the concrete path to a real attestation/oracle design or a multi-sig/custody-provider setup.
After the initial build was verified via pytest/hardhat test, walking the dashboard flow end-to-end (rather than just the API) surfaced that the invoice-OCR endpoint (POST /documents/extract-invoice) — fully implemented and tested on the backend — had no corresponding upload control in the Streamlit UI. Unit and integration tests verify the pieces work, not that every piece is wired to the surface a user touches. Fixed with a straightforward UI addition calling the already-existing extract_invoice client method.
A scoring call failed with "No transactions on file for this business," even though bank-statement data had just been uploaded. Root cause: the dashboard's ingestion section and its scoring section use two independent "select a business" dropdowns, and a business had been created twice (an accidental double-submit) — so the CSV was uploaded against one Business row while the loan application lived under a different one with the same display name. A UX footgun, not a logic bug — the kind of failure mode that only shows up when a real person drives the UI instead of a script calling the API directly.
Mapped explicitly against SOC 2 Trust Service Criteria in docs/SECURITY_COMPLIANCE.md:
Just as important, the same document states plainly what a demo like this does not cover — formal policies, real infrastructure controls, a third-party smart-contract audit and penetration test, continuous monitoring, and a real secrets-management story — because a case study that only lists what's done and omits what isn't reads as marketing, not engineering.
All numbers verified against a real local Hardhat node and Postgres database — reproducible via pytest, hardhat test, and contracts/scripts/gasless_investor_deposit.js, not just claimed.
Stated deliberately, because a demo that hides its limitations is worth less than one that states them:
For a hands-on fractional CTO / technical-lead role at an early-stage lending fintech, this project is meant to show, with working code rather than a resume bullet:
We build custom underwriting engines, loan servicing systems, and on-chain investor distribution tailored to your lending model and regulatory environment.