ExaThon India 2026 · Final idea · architecture and steps

Credora for Sapphire Bank

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.

All data is synthetic, created by the built-in mock data generator.

Diagrams

Four architecture diagrams, each one page with export to PNG or PDF (all diagrams):

  1. Big picture: the bank's databases and the outside-world relays, Exasol's layers, the web app, the four roles and the CRM.
  2. AWS deployment: region, VPC, subnets, security groups, both domains on Route 53, and the ports and protocols on every path.
  3. Inside Exasol: runtimes and BucketFS, virtual schemas, API and ML UDFs, Lua, results, roles, the preprocessor and the semantic layer.
  4. Analysis flow: the 11 steps of RUN_ANALYSIS with source, rows and timing, ending in the PREFERRING top 50 and the CRM export.

Architecture

Sapphire Bank data (Oracle, MySQL, PostgreSQL) Mock external APIs Exasol Results Mock data generator

Latest run: 545 approve, 168 refer, 287 decline. The backend writes each offer and every negotiation step to the bank's PostgreSQL crm database.

Mock data generation

Everything starts with data. The admin screen has a Generate new mock data button. It first asks for confirmation:

Generate new mock data? Generating new mock data will clear all the existing data.
CancelClear and generate

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.

WhatHow muchStored inSeen as
Companies (corporate current-account customers), relationship managers1,000MySQL cif.company, cif.rmSapphire Bank · MySQL
Partners (directors, partners, promoters)2–5 per company, about 3,500MySQL cif.partner, view partner_profileSapphire Bank · MySQL
Company current account and its transactions12–24 months each, about 850,000 rowsOracle CBS.ACCOUNT, CBS.TXNSapphire Bank · Oracle
Partners' savings accounts at Sapphire Bankabout 30% of partnersOracle CBS.ACCOUNTSapphire Bank · Oracle
Loan application (product, amount, tenure, purpose, collateral)1 per companyPostgreSQL los.applicationSapphire Bank · PostgreSQL
Past loans with outcomes (about 8% defaulted)5,000PostgreSQL los.past_loan, view past_loan_trainingSapphire Bank · PostgreSQL, used to train the ML model
Offers and negotiation trailempty until the first analysisPostgreSQL crm.offer, crm.offer_eventSapphire Bank · PostgreSQL
CIBIL consumer report per partner1 per partnerMock CIBIL databaseAPI · CIBIL (mock)
CIBIL commercial report per company (CMR rank)1 per companyMock CIBIL databaseAPI · CIBIL (mock)
GST registration and monthly returnslast 2–10 years, randomMock GSTN databaseAPI · 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 transactionsMock AA databaseAPI · Account Aggregator (mock)
Hidden true risk per company1 per companymockdb truth and S3Admin scorecard only; Exasol never reads it

GST history sets the company's stage

Years of GSTStage
2–4New
5–7Mid-level
8–10Stable

Per month: GSTR-1 turnover, GSTR-3B tax paid, due date and filing date (unfiled returns are empty). The GSTIN registration date matches the history.

A CIBIL report, as it usually looks

  • Partner (consumer): name, PAN, date of birth, score 300–900 (or no history), accounts (lender, type, sanctioned amount, balance, overdue, 36-month DPD history), enquiries in the last 6 and 12 months.
  • Company (commercial): CMR rank 1–10, total exposure, overdue, number of facilities, suit-filed and wilful-default flags.
  • A hidden "true risk" per company drives the data consistently: risky companies file GST late, bounce more, show a bigger gap between GST turnover and bank credits, and have partners with lower scores.

Data explorer: see all the data, and where it came from

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.

Sapphire Bank · Oracle / MySQL / PostgreSQL our bank's own data API · CIBIL / GSTN / AA (mock) fetched from outside Exasol result computed by Exasol
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

The same tags appear in the source cards on the admin dashboard, which show row counts, the last generation and the health of each database and API.

Roles: who sees what

RoleSapphire Bank dataExternal API dataExasol report (why, how)Can do
AdminAllAllYesGenerate new mock data, run analysis, source health, audit, purge proof
Data scientistAllAllYesTrain a model inside Exasol, compare and activate versions, what-if on policy (hurdle, EBLR, GST share)
Loan operationsAllNo raw replies; the features derived from themYesRun analysis, open any company's report, verify and sanction (maker-checker)
Relationship managerMasked company view (PAN, GSTIN masked)NoOffer, limit and reasonsTop 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 userRoleCan read or do
CREDORA_SVCR_SVCRuns analysis, training and purge; writes CREDORA and CREDORA_STAGE; uses the API, BucketFS and AI connections
CREDORA_ADMIN, CREDORA_DATASCIR_ADMIN, R_DATASCIEverything, read-only: CREDORA, CREDORA_STAGE, VS_CBS, VS_CIF, VS_LOS
CREDORA_LOANOPSR_LOANOPSCREDORA and the three virtual schemas; not CREDORA_STAGE (raw CIBIL, GST, AA data)
CREDORA_RMR_RMDECISIONS, SCORES, CONFIG, RUN_LOG and V_COMPANY_MASKED
CREDORA_MCPR_MCPRead-only results for Ask Credora: DECISIONS, SCORES, FEATURES, RUN_LOG, MODEL_REGISTRY, CONFIG, V_COMPANY_MASKED

ML and AI inside Exasol

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.

ObjectKindJob in Credora
TRAIN_PD (SET UDF), run by Lua TRAIN_MODELscikit-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 BucketFSProbability 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 configuredA 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 providerPlain-English questions about the results, answered only from what that role may read

How one company is decided

Policy values live in CREDORA.CONFIG, so the data scientist can change them in a what-if: EBLR 8.25%, operating spread 0.75%, cost of funds 6.50%, opex 1.00%, LGD 45%, capital 12%, hurdle RAROC 15%.

StepWhat happensIn Exasol
1 · CombineApplication (PostgreSQL) joined with company and partners (MySQL), in placeCREDORA.APPLICATIONS
2 · Features13 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, DSCRWindow functions over CA_MONTHLY (aggregated in Oracle) and the API data → FEATURES
3 · RiskProbability of default, grade, per-feature attributionPD_SCORE → SCORES
4 · Hard failsWilful defaulter, CMR rank 9 or worse, 4+ late GST returns in 12 months, any partner CIBIL below 600, bounce ratio above 8%SQL
5 · ROIEBLR + 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 floorSQL
6 · AmountTerm 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 · TenureTerm loan: as asked, up to 84 months (60 for grades C1, C2, D). Working capital: 12 months.SQL
8 · Negotiation limitFloor 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 84SQL
9 · DecisionDecline 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 · RankSkyline layers of approved companies, rank among 1,000, top 50PREFERRING

The Exasol report for one company (synthetic)

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 · AA

Deployment on AWS

AWS architecture-diagram style: AWS Cloud, then Region, then VPC, then subnets, then security groups, with service icons in AWS category colours. The icons are simplified redraws, not the official icon files. Numbered arrows are explained below the diagram.

  1. Users → Route 53: the domain root and www resolve to the app host's Elastic IP.
  2. Users → app host: HTTPS through the internet gateway to Caddy, which serves the React app at the domain root, sends /api to the backend, and redirects www and HTTP to https://<DOMAIN>.
  3. Backend → Exasol: SQL on :8563 over the VPC, logged in as the Exasol user of the signed-in role. Ask Credora queries through the Exasol MCP server as CREDORA_MCP.
  4. Exasol → mock APIs: Python UDFs call CIBIL, GSTN and AA on the app host's private IP, ports 8101–8103 (open to the VPC only).
  5. Exasol → RDS: the virtual schemas read Oracle (:1521), MySQL (:3306) and PostgreSQL (:5432) in place.
  6. Generator → RDS (dashed): writes the bank data into Oracle, MySQL and PostgreSQL, and the CIBIL, GST and AA data into mockdb.
  7. Backend → PostgreSQL crm: offers, negotiation steps and sanction (small, frequent writes).
  8. Generator → S3 (dashed): the hidden truth per company, for the admin scorecard.
  9. Deployer → Exasol and app host (dashed): Exasol ports 22, 2581, 8443 and 8563, and SSH to the app host, only from the deployer's IP.

Not drawn as arrows: on start, the app host reads the runtime secret from Secrets Manager with its instance role and writes it to .env.runtime for the containers. RDS is not publicly accessible; its subnet group spans two zones.

Exasol objects

Exasol featureObjectJob
Virtual schema: OracleVS_CBS on ADAPTER.ORACLE_ADAPTER (Oracle VS 6.0, Java, ojdbc17), connection ORA_CBSReads core banking (ACCOUNT, TXN) in place, with pushdown
Virtual schema: adbc-vs-rsVS_CIF (MySQL) and VS_LOS (PostgreSQL) on ADBC_VS.ADBC_ADAPTER + ADBC_VS.ADBC_SCAN, connections MYSQL_CIF, PG_LOSOne Rust adapter for two engines, moving Arrow record batches
Custom script language containerRUST: Rust SLC 0.23.1 in BucketFS, added to SCRIPT_LANGUAGESRuns the adbc-vs-rs adapter at the SDK version it was built for
BucketFSslc/rustslc, udf/libadbc_vs_adapter.so, ADBC drivers (downloaded on first use), Oracle VS jar, drivers/jdbc/ojdbc17.jar, models/pd_<version>.pklOne store for code, drivers and every model version
Python UDFs: external APIsCIBIL_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 AITRAIN_PD, PD_SCORE, CREDIT_MEMOTrain, score and explain; write a credit memo with Claude
Lua scriptsCREDORA.RUN_ANALYSIS, CREDORA.TRAIN_MODEL, CREDORA.PURGE_EXPIREDThe whole run in one call, logged per step; versioned training; purge with proof
CONNECTION objectsORA_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 schemaCREDORA_STAGE.CIBIL_CONSUMER, CIBIL_COMMERCIAL, GST_FILING, AA_ACCOUNT (each with PURGE_AT), CA_MONTHLYRaw API data and monthly account totals from Oracle
Results schemaCREDORA.CONFIG, APPLICATIONS, FEATURES, MODEL_REGISTRY, SCORES, DECISIONS, RUN_LOG, PURGE_LOG, MEMOS, view V_COMPANY_MASKEDPolicy, results, history, proof
Analytic SQLWindow functions, CTEs, pushdown aggregation, PREFERRINGFeatures, pricing, multi-criteria ranking
SecurityRoles R_SVC, R_ADMIN, R_DATASCI, R_LOANOPS, R_RM, R_MCP, one CREDORA_* user eachThe database enforces who sees what

Steps: how it works

  1. AdminClicks Generate new mock data and confirms "Generating new mock data will clear all the existing data".
  2. GeneratorTruncates every table, then loads 1,000 companies, about 3,500 partners, about 850,000 Oracle transactions, CIBIL reports for every partner and company, 2–10 years of GST, partners' other-bank accounts and 5,000 past loans. About 75 seconds.
  3. EveryoneCan browse the new data in the data explorer (as far as their role allows), with each item tagged by its source.
  4. Data scientistOptionally trains a new model version (TRAIN_MODEL → TRAIN_PD), compares it and activates it.
  5. Loan operationsClicks Run analysis. The backend runs EXECUTE SCRIPT CREDORA.RUN_ANALYSIS('<run>') as CREDORA_SVC; the 11 steps below take about 20 seconds.
  6. Loan operationsOpens the Exasol report for any company: decision, why (per-feature attribution), how (each step with its SQL, Lua or Python and its timing) and which source each number came from.
  7. Exasol · AIIf an API key is set, CREDIT_MEMO writes a short memo for each of the top 50.
  8. Relationship managerQuotes and proposes terms with the customer; Exasol checks them against the limit and RAROC. Above the limit the offer is escalated.
  9. Loan operationsVerifies and sanctions (maker-checker). PostgreSQL crm records each stage.
  10. Exasol · purgePURGE_EXPIRED deletes raw API data past its purge time; the row count and SHA-256 fingerprint are kept in PURGE_LOG as proof.

Inside RUN_ANALYSIS, as measured on the live deployment

#Step (logged in RUN_LOG)HowRowsTime
1Refresh virtual schemasVS_CBS, VS_CIF, VS_LOS31.7 s
2Read applicationsPostgreSQL joined with MySQL, in place1,0000.3 s
3CIBIL consumer reportsCIBIL_CONSUMER_FETCH, 16 parallel3,4900.7 s
4CIBIL commercial reportsCIBIL_COMMERCIAL_FETCH1,0000.4 s
5GST returnsGST_FETCH71,2171.4 s
6Partners' accounts at other banksAA_FETCH5,8103.2 s
7Compute 13 featuresOracle pushdown aggregation into CA_MONTHLY, then SQL1,00010.4 s
8Risk modelUses the active model; trains v1 if none1–
9Score probability of defaultPD_SCORE, 4 parallel1,0001.9 s
10Decide offer and negotiation limitSQL over CONFIG1,0000.06 s
11Rank with PREFERRING skylineLayers until 50 are ranked830.02 s
Totalabout 20 s

One repo, one command

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.

Repo layout

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

Only AWS_PROFILE and DOMAIN are required. The hosted zone is looked up from DOMAIN. Database, API-token and Exasol-user passwords are generated by OpenTofu into Secrets Manager. A filled copy (.env.credora) stays on the deployer's machine.

python deploy/credora.py deploy --env <name> (or make deploy ENV=<name>) does everything

  1. CheckReads .env.<name>; gets AWS credentials from the CLI profile.
  2. ExasolThe official exasol launcher (init aws ubuntu, then deploy) creates the VPC and the Exasol EC2, after its ports are restricted to the deployer's IP.
  3. AWS infraOpenTofu, run in Docker, adds two private subnets, RDS for Oracle, MySQL and PostgreSQL, the app host with its Elastic IP and IAM role, S3, the Secrets Manager runtime secret and the Route 53 A records for the domain root and www.
  4. ShipBuilds the React frontend, copies the repo to the app host, writes .env.runtime from the secret and starts Docker Compose (Caddy, backend, generator, mock APIs, mockdb).
  5. GenerateCalls the generator: 1,000 companies of mock data in the three bank databases and the mock APIs.
  6. ExtensionsUploads the Rust SLC to BucketFS and registers RUST; installs adbc-vs-rs (adapter and scan UDF), the Oracle virtual schema jar and ojdbc17.
  7. Exasol SQLCreates roles, schemas, connections, UDFs and Lua scripts, AI_LLM if a key is set, then the three virtual schemas, grants and the six users.
  8. First analysisLogs in to the API as admin and runs the analysis; it trains model v1 because none exists.
  9. DonePrints the site address (https://<DOMAIN>), the logins, the Exasol address and each container's status.

Safe to re-run. Parts can be run alone: ship, exasol, generate, status. destroy removes the OpenTofu resources and the Exasol deployment.

Portable to any AWS account and domain

DeploymentAWS accountDomainCommand
LiveAnamika's AWS accountcredora.loan (bank), credora.link (outside world)make deploy ENV=credora
  • No account IDs, zone IDs, ARNs, IPs, bucket names or domains in code. Everything comes from the env file or is looked up at deploy time.
  • The domain is set once, in DOMAIN; Caddy, Route 53, the app and the API all read it.
  • Each deployment has its own infrastructure state (infra/state/<name>/) and its own secrets in its own account.
  • Data is synthetic and regenerated, and the model is retrained, so moving needs no data copy.
  • Steps to move are in docs/PORTING.md.