Sapphire Bank has 1,000 corporate current-account customers, each with 2 to 5 partners, and every company has applied for a loan. Credora brings the bank's own data and outside data on every company and partner (CIBIL, GST and Account Aggregator) into Exasol. Exasol scores each company with an ML model it trained itself, gives it a decision, an offer (amount, ROI, tenure) and a negotiation limit, and ranks the best 50 for the relationship managers.
Exasol is the analytics (OLAP) platform. The bank's data stays in three RDS databases (Oracle, MySQL, PostgreSQL) and is read in place through virtual schemas. CIBIL, GSTN and the Account Aggregator are mock APIs, because this is a project and not connected to the real services. Everything runs on AWS from one repo with one command. The site is at https://credora.loan now and https://credora.link later.
Four architecture diagrams, each one page with export to PNG or PDF (all diagrams):
RUN_ANALYSIS with source, rows and timing, ending in the PREFERRING top 50 and the CRM export.Everything starts with data. The admin screen has a Generate new mock data button. It first asks for confirmation:
On confirm, the generator truncates every table, creates a new data set and writes each part to the place where it would really live. Each run is seeded, and the same seed gives the same data on the same day. A full run takes about 75 seconds on AWS.
| What | How much | Stored in | Seen as |
|---|---|---|---|
| Companies (corporate current-account customers), relationship managers | 1,000 | MySQL cif.company, cif.rm | Sapphire Bank · MySQL |
| Partners (directors, partners, promoters) | 2–5 per company, about 3,500 | MySQL cif.partner, view partner_profile | Sapphire Bank · MySQL |
| Company current account and its transactions | 12–24 months each, about 850,000 rows | Oracle CBS.ACCOUNT, CBS.TXN | Sapphire Bank · Oracle |
| Partners' savings accounts at Sapphire Bank | about 30% of partners | Oracle CBS.ACCOUNT | Sapphire Bank · Oracle |
| Loan application (product, amount, tenure, purpose, collateral) | 1 per company | PostgreSQL los.application | Sapphire Bank · PostgreSQL |
| Past loans with outcomes (about 8% defaulted) | 5,000 | PostgreSQL los.past_loan, view past_loan_training | Sapphire Bank · PostgreSQL, used to train the ML model |
| Offers and negotiation trail | empty until the first analysis | PostgreSQL crm.offer, crm.offer_event | Sapphire Bank · PostgreSQL |
| CIBIL consumer report per partner | 1 per partner | Mock CIBIL database | API · CIBIL (mock) |
| CIBIL commercial report per company (CMR rank) | 1 per company | Mock CIBIL database | API · CIBIL (mock) |
| GST registration and monthly returns | last 2–10 years, random | Mock GSTN database | API · GSTN (mock) |
| Partners' accounts at other banks (SBI, HDFC Bank, ICICI Bank, Axis Bank, Kotak, PNB, Bank of Baroda, Canara Bank …) | 1–3 per partner, 12 months of transactions | Mock AA database | API · Account Aggregator (mock) |
| Hidden true risk per company | 1 per company | mockdb truth and S3 | Admin scorecard only; Exasol never reads it |
| Years of GST | Stage |
|---|---|
| 2–4 | New |
| 5–7 | Mid-level |
| 8–10 | Stable |
Every piece of data in the UI carries a source tag, and every company opens as a tree of links. Click a node to see its records.
Shree Ganesh Polymers LLP · C000042 · RM007 Sapphire Bank · MySQL ├─ Current account ••4213 · 18 months of transactions Sapphire Bank · Oracle ├─ Loan application A0000042: ₹4.5 cr · TL · 60 months Sapphire Bank · PostgreSQL ├─ GST: GSTIN 27AAxxxxxx1Z5 · 7 years · Mid-level API · GSTN (mock) │ └─ 2019 … 2026 · monthly GSTR-1 / GSTR-3B ├─ CIBIL commercial: CMR rank 3 API · CIBIL (mock) ├─ Partner 1: R. Mehta · 40% Sapphire Bank · MySQL │ ├─ CIBIL: 782 · 4 accounts · 0 overdue API · CIBIL (mock) │ ├─ Savings ••8890 at Sapphire Bank Sapphire Bank · Oracle │ └─ Savings at HDFC Bank API · AA (mock) ├─ Partner 2: S. Iyer · 35% │ ├─ CIBIL: 741 API · CIBIL (mock) │ └─ Current at SBI · Savings at ICICI Bank API · AA (mock) ├─ Partner 3: A. Khan · 25% … └─ Exasol report: Approve · rank 37 of 1,000 · why and how Exasol result
| Role | Sapphire Bank data | External API data | Exasol report (why, how) | Can do |
|---|---|---|---|---|
| Admin | All | All | Yes | Generate new mock data, run analysis, source health, audit, purge proof |
| Data scientist | All | All | Yes | Train a model inside Exasol, compare and activate versions, what-if on policy (hurdle, EBLR, GST share) |
| Loan operations | All | No raw replies; the features derived from them | Yes | Run analysis, open any company's report, verify and sanction (maker-checker) |
| Relationship manager | Masked company view (PAN, GSTIN masked) | No | Offer, limit and reasons | Top 50, quote and propose terms within the limit; above it the offer is escalated |
The web app logs in to Exasol as the database user that matches the signed-in person's role, so the database itself enforces what each screen can show. A bug in the app cannot widen access.
| Exasol user | Role | Can read or do |
|---|---|---|
CREDORA_SVC | R_SVC | Runs analysis, training and purge; writes CREDORA and CREDORA_STAGE; uses the API, BucketFS and AI connections |
CREDORA_ADMIN, CREDORA_DATASCI | R_ADMIN, R_DATASCI | Everything, read-only: CREDORA, CREDORA_STAGE, VS_CBS, VS_CIF, VS_LOS |
CREDORA_LOANOPS | R_LOANOPS | CREDORA and the three virtual schemas; not CREDORA_STAGE (raw CIBIL, GST, AA data) |
CREDORA_RM | R_RM | DECISIONS, SCORES, CONFIG, RUN_LOG and V_COMPANY_MASKED |
CREDORA_MCP | R_MCP | Read-only results for Ask Credora: DECISIONS, SCORES, FEATURES, RUN_LOG, MODEL_REGISTRY, CONFIG, V_COMPANY_MASKED |
The risk model is trained and scored by Python UDFs in Exasol's PYTHON3 script language container, with scikit-learn. The training data is read live from PostgreSQL through VS_LOS, and the data never leaves the database to be scored.
| Object | Kind | Job in Credora |
|---|---|---|
TRAIN_PD (SET UDF), run by Lua TRAIN_MODEL | scikit-learn HistGradientBoostingClassifier with monotonic constraints (for example, more late GST filings can only raise risk) | Trains on 5,000 past loans, measures AUC, Gini and permutation importance on a hold-out set, saves models/pd_<version>.pkl to BucketFS and registers it in MODEL_REGISTRY. Model v1: AUC 0.745 on the hold-out set. |
PD_SCORE (SET UDF, 4 parallel instances) | Loads the active model from BucketFS | Probability of default for every application, plus how much each of the 13 features moved it. PD maps to a grade: A1 < 1%, A2 < 2%, B1 < 3.5%, B2 < 5%, C1 < 8%, C2 < 12%, else D. |
CREDIT_MEMO (scalar UDF) | Calls Claude through the connection AI_LLM, created only when an API key is configured | A short credit memo in plain English per company for the relationship manager, from the decision, the numbers and the top risk drivers |
| Ask Credora (backend) | Claude with the Exasol MCP server, logged in as the read-only CREDORA_MCP user; Amazon Bedrock is the alternative provider | Plain-English questions about the results, answered only from what that role may read |
PREFERRING HIGH RAROC_OFFER PLUS LOW PD PLUS HIGH OFFER_AMOUNT returns the skyline of approved companies that no other company beats on return, risk and size at once. Peeling off skyline after skyline gives layers; the best 50 go to the RMs. No hand-tuned weights.RUN_ANALYSIS trains v1 itself if no model exists.| Step | What happens | In Exasol |
|---|---|---|
| 1 · Combine | Application (PostgreSQL) joined with company and partners (MySQL), in place | CREDORA.APPLICATIONS |
| 2 · Features | 13 features: GST years, turnover, late filings, GST-vs-bank gap, average balance, inflow volatility, bounce ratio, CMR rank, partners' minimum and average score, overdue, other-bank balance, DSCR | Window functions over CA_MONTHLY (aggregated in Oracle) and the API data → FEATURES |
| 3 · Risk | Probability of default, grade, per-feature attribution | PD_SCORE → SCORES |
| 4 · Hard fails | Wilful defaulter, CMR rank 9 or worse, 4+ late GST returns in 12 months, any partner CIBIL below 600, bounce ratio above 8% | SQL |
| 5 · ROI | EBLR + risk spread by grade (1.00% for A1 to 7.00% for D) + operating spread, minus 0.25% if collateral covers the loan; never below the floor | SQL |
| 6 · Amount | Term loan: what 12% of GST turnover can repay at DSCR 1.25. Working capital: the smaller of 20% of GST turnover and the same cash-flow cap. Never above the amount asked. | SQL |
| 7 · Tenure | Term loan: as asked, up to 84 months (60 for grades C1, C2, D). Working capital: 12 months. | SQL |
| 8 · Negotiation limit | Floor ROI where RAROC still meets the hurdle (and at least EBLR + 0.50%); maximum amount at DSCR 1.10, up to 115% of the amount asked; maximum tenure +24 months, up to 84 | SQL |
| 9 · Decision | Decline on any hard fail; Refer if KYC is not verified, grade is C2 or D, or the offer is below half the amount asked; otherwise Approve. RAROC = (ROI − funds − opex − PD × LGD) / capital. | SQL → DECISIONS |
| 10 · Rank | Skyline layers of approved companies, rank among 1,000, top 50 | PREFERRING |
Shree Ganesh Polymers LLP · 3 partners · Mid-level (7 yrs GST)
Decision: APPROVE · Grade A2 · PD 1.8% · Rank 37 of 1,000
OFFER LIMIT IF NEGOTIATED
Amount ₹4.00 cr ₹4.50 cr (max)
ROI 10.25% 10.15% (floor)
Tenure 60 months 84 months (max)
RAROC 16.2% 15.3% (hurdle 15%)
Why (feature attribution):
+ GST turnover matches bank credits (gap 6%)
+ no bounces in 12 months
+ all partners CIBIL 740+
− partner 3 has 2 recent enquiries
Sources: Oracle CBS · MySQL cif · PostgreSQL los · CIBIL · GSTN · AAwww resolve to the app host's Elastic IP./api to the backend, and redirects www and HTTP to https://<DOMAIN>.CREDORA_MCP.crm: offers, negotiation steps and sanction (small, frequent writes).| Exasol feature | Object | Job |
|---|---|---|
| Virtual schema: Oracle | VS_CBS on ADAPTER.ORACLE_ADAPTER (Oracle VS 6.0, Java, ojdbc17), connection ORA_CBS | Reads core banking (ACCOUNT, TXN) in place, with pushdown |
| Virtual schema: adbc-vs-rs | VS_CIF (MySQL) and VS_LOS (PostgreSQL) on ADBC_VS.ADBC_ADAPTER + ADBC_VS.ADBC_SCAN, connections MYSQL_CIF, PG_LOS | One Rust adapter for two engines, moving Arrow record batches |
| Custom script language container | RUST: Rust SLC 0.23.1 in BucketFS, added to SCRIPT_LANGUAGES | Runs the adbc-vs-rs adapter at the SDK version it was built for |
| BucketFS | slc/rustslc, udf/libadbc_vs_adapter.so, ADBC drivers (downloaded on first use), Oracle VS jar, drivers/jdbc/ojdbc17.jar, models/pd_<version>.pkl | One store for code, drivers and every model version |
| Python UDFs: external APIs | CIBIL_CONSUMER_FETCH, CIBIL_COMMERCIAL_FETCH, GST_FETCH, AA_FETCH (SET scripts) | 16 parallel instances, each calling a bulk endpoint; replies land in CREDORA_STAGE |
| Python UDFs: ML and AI | TRAIN_PD, PD_SCORE, CREDIT_MEMO | Train, score and explain; write a credit memo with Claude |
| Lua scripts | CREDORA.RUN_ANALYSIS, CREDORA.TRAIN_MODEL, CREDORA.PURGE_EXPIRED | The whole run in one call, logged per step; versioned training; purge with proof |
| CONNECTION objects | ORA_CBS, MYSQL_CIF, PG_LOS, MOCK_CIBIL, MOCK_GSTN, MOCK_AA, BUCKETFS_MODELS, ADBC_VS_BUCKETFS, AI_LLM (only with an API key) | Addresses and secrets never appear in SQL text; access is granted per role |
| Staging schema | CREDORA_STAGE.CIBIL_CONSUMER, CIBIL_COMMERCIAL, GST_FILING, AA_ACCOUNT (each with PURGE_AT), CA_MONTHLY | Raw API data and monthly account totals from Oracle |
| Results schema | CREDORA.CONFIG, APPLICATIONS, FEATURES, MODEL_REGISTRY, SCORES, DECISIONS, RUN_LOG, PURGE_LOG, MEMOS, view V_COMPANY_MASKED | Policy, results, history, proof |
| Analytic SQL | Window functions, CTEs, pushdown aggregation, PREFERRING | Features, pricing, multi-criteria ranking |
| Security | Roles R_SVC, R_ADMIN, R_DATASCI, R_LOANOPS, R_RM, R_MCP, one CREDORA_* user each | The database enforces who sees what |
TRAIN_MODEL → TRAIN_PD), compares it and activates it.EXECUTE SCRIPT CREDORA.RUN_ANALYSIS('<run>') as CREDORA_SVC; the 11 steps below take about 20 seconds.CREDIT_MEMO writes a short memo for each of the top 50.crm records each stage.PURGE_EXPIRED deletes raw API data past its purge time; the row count and SHA-256 fingerprint are kept in PURGE_LOG as proof.RUN_ANALYSIS, as measured on the live deployment| # | Step (logged in RUN_LOG) | How | Rows | Time |
|---|---|---|---|---|
| 1 | Refresh virtual schemas | VS_CBS, VS_CIF, VS_LOS | 3 | 1.7 s |
| 2 | Read applications | PostgreSQL joined with MySQL, in place | 1,000 | 0.3 s |
| 3 | CIBIL consumer reports | CIBIL_CONSUMER_FETCH, 16 parallel | 3,490 | 0.7 s |
| 4 | CIBIL commercial reports | CIBIL_COMMERCIAL_FETCH | 1,000 | 0.4 s |
| 5 | GST returns | GST_FETCH | 71,217 | 1.4 s |
| 6 | Partners' accounts at other banks | AA_FETCH | 5,810 | 3.2 s |
| 7 | Compute 13 features | Oracle pushdown aggregation into CA_MONTHLY, then SQL | 1,000 | 10.4 s |
| 8 | Risk model | Uses the active model; trains v1 if none | 1 | – |
| 9 | Score probability of default | PD_SCORE, 4 parallel | 1,000 | 1.9 s |
| 10 | Decide offer and negotiation limit | SQL over CONFIG | 1,000 | 0.06 s |
| 11 | Rank with PREFERRING skyline | Layers until 50 are ranked | 83 | 0.02 s |
| Total | about 20 s |
The repo is the submission: web app, backend, mock APIs, generator, infrastructure, every Exasol script and UDF, and the ML code. Anyone with an AWS account fills in an env file and runs one command to recreate the whole system.
credora/ ├─ README.md what it is, logins, deploy ├─ .env.example every setting, no secrets ├─ Makefile setup · deploy · ship · exasol │ generate · status · destroy ├─ docker-compose.yml app host services ├─ deploy/ credora.py, exasol_setup.py, │ Caddyfile, remote/start.sh ├─ infra/ OpenTofu: private subnets, │ RDS x3, app host, S3, │ Secrets Manager, IAM, Route 53 ├─ exasol/ │ ├─ sql/ schemas, connections, │ │ virtual schemas, security │ ├─ udf/ API, ML and AI UDFs │ └─ lua/ RUN_ANALYSIS, TRAIN_MODEL, │ PURGE_EXPIRED ├─ db/ Oracle, MySQL, PostgreSQL │ and mockdb schemas ├─ mocks/ │ ├─ cibil/ gstn/ aa/ FastAPI, own DB each │ └─ generator/ mock data generator ├─ backend/ FastAPI ├─ frontend/ React + TypeScript (Vite) ├─ docs/ architecture, data model, │ APIs, why Exasol, porting └─ tests/
.env.<name> (copied from .env.example)AWS_PROFILE= # the target account AWS_REGION=ap-south-1 PROJECT=credora DOMAIN= # e.g. credora.loan EXTERNAL_DOMAIN= # e.g. credora.link (outside world) ADMIN_CIDR= # your IP/32; blank = detected EXASOL_INSTANCE=r6i.xlarge ORACLE_INSTANCE= # optional, db.t3.small APP_INSTANCE= # optional, t3.large APP_ADMIN_PASSWORD= # optional: all four or none APP_DATASCI_PASSWORD= APP_LOANOPS_PASSWORD= APP_RM_PASSWORD= AI_PROVIDER= # anthropic | bedrock | blank = off ANTHROPIC_API_KEY= AI_MODEL=claude-sonnet-5-5
python deploy/credora.py deploy --env <name> (or make deploy ENV=<name>) does everything.env.<name>; gets AWS credentials from the CLI profile.exasol launcher (init aws ubuntu, then deploy) creates the VPC and the Exasol EC2, after its ports are restricted to the deployer's IP.www..env.runtime from the secret and starts Docker Compose (Caddy, backend, generator, mock APIs, mockdb).RUST; installs adbc-vs-rs (adapter and scan UDF), the Oracle virtual schema jar and ojdbc17.AI_LLM if a key is set, then the three virtual schemas, grants and the six users.https://<DOMAIN>), the logins, the Exasol address and each container's status.| Deployment | AWS account | Domain | Command |
|---|---|---|---|
| Live | Anamika's AWS account | credora.loan (bank), credora.link (outside world) | make deploy ENV=credora |
DOMAIN; Caddy, Route 53, the app and the API all read it.infra/state/<name>/) and its own secrets in its own account.docs/PORTING.md.