UPI Payment App with ML Fraud Detection
Build a payment app with a Random Forest model that blocks fraud in real time.
Introduction
30 Second Summary
Every time you tap "Pay" on a payment app, a hidden system is checking whether your transaction looks legitimate. If something seems off, it gets blocked before the money moves.
In this project, you will build a UPI payment app with a machine learning model that detects and blocks fraudulent transactions in real-time. You will generate synthetic data, train a fraud detection model, and integrate it into a working web application.
What You'll Build
A full-stack payment simulator powered by a Random Forest classifier that scores every transaction for fraud risk before processing it.
By the end of this project, you'll have:
- A synthetic dataset of 10,000 UPI transactions with realistic fraud patterns baked in.
- A trained fraud detection model saved as a reusable file that your app loads at startup.
- A Flask web application with user wallets, a payment flow, and a transaction history dashboard showing approved and blocked payments.
- Secret Mission: a fraud analytics dashboard displaying model performance metrics and top fraud patterns.
Are there any prerequisites?
You need Python and pip installed on your computer. Basic Python knowledge (variables, functions, and imports) is helpful but not required.
Before We Start
Before building a fraud detection system, you need a development environment with the right tools installed.
In this step, you will create a project folder, set up a Python virtual environment, install the machine learning and web packages, and confirm everything works by running a minimal Flask app.
In this step, get ready to:
- Create a project folder and Python virtual environment.
- Install Flask, scikit-learn, pandas, numpy, and joblib.
- Create subdirectories and verify Flask runs locally.
Create your project folder and virtual environment
A virtual environment is an isolated Python workspace that keeps this project's packages separate from your system Python.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir upi-fraud-detection
cd upi-fraud-detection
- Confirm the folder exists by running:
ls
You should see an empty directory (no output). That means you are inside the new folder.
Now create and activate a virtual environment. The commands differ by operating system.
macOS
- Create and activate your virtual environment by running these commands:
python3 -m venv .venv
source .venv/bin/activate
Windows
- Create and activate your virtual environment by running these commands:
py -3 -m venv .venv
.venv\Scripts\activate
You should see (.venv) at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (.venv) in your prompt?
On Windows, you may see a permissions error. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.
On macOS, make sure you typed source before the path. Without it, the command will not work.
Still stuck? Help me activate my Python virtual environment
Install packages and create the folder structure
Your project needs five packages. Flask is the web framework. scikit-learn provides the machine learning model. pandas and numpy handle data processing. joblib saves and loads trained models from disk.
- Install all five packages by running this command:
pip install Flask==3.1.3 scikit-learn==1.8.0 pandas numpy joblib==1.5.3
What do the version numbers mean?
Pinning versions (like Flask==3.1.3) ensures everyone running this project uses the same package versions. This prevents unexpected bugs from newer releases.
The install may take 1-2 minutes. You should see Successfully installed followed by a list of packages when it finishes.
Seeing an installation error?
Make sure your virtual environment is active. You should see (.venv) in your prompt. If not, re-run the activate command from the previous substep.
If you see a pip not found error, try python -m pip install instead of just pip install.
Still stuck? Help me fix my pip install error
- Create the templates and static subdirectories by running:
mkdir templates static
What are these folders for?
Flask expects a specific folder structure. The templates/ folder holds your HTML pages. The static/ folder holds CSS, JavaScript, and images.
- Confirm the folders exist by running:
ls
You should see static and templates listed in the output.
Verify your setup with a test Flask app
The quickest way to confirm everything installed correctly is to run a minimal Flask app. This test file will be replaced by the real application later.
- Create a file called app.py in your upi-fraud-detection folder. Open it in any text editor and paste the following code:
from flask import Flask
# Create a minimal Flask app to verify our setup
app = Flask(__name__)
@app.route('/')
def home():
return 'UPI Fraud Detection System - Running'
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
This creates a tiny web server with one page. When you visit the home page, it responds with a confirmation message.
The debug=True setting makes Flask auto-reload when you change code. This is helpful during development.
- Save app.py.
- Run the app by executing this command in your terminal:
python app.py
You should see output showing Flask is running on http://127.0.0.1:5000.
- Open your browser and go to http://127.0.0.1:5000.
You should see the text UPI Fraud Detection System - Running displayed on the page.
- Go back to your terminal and press Ctrl+C to stop the server.
Don't see the running message?
If you see ModuleNotFoundError: No module named 'flask', your virtual environment is not active. Check that (.venv) appears in your prompt, then try running again.
If the browser shows This site can't be reached, make sure the Flask server is still running in your terminal. Do not close that terminal window.
Need more help? Help me get my Flask test app running
Your environment is ready. Next up, you will generate a synthetic dataset with realistic fraud patterns to train the machine learning model.
Generate Synthetic UPI Transaction Data
Your project environment is set up and ready to go. Now you need the data that your fraud detection model will learn from.
Every ML model needs training data. You'll write a Python script that generates 10,000 synthetic UPI transactions. About 5% of these will contain realistic fraud patterns, teaching your model exactly what suspicious activity looks like.
In this step, get ready to:
- Write a synthetic data generation script with realistic transaction features.
- Inject fraud patterns into 5% of transactions.
- Save the dataset as a CSV and verify the class distribution.
Create the legitimate transaction generator
Your fraud detection model needs to learn from examples of both normal and suspicious transactions. You'll generate this data with pandas and NumPy, two Python libraries designed for working with numbers and tabular data.
Why synthetic data instead of a real dataset?
Generating synthetic data gives you full control over what fraud patterns exist. This means you can verify your model catches exactly the scenarios you defined.
It also removes dependency on external downloads and makes the project fully self-contained for GitHub. You'll learn feature engineering in the process.
- Create a new file called generate_data.py inside your upi-fraud-detection folder.
- Add the imports, configuration, and the legitimate transaction generator by pasting the following code:
import pandas as pd
import numpy as np
np.random.seed(42)
n_transactions = 10000
fraud_ratio = 0.05
n_fraud = int(n_transactions * fraud_ratio)
n_legit = n_transactions - n_fraud
def generate_legit_transactions(n):
data = {
'amount': np.random.exponential(scale=500, size=n).clip(10, 5000),
'sender_balance': np.random.uniform(1000, 50000, size=n),
'receiver_balance': np.random.uniform(500, 50000, size=n),
'transaction_hour': np.random.choice(range(8, 22), size=n),
'transaction_frequency': np.random.poisson(lam=3, size=n),
'account_age_days': np.random.randint(30, 1825, size=n),
'is_fraud': np.zeros(n, dtype=int)
}
return pd.DataFrame(data)
What does this code do?
- np.random.seed(42) locks the random number generator so the same data is produced every time you run the script.
- The configuration block sets 10,000 total transactions with a 5% fraud ratio (500 fraudulent, 9,500 legitimate).
- generate_legit_transactions() creates normal-looking transactions: moderate amounts (exponential distribution clipped to 10-5000), healthy sender balances, daytime hours (8-22), low transaction frequency, and established accounts (30-1825 days old).
- Save generate_data.py.
- Confirm the file exists by running:
ls
- You should see generate_data.py listed alongside your templates/ and static/ directories.
Don't see generate_data.py?
- Make sure you created the file inside the upi-fraud-detection folder, not inside templates/ or static/.
- Check that your terminal is in the upi-fraud-detection directory. Run cd ~/Desktop/upi-fraud-detection if needed.
- Why isn't my generate_data.py file showing up?
Add the fraud transaction generator
The fraud generator creates transactions with the opposite patterns to legitimate ones. Fraudulent transactions tend to be large amounts from low-balance accounts, happen late at night, come from brand new accounts, and show high transaction frequency.
- In generate_data.py, add the fraud transaction generator below the generate_legit_transactions function:
def generate_fraud_transactions(n):
data = {
'amount': np.random.uniform(5000, 50000, size=n),
'sender_balance': np.random.uniform(100, 5000, size=n),
'receiver_balance': np.random.uniform(500, 50000, size=n),
'transaction_hour': np.random.choice([0, 1, 2, 3, 4, 23], size=n),
'transaction_frequency': np.random.poisson(lam=15, size=n),
'account_age_days': np.random.randint(1, 30, size=n),
'is_fraud': np.ones(n, dtype=int)
}
return pd.DataFrame(data)
What makes these transactions look fraudulent?
- Amounts range from 5,000 to 50,000 (much higher than legitimate transactions averaging around 500).
- Sender balances are low (100-5,000), meaning the transaction amount often exceeds what the account normally holds.
- Transaction hours are late night (0-4) or very late (23), times when legitimate users rarely transact.
- Frequency is high (average 15 per day vs 3 for legitimate) and accounts are brand new (1-30 days old).
- Save generate_data.py.
- You won't see a change yet. The next chunk adds the execution code that actually generates the dataset.
Combine datasets and engineer features
Now you'll add the main execution block that calls both generators, combines the results, and creates derived features. Feature engineering means creating new columns from existing data that make patterns more obvious to the model.
- Add the following code at the bottom of generate_data.py, below the generate_fraud_transactions function:
legit_df = generate_legit_transactions(n_legit)
fraud_df = generate_fraud_transactions(n_fraud)
dataset = pd.concat([legit_df, fraud_df], ignore_index=True)
dataset = dataset.sample(frac=1, random_state=42).reset_index(drop=True)
dataset['amount_to_balance_ratio'] = dataset['amount'] / (dataset['sender_balance'] + 1)
dataset['is_new_account'] = (dataset['account_age_days'] < 30).astype(int)
dataset['is_night_transaction'] = dataset['transaction_hour'].apply(
lambda h: 1 if h < 6 or h > 22 else 0
)
dataset.to_csv('upi_transactions.csv', index=False)
print("Dataset generated successfully!")
print(f"Total transactions: {len(dataset)}")
print(f"Legitimate: {len(dataset[dataset['is_fraud'] == 0])}")
print(f"Fraudulent: {len(dataset[dataset['is_fraud'] == 1])}")
print(f"Fraud ratio: {dataset['is_fraud'].mean():.2%}")
What does this code do?
- pd.concat() merges the legitimate and fraud DataFrames into one dataset. .sample(frac=1) shuffles the rows so fraud and legitimate transactions are mixed randomly.
- amount_to_balance_ratio highlights when a transaction is disproportionately large relative to the sender's balance. A ratio of 10 means the amount is 10x the sender's balance.
- is_new_account and is_night_transaction convert continuous values into binary flags that the model can use as clear signals.
- The dataset is saved as a CSV file so the training script can load it without regenerating the data.
- Save generate_data.py.
- Run the script to generate your dataset:
python generate_data.py
- You should see output like this in your terminal:
Dataset generated successfully!
Total transactions: 10000
Legitimate: 9500
Fraudulent: 500
Fraud ratio: 5.00%
- Confirm the CSV file was created by running:
ls
- You should see upi_transactions.csv alongside generate_data.py in the output.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is activated. You should see (.venv) at the start of your terminal prompt.
- If the environment is not active, run .venv\Scripts\activate to reactivate it.
- If pandas or numpy is missing, run pip install pandas numpy inside the activated environment.
- Help me fix a ModuleNotFoundError when running generate_data.py
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete generate_data.py file for comparison:
import pandas as pd
import numpy as np
np.random.seed(42)
n_transactions = 10000
fraud_ratio = 0.05
n_fraud = int(n_transactions * fraud_ratio)
n_legit = n_transactions - n_fraud
def generate_legit_transactions(n):
data = {
'amount': np.random.exponential(scale=500, size=n).clip(10, 5000),
'sender_balance': np.random.uniform(1000, 50000, size=n),
'receiver_balance': np.random.uniform(500, 50000, size=n),
'transaction_hour': np.random.choice(range(8, 22), size=n),
'transaction_frequency': np.random.poisson(lam=3, size=n),
'account_age_days': np.random.randint(30, 1825, size=n),
'is_fraud': np.zeros(n, dtype=int)
}
return pd.DataFrame(data)
def generate_fraud_transactions(n):
data = {
'amount': np.random.uniform(5000, 50000, size=n),
'sender_balance': np.random.uniform(100, 5000, size=n),
'receiver_balance': np.random.uniform(500, 50000, size=n),
'transaction_hour': np.random.choice([0, 1, 2, 3, 4, 23], size=n),
'transaction_frequency': np.random.poisson(lam=15, size=n),
'account_age_days': np.random.randint(1, 30, size=n),
'is_fraud': np.ones(n, dtype=int)
}
return pd.DataFrame(data)
legit_df = generate_legit_transactions(n_legit)
fraud_df = generate_fraud_transactions(n_fraud)
dataset = pd.concat([legit_df, fraud_df], ignore_index=True)
dataset = dataset.sample(frac=1, random_state=42).reset_index(drop=True)
dataset['amount_to_balance_ratio'] = dataset['amount'] / (dataset['sender_balance'] + 1)
dataset['is_new_account'] = (dataset['account_age_days'] < 30).astype(int)
dataset['is_night_transaction'] = dataset['transaction_hour'].apply(
lambda h: 1 if h < 6 or h > 22 else 0
)
dataset.to_csv('upi_transactions.csv', index=False)
print("Dataset generated successfully!")
print(f"Total transactions: {len(dataset)}")
print(f"Legitimate: {len(dataset[dataset['is_fraud'] == 0])}")
print(f"Fraudulent: {len(dataset[dataset['is_fraud'] == 1])}")
print(f"Fraud ratio: {dataset['is_fraud'].mean():.2%}")
Your dataset is ready with 10,000 transactions and clear fraud signals baked in. Next up, you'll train a Random Forest model to learn these patterns and predict fraud on new transactions.
Train and Save the Fraud Detection Model
You have 10,000 synthetic UPI transactions sitting in a CSV file. Now you need to teach a machine learning model to spot the fraudulent ones.
In this step, you will train a Random Forest classifier on your dataset and save the trained model to a file. This saved model is what your Flask app will load later to score transactions in real-time.
In this step, get ready to:
- Load the dataset and split it into training and testing sets.
- Train a Random Forest model that handles imbalanced fraud data.
- Save the trained model as a .pkl file for your Flask app to use.
Load and split the dataset
Before training a model, you need to separate your data into features (the inputs the model learns from) and labels (the answer it tries to predict). You also split the data so you can test the model on transactions it has never seen.
- Create a new file called train_model.py in your upi-fraud-detection folder by running:
New-Item train_model.py
- Open train_model.py and add the following code to load your dataset and split it:
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import joblib
# Load the generated transaction dataset
dataset = pd.read_csv('upi_transactions.csv')
# Define which columns the model will learn from
feature_columns = [
'amount', 'sender_balance', 'receiver_balance',
'transaction_hour', 'transaction_frequency', 'account_age_days',
'amount_to_balance_ratio', 'is_new_account', 'is_night_transaction'
]
# Separate features (X) from the label (y)
X = dataset[feature_columns]
y = dataset['is_fraud']
# Split into 80% training and 20% testing data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
What does this code do?
- feature_columns lists the 9 inputs the model will learn from. The is_fraud column is excluded because that is what the model predicts.
- train_test_split reserves 20% of the data for testing so you can measure accuracy on unseen transactions.
- stratify=y ensures the 5% fraud ratio is preserved in both the training and test sets. Without this, the test set might end up with zero fraud cases by chance.
- Save train_model.py.
Train, evaluate, and save the model
Now you will create the Random Forest classifier, train it on the data, print a performance report, and save the trained model to disk so your Flask app can load it later.
- In train_model.py, add the following code below the train_test_split block:
# Create the Random Forest model with balanced class weights
model = RandomForestClassifier(
n_estimators=100,
max_depth=10,
class_weight='balanced',
random_state=42
)
# Train the model on the training data
model.fit(X_train, y_train)
# Evaluate on the test set
y_pred = model.predict(X_test)
print("Model Performance:")
print("=" * 50)
print(classification_report(y_test, y_pred, target_names=['Legitimate', 'Fraudulent']))
# Show which features matter most for detecting fraud
feature_importance = pd.DataFrame({
'feature': feature_columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nFeature Importance:")
print("=" * 50)
for _, row in feature_importance.iterrows():
print(f" {row['feature']}: {row['importance']:.4f}")
# Save the trained model so Flask can load it without retraining
joblib.dump(model, 'fraud_model.pkl')
print("\nModel saved as 'fraud_model.pkl'")
What does this code do?
- RandomForestClassifier creates an ensemble of 100 decision trees. Each tree votes on whether a transaction is fraud, and the majority wins.
- class_weight='balanced' tells the model to pay extra attention to fraud cases. Without this, the model would just predict "legitimate" for everything and get 95% accuracy while catching zero fraud.
- classification_report prints precision, recall, and F1-score for both classes so you can see how well the model detects fraud specifically.
- joblib.dump() serializes the trained model to a .pkl file. Your Flask app will call joblib.load() to load this model without needing to retrain every time the server starts.
Why use class_weight='balanced'?
Your dataset has 9,500 legitimate transactions and only 500 fraudulent ones. A lazy model could predict "legitimate" every single time and still be 95% accurate. That is useless for fraud detection.
The balanced setting automatically increases the penalty for misclassifying fraud cases, forcing the model to actually learn fraud patterns instead of ignoring them.
- Save train_model.py.
Run the training script
Your training script is ready. Running it will train the model, print performance metrics to the terminal, and save the trained model as a file.
- Make sure your virtual environment is still active (you should see (.venv) in your terminal prompt).
- Run the training script:
python train_model.py
You should see a classification report printed to your terminal showing precision, recall, and F1-score for both Legitimate and Fraudulent classes. Below that, you will see a ranked list of feature importances and a confirmation that the model was saved.
- Confirm the model file was created by running:
ls fraud_model.pkl
- You should see fraud_model.pkl listed in the output. This is your trained model ready for the Flask app to load.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is active. Run .venv\Scripts\activate and try again.
- If you see No module named 'sklearn', reinstall scikit-learn inside your venv:
pip install scikit-learn==1.8.0
If you see a FileNotFoundError for upi_transactions.csv, make sure you are running the command from your upi-fraud-detection folder where the CSV lives.
Still stuck? Help me debug why my train_model.py script is failing.
What should I see?
Your terminal output should include a classification report table with columns for precision, recall, f1-score, and support. The Fraudulent class should show recall above 0.90, meaning the model catches more than 90% of fraud cases.
The feature importance list shows which inputs matter most for detection. You should see amount and amount_to_balance_ratio near the top, which makes sense because fraudulent transactions tend to be unusually large relative to the sender's balance.
✔️ Awesome, I've got everything!
Great. Double check you have saved train_model.py and that fraud_model.pkl exists in your project folder.
ⓧ I'd like to double check the full code
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import joblib
# Load the generated transaction dataset
dataset = pd.read_csv('upi_transactions.csv')
# Define which columns the model will learn from
feature_columns = [
'amount', 'sender_balance', 'receiver_balance',
'transaction_hour', 'transaction_frequency', 'account_age_days',
'amount_to_balance_ratio', 'is_new_account', 'is_night_transaction'
]
# Separate features (X) from the label (y)
X = dataset[feature_columns]
y = dataset['is_fraud']
# Split into 80% training and 20% testing data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Create the Random Forest model with balanced class weights
model = RandomForestClassifier(
n_estimators=100,
max_depth=10,
class_weight='balanced',
random_state=42
)
# Train the model on the training data
model.fit(X_train, y_train)
# Evaluate on the test set
y_pred = model.predict(X_test)
print("Model Performance:")
print("=" * 50)
print(classification_report(y_test, y_pred, target_names=['Legitimate', 'Fraudulent']))
# Show which features matter most for detecting fraud
feature_importance = pd.DataFrame({
'feature': feature_columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nFeature Importance:")
print("=" * 50)
for _, row in feature_importance.iterrows():
print(f" {row['feature']}: {row['importance']:.4f}")
# Save the trained model so Flask can load it without retraining
joblib.dump(model, 'fraud_model.pkl')
print("\nModel saved as 'fraud_model.pkl'")
Your fraud detection model is trained and saved. Next up, you will build the Flask web application that lets users send payments through a UPI-style interface.
Build the UPI Payment Web Interface
You have a trained fraud detection model sitting in your project folder. Now you need a web application where users can actually send payments.
In this step, you will build a Flask web application with user wallets, a payment form, and a transaction history page. The app will look and feel like a real UPI payment interface.
In this step, get ready to:
- Create the Flask application with routes for the dashboard, payment form, and transaction history.
- Build HTML templates with a clean UPI-style payment interface.
- Add CSS styling and pre-seeded user accounts so the app works immediately.
Create the Flask application
The Flask app needs helper functions to manage user data and transaction records. It also needs routes that tell Flask which page to show for each URL.
- Create a new file called app.py in your upi-fraud-detection folder.
- Add the imports, Flask setup, and helper functions by pasting this code:
import json
import os
from flask import Flask, render_template, request, redirect, url_for, flash
import joblib
import numpy as np
app = Flask(__name__)
app.secret_key = 'upi-fraud-detection-secret-key'
# File paths for storing user and transaction data
USERS_FILE = 'users.json'
TRANSACTIONS_FILE = 'transactions.json'
def load_users():
# Load users from JSON file, or create default accounts on first run
if os.path.exists(USERS_FILE):
with open(USERS_FILE, 'r') as f:
return json.load(f)
users = {
"rahul@upi": {"name": "Rahul Sharma", "balance": 25000.0},
"priya@upi": {"name": "Priya Patel", "balance": 18000.0},
"amit@upi": {"name": "Amit Kumar", "balance": 32000.0},
"sneha@upi": {"name": "Sneha Reddy", "balance": 15000.0},
"vikram@upi": {"name": "Vikram Singh", "balance": 40000.0}
}
save_users(users)
return users
def save_users(users):
# Write updated user data back to JSON
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)
def load_transactions():
# Load transaction history from JSON file
if os.path.exists(TRANSACTIONS_FILE):
with open(TRANSACTIONS_FILE, 'r') as f:
return json.load(f)
return []
def save_transaction(transaction):
# Append a new transaction to the history file
transactions = load_transactions()
transactions.append(transaction)
with open(TRANSACTIONS_FILE, 'w') as f:
json.dump(transactions, f, indent=2)
What does this code do?
This sets up the Flask app and creates four helper functions for managing data stored in JSON files.
- load_users() checks if a users.json file exists. If not, it creates one with 5 pre-seeded UPI accounts and their starting balances.
- save_users() writes updated balances back to the file after a payment.
- load_transactions() and save_transaction() keep a running log of every payment attempt in transactions.json.
- The joblib and numpy imports are not used yet. You will use them in the next step when you wire in the fraud detection model.
- Now add the route definitions below the helper functions in the same file:
# Dashboard route - shows user wallets and recent transactions
@app.route('/')
def dashboard():
users = load_users()
transactions = load_transactions()
recent_transactions = transactions[-10:] if transactions else []
recent_transactions.reverse()
return render_template('dashboard.html', users=users, transactions=recent_transactions)
# Send payment page - displays the payment form
@app.route('/send', methods=['GET'])
def send_payment():
users = load_users()
return render_template('send.html', users=users)
# Transaction history page - shows all past transactions
@app.route('/history')
def history():
transactions = load_transactions()
transactions.reverse()
return render_template('history.html', transactions=transactions)
if __name__ == '__main__':
app.run(debug=True)
What do these routes do?
- The / route is the dashboard. It loads all users and the 10 most recent transactions, then passes them to the template.
- The /send route shows the payment form with a dropdown of available users. It only handles GET requests for now. The POST handler comes in the next step.
- The /history route displays all transactions in reverse chronological order.
- debug=True enables auto-reload so Flask restarts whenever you save a file.
- Save app.py.
- Verify Flask starts by running this command:
python app.py
You should see output like this in your terminal:
* Running on http://127.0.0.1:5000
* Debug mode: on
- Stop the server by pressing Ctrl+C in your terminal.
Don't visit the page yet
The Flask server started, but visiting the URL would show a TemplateNotFound error. You need to create the HTML templates first.
Seeing ModuleNotFoundError?
Make sure your virtual environment is activated. You should see (.venv) at the start of your terminal prompt.
- Run .venv\Scripts\activate (Windows) or source .venv/bin/activate (macOS/Linux) to reactivate it.
Help me fix a ModuleNotFoundError when running my Flask app.
✔️ Awesome, I've got everything!
Great. Make sure you have saved app.py before continuing.
ⓧ I'd like to double check the full code
import json
import os
from flask import Flask, render_template, request, redirect, url_for, flash
import joblib
import numpy as np
app = Flask(__name__)
app.secret_key = 'upi-fraud-detection-secret-key'
USERS_FILE = 'users.json'
TRANSACTIONS_FILE = 'transactions.json'
def load_users():
if os.path.exists(USERS_FILE):
with open(USERS_FILE, 'r') as f:
return json.load(f)
users = {
"rahul@upi": {"name": "Rahul Sharma", "balance": 25000.0},
"priya@upi": {"name": "Priya Patel", "balance": 18000.0},
"amit@upi": {"name": "Amit Kumar", "balance": 32000.0},
"sneha@upi": {"name": "Sneha Reddy", "balance": 15000.0},
"vikram@upi": {"name": "Vikram Singh", "balance": 40000.0}
}
save_users(users)
return users
def save_users(users):
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)
def load_transactions():
if os.path.exists(TRANSACTIONS_FILE):
with open(TRANSACTIONS_FILE, 'r') as f:
return json.load(f)
return []
def save_transaction(transaction):
transactions = load_transactions()
transactions.append(transaction)
with open(TRANSACTIONS_FILE, 'w') as f:
json.dump(transactions, f, indent=2)
@app.route('/')
def dashboard():
users = load_users()
transactions = load_transactions()
recent_transactions = transactions[-10:] if transactions else []
recent_transactions.reverse()
return render_template('dashboard.html', users=users, transactions=recent_transactions)
@app.route('/send', methods=['GET'])
def send_payment():
users = load_users()
return render_template('send.html', users=users)
@app.route('/history')
def history():
transactions = load_transactions()
transactions.reverse()
return render_template('history.html', transactions=transactions)
if __name__ == '__main__':
app.run(debug=True)
Build the HTML templates
Flask uses Jinja2 templates to render HTML pages. Each route in your app points to a template file inside the templates/ folder.
- Create a new file called dashboard.html inside the templates/ folder and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPI Fraud Detection - Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar">
<h1>UPI Pay</h1>
<div class="nav-links">
<a href="{{ url_for('dashboard') }}" class="active">Dashboard</a>
<a href="{{ url_for('send_payment') }}">Send Money</a>
<a href="{{ url_for('history') }}">History</a>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<h2>User Wallets</h2>
<div class="wallet-grid">
{% for upi_id, user in users.items() %}
<div class="wallet-card">
<h3>{{ user.name }}</h3>
<p class="upi-id">{{ upi_id }}</p>
<p class="balance">₹{{ "%.2f"|format(user.balance) }}</p>
</div>
{% endfor %}
</div>
<h2>Recent Transactions</h2>
{% if transactions %}
<div class="transaction-list">
{% for t in transactions %}
<div class="transaction-item {{ t.status }}">
<div class="transaction-info">
<strong>{{ t.sender_name }}</strong> sent ₹{{ "%.2f"|format(t.amount) }} to <strong>{{ t.receiver_name }}</strong>
</div>
<div class="transaction-status">
<span class="badge badge-{{ t.status }}">{{ t.status|upper }}</span>
<span class="risk risk-{{ t.risk_level|lower }}">{{ t.risk_level }} Risk ({{ "%.1f"|format(t.fraud_probability * 100) }}%)</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-state">No transactions yet. Send your first payment!</p>
{% endif %}
</div>
</body>
</html>
What does this template do?
- The {% for upi_id, user in users.items() %} loop creates a wallet card for each pre-seeded user showing their name, UPI ID, and balance.
- The get_flashed_messages() block displays success or error alerts after a payment attempt.
- The transactions section shows recent payments with their approval status and risk scores. It starts empty until you make your first payment.
- Create a new file called send.html inside the templates/ folder and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPI Fraud Detection - Send Payment</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar">
<h1>UPI Pay</h1>
<div class="nav-links">
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('send_payment') }}" class="active">Send Money</a>
<a href="{{ url_for('history') }}">History</a>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="send-form-container">
<h2>Send Payment</h2>
<form method="POST" action="{{ url_for('send_payment') }}" class="send-form">
<div class="form-group">
<label for="sender">From (Sender)</label>
<select name="sender" id="sender" required>
<option value="">Select sender</option>
{% for upi_id, user in users.items() %}
<option value="{{ upi_id }}">{{ user.name }} ({{ upi_id }}) - ₹{{ "%.2f"|format(user.balance) }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="receiver">To (Receiver)</label>
<select name="receiver" id="receiver" required>
<option value="">Select receiver</option>
{% for upi_id, user in users.items() %}
<option value="{{ upi_id }}">{{ user.name }} ({{ upi_id }})</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="amount">Amount (₹)</label>
<input type="number" name="amount" id="amount" min="1" step="0.01" required placeholder="Enter amount">
</div>
<details class="advanced-options">
<summary>Advanced Options (Simulate fraud patterns)</summary>
<div class="form-group">
<label for="hour">Transaction Hour (0-23)</label>
<input type="number" name="hour" id="hour" min="0" max="23" value="14">
</div>
<div class="form-group">
<label for="frequency">Transaction Frequency (today)</label>
<input type="number" name="frequency" id="frequency" min="0" max="50" value="3">
</div>
<div class="form-group">
<label for="account_age">Account Age (days)</label>
<input type="number" name="account_age" id="account_age" min="1" max="2000" value="365">
</div>
</details>
<button type="submit" class="btn-send">Send Payment</button>
</form>
</div>
</div>
</body>
</html>
What does this template do?
- The form has dropdown selectors for the sender and receiver, populated dynamically from the user data.
- The <details> block contains Advanced Options that let you simulate fraud patterns by setting the transaction hour, frequency, and account age. These are hidden by default.
- The form submits via POST. The route does not handle POST yet, so the button will not process payments until the next step.
- Create a new file called history.html inside the templates/ folder and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPI Fraud Detection - Transaction History</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="navbar">
<h1>UPI Pay</h1>
<div class="nav-links">
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('send_payment') }}">Send Money</a>
<a href="{{ url_for('history') }}" class="active">History</a>
</div>
</nav>
<div class="container">
<h2>Transaction History</h2>
{% if transactions %}
<div class="transaction-list">
{% for t in transactions %}
<div class="transaction-item {{ t.status }}">
<div class="transaction-info">
<strong>{{ t.sender_name }}</strong> → <strong>{{ t.receiver_name }}</strong>
<span class="amount">₹{{ "%.2f"|format(t.amount) }}</span>
</div>
<div class="transaction-status">
<span class="badge badge-{{ t.status }}">{{ t.status|upper }}</span>
<span class="risk risk-{{ t.risk_level|lower }}">{{ t.risk_level }} Risk ({{ "%.1f"|format(t.fraud_probability * 100) }}%)</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-state">No transactions yet.</p>
{% endif %}
</div>
</body>
</html>
What does this template do?
This page shows a complete log of every transaction. Each entry displays the sender, receiver, amount, status badge (approved or blocked), and the fraud risk percentage. It starts with an empty state message until payments are processed.
- Save all three template files.
- Confirm the files exist by running:
ls templates/
You should see dashboard.html, send.html, and history.html listed in the output.
Missing a template file?
- Make sure the files are saved inside the templates/ subfolder, not in the project root.
- Check the file names are exactly dashboard.html, send.html, and history.html. Flask is case-sensitive with template names.
Help me find why Flask can't find my templates.
Style the app and run it
The templates reference a stylesheet at static/style.css. This file gives the app a purple-gradient UPI theme with card layouts and responsive styling.
- Create a new file called style.css inside the static/ folder and paste this code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
color: #333;
}
.navbar {
background: linear-gradient(135deg, #6c3ce0, #4a1db5);
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar h1 {
font-size: 1.5rem;
}
.nav-links a {
color: white;
text-decoration: none;
margin-left: 1.5rem;
opacity: 0.8;
transition: opacity 0.2s;
}
.nav-links a:hover,
.nav-links a.active {
opacity: 1;
border-bottom: 2px solid white;
padding-bottom: 2px;
}
.container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
h2 {
margin-bottom: 1rem;
color: #333;
}
.alert {
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
font-weight: 500;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-error {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.wallet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.wallet-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.wallet-card h3 {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.wallet-card .upi-id {
color: #666;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.wallet-card .balance {
font-size: 1.3rem;
font-weight: bold;
color: #6c3ce0;
}
.transaction-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.transaction-item {
background: white;
border-radius: 8px;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
border-left: 4px solid #28a745;
}
.transaction-item.blocked {
border-left-color: #dc3545;
background: #fff8f8;
}
.transaction-info {
flex: 1;
}
.transaction-info .amount {
margin-left: 1rem;
font-weight: bold;
}
.transaction-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.badge-approved {
background: #d4edda;
color: #155724;
}
.badge-blocked {
background: #f8d7da;
color: #721c24;
}
.risk {
font-size: 0.8rem;
}
.risk-low {
color: #28a745;
}
.risk-medium {
color: #ffc107;
}
.risk-high {
color: #dc3545;
}
.send-form-container {
max-width: 500px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #6c3ce0;
}
.advanced-options {
margin-bottom: 1.5rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.advanced-options summary {
cursor: pointer;
font-weight: 500;
color: #6c3ce0;
margin-bottom: 1rem;
}
.btn-send {
width: 100%;
padding: 1rem;
background: linear-gradient(135deg, #6c3ce0, #4a1db5);
color: white;
border: none;
border-radius: 8px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: transform 0.1s;
}
.btn-send:hover {
transform: scale(1.02);
}
.btn-send:active {
transform: scale(0.98);
}
.empty-state {
text-align: center;
color: #999;
padding: 2rem;
}
What does this stylesheet do?
- The .navbar uses a purple gradient that matches real UPI app branding.
- The .wallet-grid creates a responsive card layout that wraps user wallets across the screen.
- The .badge-approved and .badge-blocked classes give visual color-coding for transaction outcomes (green for approved, red for blocked).
- Save static/style.css.
- Run the Flask app by running this command:
python app.py
- Open your browser and navigate to http://127.0.0.1:5000.
You should see the dashboard page with a purple navigation bar at the top and 5 wallet cards showing user names, UPI IDs, and balances in purple text. The Recent Transactions section shows an empty state message.
- Click Send Money in the navigation bar.
You should see a clean payment form with sender and receiver dropdowns, an amount field, and a purple Send Payment button.
What about the users.json file?
When you loaded the dashboard, Flask called load_users() for the first time. Because no users.json existed yet, the function created one automatically with the 5 pre-seeded accounts. Check your project folder and you will see it there now.
Seeing a TemplateNotFound error?
- Double-check that your template files are inside a folder named exactly templates (plural, lowercase). Flask looks for this specific folder name.
- Make sure the file names match what the routes expect: dashboard.html, send.html, history.html.
- Verify that your terminal is inside the upi-fraud-detection folder when you run python app.py.
Help me debug my Flask TemplateNotFound error.
Your payment interface is live. Next up, you will wire the fraud detection model into the payment flow so every transaction gets scored before it processes.
Integrate Fraud Detection into the Payment Flow
You have a trained fraud detection model sitting in your project folder and a working payment interface. Right now, though, they exist in separate worlds.
In this step, you will connect the model to the payment route so every transaction gets scored in real-time before it processes. Suspicious payments get blocked automatically.
In this step, get ready to:
- Load the trained model into the Flask app and create a prediction function.
- Wire fraud scoring into the payment route so transactions are evaluated before processing.
- Verify that approved and blocked transactions display correctly to the user.
Load the model and create the prediction function
For the Flask app to score transactions, it needs access to the trained Random Forest model. Loading the model once at startup is more efficient than reloading it on every request.
- In app.py, find this line near the top of the file:
app.secret_key = 'upi-fraud-detection-secret-key'
- Add this line directly below it to load your trained model into memory:
# Load the trained fraud detection model once at startup
model = joblib.load('fraud_model.pkl')
What does this line do?
joblib.load('fraud_model.pkl') reads the saved RandomForestClassifier from disk into a Python object. Because this line runs at app startup (outside any function), the model stays in memory and is ready for every incoming request.
Now you need a function that takes raw transaction details, engineers the same features the model was trained on, and returns a fraud probability with a risk level.
- In app.py, find the save_transaction() function. Add the following predict_fraud() function below it (before the @app.route('/') line):
def predict_fraud(amount, sender_balance, receiver_balance, hour, frequency, account_age):
# Engineer the same features the model was trained on
amount_to_balance_ratio = amount / (sender_balance + 1)
is_new_account = 1 if account_age < 30 else 0
is_night = 1 if hour < 6 or hour > 22 else 0
# Build the 9-feature array matching training data columns
features = np.array([[
amount, sender_balance, receiver_balance,
hour, frequency, account_age,
amount_to_balance_ratio, is_new_account, is_night
]])
# Get the probability of fraud (index 1 = fraud class)
fraud_probability = model.predict_proba(features)[0][1]
# Classify risk level based on probability thresholds
if fraud_probability > 0.7:
risk_level = "High"
elif fraud_probability > 0.4:
risk_level = "Medium"
else:
risk_level = "Low"
return fraud_probability, risk_level
What does this function do?
This function mirrors the feature engineering from your training script. It computes three derived features (amount-to-balance ratio, new account flag, night transaction flag) so the input matches what the model expects.
model.predict_proba(features)[0][1] returns the probability that the transaction is fraudulent. The function then maps that probability to a human-readable risk level: Low, Medium, or High.
- Save app.py.
You will not see a change in the browser yet. The next chunk wires this function into the payment route.
Wire fraud detection into the payment route
The current send_payment() route only handles GET requests (showing the form). You need to add POST handling so it processes submitted payments through the fraud model.
- In app.py, find your existing send_payment() function:
@app.route('/send', methods=['GET'])
def send_payment():
users = load_users()
return render_template('send.html', users=users)
- Replace the entire send_payment() function (including the @app.route decorator) with the following code:
@app.route('/send', methods=['GET', 'POST'])
def send_payment():
users = load_users()
if request.method == 'POST':
# Extract form data
sender = request.form['sender']
receiver = request.form['receiver']
amount = float(request.form['amount'])
hour = int(request.form.get('hour', 14))
frequency = int(request.form.get('frequency', 3))
account_age = int(request.form.get('account_age', 365))
# Validate the transaction basics
if sender == receiver:
flash('Cannot send money to yourself!', 'error')
return redirect(url_for('send_payment'))
if sender not in users or receiver not in users:
flash('Invalid sender or receiver!', 'error')
return redirect(url_for('send_payment'))
if users[sender]['balance'] < amount:
flash('Insufficient balance!', 'error')
return redirect(url_for('send_payment'))
# Get current balances for fraud scoring
sender_balance = users[sender]['balance']
receiver_balance = users[receiver]['balance']
# Score the transaction through the fraud model
fraud_prob, risk_level = predict_fraud(
amount, sender_balance, receiver_balance,
hour, frequency, account_age
)
# Build the transaction record
transaction = {
'sender': sender,
'sender_name': users[sender]['name'],
'receiver': receiver,
'receiver_name': users[receiver]['name'],
'amount': amount,
'fraud_probability': round(fraud_prob, 4),
'risk_level': risk_level,
'status': 'blocked' if fraud_prob > 0.5 else 'approved'
}
# Block or approve based on fraud probability
if fraud_prob > 0.5:
transaction['reason'] = f"Transaction blocked - Fraud risk: {fraud_prob:.1%} ({risk_level})"
save_transaction(transaction)
flash(f'BLOCKED: Fraud detected! Risk score: {fraud_prob:.1%} ({risk_level} risk)', 'danger')
else:
users[sender]['balance'] -= amount
users[receiver]['balance'] += amount
save_users(users)
save_transaction(transaction)
flash(f'Payment successful! Risk score: {fraud_prob:.1%} ({risk_level} risk)', 'success')
return redirect(url_for('dashboard'))
return render_template('send.html', users=users)
What does this code do?
- methods=['GET', 'POST'] tells Flask this route now accepts form submissions, not just page loads.
- The validation block checks three things: the sender is not the receiver, both accounts exist, and the sender has enough money.
- predict_fraud() scores the transaction BEFORE any money moves. If the fraud probability exceeds 0.5, the payment is blocked and no balances change.
- flash() sends a one-time message to the next page the user sees. The category ('danger' or 'success') controls the alert styling in the template.
- Save app.py.
Seeing a syntax error when you save?
Make sure the old send_payment() function is completely removed. Having two functions with the same name or a leftover @app.route('/send') decorator will cause errors.
Also check that the f-strings use matching curly braces. Each { must have a closing }.
Help me debug a syntax error in my Flask send_payment route
Verify the fraud detection displays results
Your templates already handle displaying success and blocked messages. Time to confirm the full flow works end-to-end.
- If your Flask server is still running, it will auto-reload after you save. If you stopped it, start it again by running:
python app.py
- Open http://127.0.0.1:5000/send in your browser.
- Select any sender from the **From (Sender)** dropdown.
- Select a different user from the **To (Receiver)** dropdown.
- Enter 500 in the **Amount** field.
- Click **Send Payment**.
You should be redirected to the dashboard with a green success alert showing the payment went through and a low risk score.
Why does a normal payment pass?
A 500 rupee transfer during business hours from an established account matches the legitimate transaction patterns in your training data. The model scores it with a low fraud probability, well below the 0.5 threshold.
Now test with a suspicious transaction to verify blocking works.
- Go back to http://127.0.0.1:5000/send.
- Select any sender and a different receiver.
- Enter 45000 in the **Amount** field.
- Click **Advanced Options (Simulate fraud patterns)** to expand the extra fields.
- Change **Transaction Hour** to 2.
- Change **Transaction Frequency** to 20.
- Change **Account Age** to 5.
- Click **Send Payment**.
You should see a red alert saying the transaction was blocked with a high fraud risk score. The sender's balance remains unchanged.
Why does this transaction get blocked?
This transaction hits multiple fraud signals at once: a very large amount, a late-night hour, high transaction frequency, and a brand-new account. These are the exact patterns you injected into the fraud training data in Step 2.
Payment not being blocked?
Make sure the Advanced Options section is expanded and you entered the suspicious values before clicking Send. If the fields are collapsed, the form submits default values (hour 14, frequency 3, age 365) which look legitimate.
Also verify that fraud_model.pkl exists in your upi-fraud-detection folder. If it is missing, re-run python train_model.py to regenerate it.
Help me debug why my fraud model is not blocking suspicious transactions
✔️ Awesome, I've got everything!
Double check you have saved app.py and your server is running without errors.
ⓧ I'd like to double check the full code
Here is the complete app.py file as it should look at the end of this step:
import json
import os
from flask import Flask, render_template, request, redirect, url_for, flash
import joblib
import numpy as np
app = Flask(__name__)
app.secret_key = 'upi-fraud-detection-secret-key'
model = joblib.load('fraud_model.pkl')
USERS_FILE = 'users.json'
TRANSACTIONS_FILE = 'transactions.json'
def load_users():
if os.path.exists(USERS_FILE):
with open(USERS_FILE, 'r') as f:
return json.load(f)
users = {
"rahul@upi": {"name": "Rahul Sharma", "balance": 25000.0},
"priya@upi": {"name": "Priya Patel", "balance": 18000.0},
"amit@upi": {"name": "Amit Kumar", "balance": 32000.0},
"sneha@upi": {"name": "Sneha Reddy", "balance": 15000.0},
"vikram@upi": {"name": "Vikram Singh", "balance": 40000.0}
}
save_users(users)
return users
def save_users(users):
with open(USERS_FILE, 'w') as f:
json.dump(users, f, indent=2)
def load_transactions():
if os.path.exists(TRANSACTIONS_FILE):
with open(TRANSACTIONS_FILE, 'r') as f:
return json.load(f)
return []
def save_transaction(transaction):
transactions = load_transactions()
transactions.append(transaction)
with open(TRANSACTIONS_FILE, 'w') as f:
json.dump(transactions, f, indent=2)
def predict_fraud(amount, sender_balance, receiver_balance, hour, frequency, account_age):
amount_to_balance_ratio = amount / (sender_balance + 1)
is_new_account = 1 if account_age < 30 else 0
is_night = 1 if hour < 6 or hour > 22 else 0
features = np.array([[
amount, sender_balance, receiver_balance,
hour, frequency, account_age,
amount_to_balance_ratio, is_new_account, is_night
]])
fraud_probability = model.predict_proba(features)[0][1]
if fraud_probability > 0.7:
risk_level = "High"
elif fraud_probability > 0.4:
risk_level = "Medium"
else:
risk_level = "Low"
return fraud_probability, risk_level
@app.route('/')
def dashboard():
users = load_users()
transactions = load_transactions()
recent_transactions = transactions[-10:] if transactions else []
recent_transactions.reverse()
return render_template('dashboard.html', users=users, transactions=recent_transactions)
@app.route('/send', methods=['GET', 'POST'])
def send_payment():
users = load_users()
if request.method == 'POST':
sender = request.form['sender']
receiver = request.form['receiver']
amount = float(request.form['amount'])
hour = int(request.form.get('hour', 14))
frequency = int(request.form.get('frequency', 3))
account_age = int(request.form.get('account_age', 365))
if sender == receiver:
flash('Cannot send money to yourself!', 'error')
return redirect(url_for('send_payment'))
if sender not in users or receiver not in users:
flash('Invalid sender or receiver!', 'error')
return redirect(url_for('send_payment'))
if users[sender]['balance'] < amount:
flash('Insufficient balance!', 'error')
return redirect(url_for('send_payment'))
sender_balance = users[sender]['balance']
receiver_balance = users[receiver]['balance']
fraud_prob, risk_level = predict_fraud(
amount, sender_balance, receiver_balance,
hour, frequency, account_age
)
transaction = {
'sender': sender,
'sender_name': users[sender]['name'],
'receiver': receiver,
'receiver_name': users[receiver]['name'],
'amount': amount,
'fraud_probability': round(fraud_prob, 4),
'risk_level': risk_level,
'status': 'blocked' if fraud_prob > 0.5 else 'approved'
}
if fraud_prob > 0.5:
transaction['reason'] = f"Transaction blocked - Fraud risk: {fraud_prob:.1%} ({risk_level})"
save_transaction(transaction)
flash(f'BLOCKED: Fraud detected! Risk score: {fraud_prob:.1%} ({risk_level} risk)', 'danger')
else:
users[sender]['balance'] -= amount
users[receiver]['balance'] += amount
save_users(users)
save_transaction(transaction)
flash(f'Payment successful! Risk score: {fraud_prob:.1%} ({risk_level} risk)', 'success')
return redirect(url_for('dashboard'))
return render_template('send.html', users=users)
@app.route('/history')
def history():
transactions = load_transactions()
transactions.reverse()
return render_template('history.html', transactions=transactions)
if __name__ == '__main__':
app.run(debug=True)
Your fraud detection system is live. Every payment now passes through the ML model before processing. Next up, you will run a full suite of tests with both normal and suspicious transactions to see how well the model performs in practice.
Test Your Fraud Detection System
You have a fully built payment app with a trained Random Forest model watching every transaction. The real question is: does it actually catch fraud?
In this step, you will put your fraud detection system through its paces. You will send both normal and suspicious transactions to verify the model approves legitimate payments and blocks fraudulent ones.
In this step, get ready to:
- Send normal transactions and verify they are approved with low risk scores.
- Send suspicious transactions and verify they are blocked with high risk scores.
- Review the transaction history page showing the full audit log.
Send a normal transaction
A normal transaction has a small amount, happens during daytime hours, comes from an established account, and has a low transaction frequency. These are the defaults your form already uses.
- Open your browser and navigate to http://127.0.0.1:5000/send.
- Select Rahul Sharma (rahul@upi) from the From (Sender) dropdown.
- Select Priya Patel (priya@upi) from the To (Receiver) dropdown.
- Enter 500 in the Amount field.
- Leave the Advanced Options section closed (the defaults simulate a normal transaction: hour 14, frequency 3, account age 365 days).
- Click Send Payment.
You should be redirected to the dashboard with a green success banner showing "Payment successful!" along with a Low risk score percentage.
Why did it pass?
Your model learned from the training data that legitimate transactions tend to have small amounts relative to the sender's balance, happen during business hours, and come from older accounts with low activity. This transaction matches all of those patterns, so the model assigns a low fraud probability.
Seeing an error instead of the success message?
Make sure you selected different users for sender and receiver. The app blocks payments to yourself.
Check that your Flask server is still running in the terminal. If it stopped, run python app.py again from inside your upi-fraud-detection folder.
Help me debug my Flask payment app
Send a suspicious transaction
Now you will simulate a fraudulent transaction. Fraudsters typically send very large amounts from new accounts at unusual hours with high transaction frequency.
- Click Send Money in the navigation bar.
- Select Amit Kumar (amit@upi) as the sender.
- Select Sneha Reddy (sneha@upi) as the receiver.
- Enter 30000 in the Amount field.
- Click Advanced Options (Simulate fraud patterns) to expand the section.
- Change Transaction Hour to 2 (2 AM).
- Change Transaction Frequency to 20.
- Change Account Age to 5 days.
- Click Send Payment.
You should see a red alert banner with "BLOCKED: Fraud detected!" followed by a High risk score percentage. The payment does not go through and the sender's balance stays the same.
What triggered the fraud detection?
This transaction hit multiple fraud signals that your model learned from the synthetic data. The large amount relative to balance, the 2 AM timestamp, the 20 transactions in a single day, and the 5-day-old account all match the fraud patterns you injected in generate_data.py.
The model combines all these features together. Even triggering just two or three of these flags can push the fraud probability above the 0.5 threshold.
Check the transaction history
Every transaction you send, whether approved or blocked, gets logged in transactions.json. The history page gives you a full audit trail with risk scores.
- Click History in the navigation bar.
You should see both of your test transactions listed. The approved transaction has a green left border and an "APPROVED" badge. The blocked transaction has a red left border and a "BLOCKED" badge.
What do the risk levels mean?
- Low Risk (0-40% probability): Transaction looks normal. Approved automatically.
- Medium Risk (40-70% probability): Some suspicious signals. Blocked if above 50%.
- High Risk (70%+ probability): Strong fraud indicators. Always blocked.
- Try sending a few more transactions with different amounts and settings to see how the model responds to various patterns.
- Return to the Dashboard to confirm the wallet balances updated correctly for approved transactions.
Test edge cases
Try a medium-sized amount (like 8000) with just one suspicious feature (like hour 2 but normal frequency and account age). This helps you understand where the model draws the boundary between normal and suspicious.
All transactions getting approved even with suspicious values?
Make sure you expanded the Advanced Options section and changed the values before clicking Send Payment. The form defaults are intentionally set to look normal.
Verify your fraud_model.pkl file was generated by running train_model.py successfully in the previous step. If the file is empty or corrupt, the model will not score correctly.
Help me debug why my fraud model is not blocking suspicious transactions
Your fraud detection system is working. You have a complete ML-powered payment app that evaluates every transaction in real-time. Next, take on the Secret Mission to build an analytics dashboard that visualizes your model's performance and fraud patterns.
Secret mission
Build a Fraud Analytics Dashboard
Your fraud detection system works, but how well is it really performing? In this secret mission, you'll build a dedicated analytics page that visualizes model metrics, fraud patterns, and lets you batch-test transactions with color-coded risk levels.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Python virtual environment (.venv folder).
- Generated dataset (upi_transactions.csv).
- Trained model file (fraud_model.pkl).
- Flask application files (app.py, templates, static folder).
- Data scripts (generate_data.py, train_model.py).
- JSON data files (users.json, transactions.json).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment further with the fraud detection model.
- Your Flask server will stop when you close the terminal, but all files remain on your Desktop.
- To restart the app later, activate the virtual environment and run python app.py again.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk so you can pick up where you left off.
- Stop the Flask server by pressing Ctrl+C in the terminal where it is running.
- Deactivate the virtual environment by running deactivate in your terminal.
- Close the terminal window.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your trained model, dataset, and application code.
- Stop the Flask server by pressing Ctrl+C if it is still running.
- Deactivate the virtual environment by running deactivate.
- Delete the entire project folder from your Desktop. Open File Explorer, navigate to your Desktop, right-click the project folder, and select Delete.
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack UPI payment application with real-time machine learning fraud detection.
You've learned how to:
- Generate synthetic training data with realistic fraud patterns and train a Random Forest classifier to detect them.
- Build a Flask web application with user wallets, payment flows, and transaction history that simulates a real UPI system.
- Integrate a trained ML model as real-time middleware that scores and blocks suspicious transactions before they process.
- Built a fraud analytics dashboard showing model performance metrics and top fraud pattern insights.
Ready to quiz yourself?