A full-stack financial fraud detection web application that combines an XGBoost machine learning model with a rule-based filter and uses a local LLM (Ollama) to generate human-readable explanations for each prediction.
- Hybrid fraud detection — XGBoost classifier trained on the PaySim dataset, augmented with a precision-optimized rule filter for high-confidence cases
- LLM-generated explanations — every classification comes with a natural-language explanation (Ollama /
llama3.1:8b) referencing the specific transaction features that drove the decision - Browser geolocation — uses the user's actual location (with permission) instead of a hardcoded fallback
- Cross-country risk boost — international transfers automatically gain +40% fraud probability
- Four-tier decision system —
CONFIRMED_FRAUD,HIGH_RISK,NEEDS_REVIEW,LEGITIMATE - Interactive dashboard — recent transactions, statistics, location analysis with map, full transaction history with filters
- Reverse geocoding — Nominatim (OpenStreetMap) translates coordinates to city/region/country, no API key required
Backend
- Python 3, Flask, flask-cors
- XGBoost (model) + scikit-learn (preprocessing)
- SQLite (transaction storage)
- IPInfo (IP-based geolocation fallback)
- Nominatim / OpenStreetMap (reverse geocoding)
- Ollama (local LLM for explanations)
Frontend
- React 18, React Router 6
- Tailwind CSS
- Axios (API client)
- Recharts (charts)
- Leaflet (map)
Model training
- Pandas, NumPy
- XGBoost classifier with
aucprevaluation imblearn.RandomUnderSamplerfor class balancing
Browser (React SPA)
│
│ HTTPS / JSON (Axios)
▼
Flask REST API ───► XGBoost model (fraud_model.pkl)
│ │
│ ▼
│ Rule filter
│ + cross-country boost
│
├──► SQLite (transaction history)
├──► Nominatim API (reverse geocoding from coords)
├──► IPInfo API (fallback IP geolocation)
└──► Ollama (local) (explanation generation)
FraudGen/
├── Model.ipynb # XGBoost training notebook
├── fraudgen/ # Flask backend
│ ├── app.py # Main Flask application
│ ├── reprocess_transactions.py # Re-score existing DB rows with current model
│ ├── fraud_model.pkl # Trained XGBoost model (generated by notebook)
│ ├── preprocess_info.pkl # Feature names + thresholds (generated)
│ ├── fraud_detection.db # SQLite DB (auto-created)
│ ├── requirements.txt
│ └── Procfile # gunicorn config for Render
└── fraudgen-client/ # React frontend
├── src/
│ ├── App.js
│ ├── api.js # Axios instance
│ ├── components/
│ │ ├── Header.jsx
│ │ ├── Navigation.jsx
│ │ ├── Dashboard.jsx
│ │ ├── TransactionForm.jsx
│ │ ├── TransactionHistory.jsx
│ │ ├── Statistics.jsx
│ │ ├── LocationDashboard.jsx
│ │ ├── LocationMap.jsx
│ │ └── CountryCombobox.jsx
│ ├── constants/countries.js # ISO 3166-1 country list (249 entries)
│ └── fraudgen.png # Logo
└── package.json
- Python 3.10+
- Node.js 18+
- Ollama running locally with
llama3.1:8bpulled - The PaySim dataset (
PS_20174392719_1491204439457_log.csv) — available on Kaggle
The model files (fraud_model.pkl, preprocess_info.pkl) are not committed. Generate them by running the notebook:
# Place the PaySim CSV at the project root
cp /path/to/PS_20174392719_1491204439457_log.csv ./
# Execute the notebook (saves models into fraudgen/)
jupyter nbconvert --to notebook --execute --ExecutePreprocessor.kernel_name=python3 Model.ipynbcd fraudgen
pip install -r requirements.txt
python app.py
# → Running on http://localhost:5050Optional environment variables:
OLLAMA_BASE_URL(defaulthttp://localhost:11434)OLLAMA_MODEL(defaultllama3.1:8b)IPINFO_TOKEN(for IP-based geolocation fallback)
cd fraudgen-client
npm install
echo "REACT_APP_API_BASE_URL=http://localhost:5050" > .env.local
npm start
# → Running on http://localhost:3000ollama pull llama3.1:8b
ollama serve # if not already running| Method | Path | Description |
|---|---|---|
| GET | / |
Health check, returns ML-model load status |
| GET | /api/test-transaction |
Returns a random sample transaction |
| POST | /api/predict |
Score a transaction; returns decision + LLM explanation |
| GET | /api/transactions |
Paginated history; supports prediction, country filters |
| DELETE | /api/transactions/<id> |
Delete a transaction |
| GET | /api/statistics |
Aggregate stats (counts, fraud rates, trends) |
| GET | /api/statistics/locations |
Per-country and VPN/proxy fraud breakdowns |
{
"type": "TRANSFER",
"amount": 85000,
"oldbalanceOrg": 100000,
"newbalanceOrig": 15000,
"oldbalanceDest": 5000,
"newbalanceDest": 90000,
"receiver_country": "CA",
"step": 132,
"user_latitude": 40.7128,
"user_longitude": -74.0060
}{
"decision": "⚠️ HIGH_RISK",
"probability": 0.85,
"action": "block_with_review",
"explanation": "This transaction appears ⚠️ HIGH_RISK with 85.0% probability...",
"location": { "country": "US", "region": "New York", "city": "New York" }
}| Probability | Decision | Action |
|---|---|---|
| Rule-filter match | 🚨 CONFIRMED_FRAUD |
block_and_alert |
| ≥ 90% | 🚨 CONFIRMED_FRAUD |
block_and_alert |
| 70% – 89% | HIGH_RISK |
block_with_review |
| 40% – 69% | 🕵️ NEEDS_REVIEW |
manual_review |
| < 40% | ✅ LEGITIMATE |
allow |
The rule-based filter flags as CONFIRMED_FRAUD when all of these hold:
- TRANSFER type
- Amount in top 0.05% of training data
- Sender's balance fully drained
- Night-time hours (step % 24 < 6)
- Amount > 3× population median
A cross-country transfer (receiver_country ≠ sender's country) adds +0.4 to the model's probability before tier mapping.
- Dataset: PaySim — 6.36M synthetic mobile-money transactions, 8,213 frauds (~0.13%)
- Features (9 total):
step,type(encoded),amount,oldbalanceOrg,newbalanceOrig,oldbalanceDest,newbalanceDest,balance_change_orig,balance_change_dest - Class balancing:
RandomUnderSampleron the training split - Model: XGBoost (
max_depth=5,learning_rate=0.1,aucpreval, early stopping) - Test-set metrics: Recall ~0.97, ROC-AUC ~0.999, Average Precision ~0.88
If you retrain the model or change the rules, you can re-score every record in the database (preserving timestamps):
cd fraudgen
python reprocess_transactions.pyEach row is re-scored with the current model and gets a fresh Ollama-generated explanation.