Real-time fraud detection for Squad merchants.
Built by Team Sentinel for Squad Hackathon 3.0 — Smart Systems: The Intelligent Economy track, organised by HabariPay/GTCO.
cp .env.example .env
# Add your GROQ_API_KEY — free at console.groq.com
# Add SQUAD_SECRET_KEY and SQUAD_PUBLIC_KEY from sandbox.squadco.com
docker-compose up --build- Dashboard → http://localhost:3001
- API docs → http://localhost:8011/docs
GROQ_API_KEY is optional. Without it, ShieldPay uses its built-in deterministic rule engine — all Squad integrations and the dashboard remain fully functional.
Demo flow (6 steps):
- Start the stack:
docker-compose up --build - Seed multi-merchant demo data:
BASE_URL=http://localhost:8011 python backend/seed_multi_merchant.py
- Open the dashboard: http://localhost:3001
- Fire a fraud transaction:
curl -X POST http://localhost:8011/webhook/simulate \ -H "Content-Type: application/json" \ -d '{"transaction_ref":"DEMO-001","amount":950000,"merchant_id":"demo_merchant","email":"test@demo.ng","timestamp":"2026-05-15T02:30:00"}'
- Watch the verdict appear in the transaction feed (dashboard auto-refreshes every 5 s).
- Click the verdict row → inspect the investigation log → submit a feedback label to queue a retrain.
# Backend
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python -m uvicorn main:app --port 8010 --reload
# Frontend (separate terminal)
cd frontend
npm install && npm run devShieldPay sits behind Squad's payment webhooks. Every transaction that settles fires a webhook — ShieldPay intercepts it, runs it through an ML scoring pipeline and an LLM-backed investigation agent, and produces a verdict: SAFE, SUSPICIOUS, or BLOCK. High-confidence blocks trigger an automatic refund through Squad's API. Merchants confirm or dispute verdicts, and those labels feed back into the model to retrain it.
The system never sits in the transaction path. Scoring happens post-settlement, asynchronously — zero latency impact on checkout.
Nigeria loses an estimated ₦52 billion to payment fraud annually. Squad merchants have no native fraud intelligence layer — they receive transaction webhooks but have no tooling to score them, investigate patterns, or act on suspicious activity. ShieldPay is that layer.
| Name | Role | Owns |
|---|---|---|
| Xanes (Oladapo) | Squad API + Agent Core | squad/, agent/, rag/, network/, ml/retrain.py, data/ |
| Femi | Backend + API Server | api/, db/, main.py |
| obimz | ML Engineer | ml/model.py, ml/features.py |
| Daniel | Frontend + Dashboard | frontend/ |
flowchart TD
A[Squad Payment Webhook] --> B[HMAC-SHA512 Verification\nsquad/hmac.py]
B -->|Invalid signature| C[401 Rejected]
B -->|Valid| D[Enrichment + Flatten\nsquad/investigation.py]
D --> E[Squad /transaction/verify\nenrich_transaction]
E --> F[Cross-Merchant Fingerprint Check\nnetwork/fingerprints.py]
F -->|3+ fingerprint hits| G[Instant BLOCK\nrisk_score = 0.98]
F -->|No prior match| H[FraudAgent Dispatcher\nagent/dispatcher.py]
H --> I[RAG Query\nrag/retriever.py\nChromaDB — 12 fraud pattern docs]
I --> J{Groq API healthy?}
J -->|Yes| K[Groq LLM Agent\nLlama 3.3-70b\nagent/groq_agent.py]
J -->|No / Timeout| L[Deterministic Orchestrator\nagent/deterministic.py]
K --> M[Shared Tool Harness\nagent/tools.py]
L --> M
M --> N[check_velocity]
M --> O[get_anomaly_score]
M --> P[check_amount_deviation]
M --> Q[get_merchant_profile]
O --> R[ML Scoring Pipeline\nml/model.py + ml/features.py]
N & O & P & Q --> S[Verdict\nBLOCK / SUSPICIOUS / SAFE]
G --> T[Store Verdict + Tag Source\ndb/models.py]
S --> T
T -->|risk_score > 0.92| U[Auto-Refund\nSquad /transaction/refund]
T --> V[Dashboard Feed\nGET /verdicts/]
V --> W[Merchant Feedback\nPOST /feedback/txn_id]
W --> X[Retrain Loop\nml/retrain.py]
X -->|Shadow test passes| Y[Deploy New Models\nisolation_forest.pkl\nxgboost.pkl]
This is what ShieldPay produces for every transaction — not a score, an investigation:
{
"transaction_ref": "TXN-9f3k2",
"verdict": "BLOCK",
"risk_score": 0.91,
"source": "groq",
"reasoning": "Three concurrent signals detected: velocity anomaly — 7 transactions in 6 minutes from the same device fingerprint; amount 380% above this merchant's 30-day average; odd-hours flag — 02:14 WAT. Cross-merchant fingerprint matched: device previously confirmed fraudulent at 2 other Squad merchants. Auto-refund initiated.",
"signals": {
"velocity_1h": 7,
"amount_zscore_merchant": 3.8,
"is_odd_hours": true,
"fingerprint_hits": 2
},
"action": "auto_refund_initiated"
}flowchart LR
A[dispatcher.py\nroutes the call] --> B{asyncio.wait_for\n5s timeout}
B -->|Within timeout| C[groq_agent.py\nLlama 3.3-70b]
B -->|Timeout / RateLimitError\nAPIConnectionError| D[deterministic.py\nrule-based orchestrator]
C --> E[Tool-use loop\nmax 10 iterations]
D --> F[Phase 1: velocity + anomaly\nalways runs]
E --> G[tools.py\nshared harness]
F --> G
G --> H[check_velocity\nDB rolling count]
G --> I[get_anomaly_score\nML pipeline]
G --> J[check_amount_deviation\nz-score vs user history]
G --> K[get_merchant_profile\ndeviation vs merchant avg]
E -->|submit_verdict intercepted| L[Verdict\nsource: groq]
F --> M[Score fusion\n40% anomaly + 25% velocity\n20% deviation + 15% merchant\nx1.25 odd-hours multiplier]
M --> N[Verdict\nsource: deterministic]
flowchart TD
A[Incoming Transaction\nfrom investigation context] --> B[engineer_features_online\nml/features.py]
C[User Transaction History\nfrom DB — last 200 rows] --> B
B --> D1[count_1h\nrolling velocity 1-hour]
B --> D2[count_24h\nrolling velocity 24-hour]
B --> D3[amount_zscore_user\nvs user history mean/std]
B --> D4[amount_zscore_merchant\nvs merchant history mean/std]
B --> D5[time_since_last_txn\nseconds since prior transaction]
B --> D6[is_odd_hours\n00:00-05:00 WAT flag]
B --> D7[merchant_txn_count\nprior visits to this merchant]
D1 & D2 & D3 & D4 & D5 & D6 & D7 --> E[features_to_vector\nnumpy 1x7 array]
E --> F[IsolationForest\nscore_samples + normalize]
E --> G[XGBoost\npredict_proba]
F --> H[IF Score x 0.40]
G --> I[XGB Score x 0.60]
H & I --> J[Ensemble Score 0.0 to 1.0]
J -->|greater than 0.75| L[BLOCK]
J -->|0.45 to 0.75| M[SUSPICIOUS]
J -->|less than 0.45| N[SAFE]
sequenceDiagram
participant M as Merchant Dashboard
participant F as feedback.py
participant TL as TrustedLabel DB
participant FP as FraudFingerprint DB
participant R as retrain.py
participant S as Shadow Test
M->>F: POST /feedback/{txn_id} label: fraud / safe
F->>TL: Store TrustedLabel weight based on source
F->>FP: write_fingerprint() on fraud label
F->>R: retrain_if_ready() as background task
R->>R: should_retrain()? min 20 new labels OR 6 hours elapsed gated on 100 total
R->>R: Fetch labeled transactions from DB engineer_features() batch
R->>S: Train candidate model
S->>S: Evaluate accuracy vs current baseline
alt Shadow test passes
R->>R: Deploy isolation_forest.pkl + xgboost.pkl
R->>R: Log RetrainEvent deployed: true
else Shadow test fails
R->>R: Log RetrainEvent deployed: false keep current models
end
The initial proposal was simple: score Squad webhook payloads with an Isolation Forest model and flag anomalies. That version had a core problem — it produced a number. A risk score of 0.73 tells a merchant nothing. They still have to decide what to do with it, they still don't know why it fired, and there is no mechanism to make the system smarter over time.
First shift — investigation over scoring. Instead of a static pipeline that returns a number, the team built a FraudAgent: an orchestrator that receives a transaction, dynamically calls a suite of analysis tools, synthesises the results, and produces a verdict with a written reasoning string a merchant can actually read and act on.
Second shift — Nigerian context. A generic fraud model trained on synthetic data doesn't know that transactions between midnight and 5am in Nigeria carry elevated risk, what card-testing looks like in a Lagos market, or what CBN AML guidelines say about sub-threshold splitting. A RAG layer was added: 12 documents covering Nigerian-specific fraud patterns, loaded into ChromaDB, queried before every LLM agent call to give it grounded regional context.
Third shift — reliability. The LLM is a network call. It can fail, rate-limit, or time out during a live demo. Rather than accepting that risk, a deterministic fallback was built: a rule-based orchestrator that uses the exact same tool harness as the Groq agent and produces verdicts with identical shape and quality. Every verdict is tagged source: groq or source: deterministic. The system degrades invisibly.
Fourth shift — collective intelligence. A fraud fingerprint confirmed at one merchant shouldn't be invisible to every other merchant on Squad. A cross-merchant fingerprint store was added: device fingerprints and IP addresses tied to confirmed fraud are written to a shared table. Every new transaction is checked against it before the agent runs. Three or more matches triggers an instant block without touching the LLM.
Femi designed and built the entire server layer from the ground up. Every FastAPI route file across all nine endpoints, the complete Pydantic schema layer covering every request validation and response serialisation shape, SQLAlchemy ORM models for all five database tables with correct foreign key relationships, enum type definitions, and eager-load configurations. He built the database session management, dependency injection pattern, and the init_db() initialisation that runs on startup. The BackgroundTask pattern that makes the entire async pipeline possible — returning 200 immediately while investigation runs in the background — is his design decision, and it's what makes the webhook architecture work at all. He wired CORS, registered all routers, and built the simulate endpoint that makes live testing possible without a real Squad sandbox event. The entire API surface that the frontend and Squad integration depend on was designed, written, and owned by Femi.
obimz owned every part of the ML system. He designed the full feature engineering pipeline in features.py — including the rolling 1-hour and 24-hour velocity counts using correct pandas windowing, per-user and per-merchant amount z-scores with proper minimum-sample guards, WAT odd-hours detection, time-since-last-transaction delta computation, and the strictly ordered features_to_vector() transformation that produces a consistent numpy array for model input. He built and trained both the IsolationForest and XGBoost models, tuned the 40/60 ensemble weighting that balances anomaly detection sensitivity with classification precision, and wrote the shadow testing framework inside the retrain loop — the mechanism that evaluates every candidate model against the current production baseline before deployment, protecting accuracy from label noise. His feature engineering is the foundation that every agent tool call ultimately depends on when scoring a transaction.
Daniel built the entire user-facing product. The admin dashboard with its live transaction feed polling every 5 seconds, animated KPI cards, model accuracy gauge that responds to retrain events, fraud pattern breakdown charts, and investigation log drawer that expands per-transaction reasoning. The merchant profile view with scoped transaction history, fraud rate tracking, total volume display, and top fraud signal chips. The splash screen entry flow with the ShieldPay brand identity, the simulate webhook demo panel built for live judging, and all the real-time polling logic managing seven separate data streams without race conditions. Every visual element the judges and merchants interact with is Daniel's work.
Xanes built the Squad integration layer from scratch — transaction enrichment against Squad's verify endpoint, webhook payload flattening with kobo-to-NGN conversion, PII-safe user ID hashing, and the refund trigger. He assembled squad/investigation.py, the end-to-end pipeline that sequences enrichment, fingerprint lookup, agent dispatch, verdict storage, and conditional auto-refund into a single coherent flow. He implemented the FraudAgent dispatcher with its timeout and failover logic, the full Groq LLM agent loop with multi-turn tool-use handling and graceful fallback on max iterations, and the deterministic orchestrator with three-phase conditional tool execution, score fusion, and natural language reasoning generation. He built the RAG retriever with lazy ChromaDB initialisation and the full 12-document Nigerian fraud pattern knowledge base. He built the cross-merchant fingerprint store and the network hit detection logic. He wrote the ML retrain wiring that connects the feedback API to the retraining pipeline. He generated the ~10K synthetic transaction dataset with injected fraud patterns. He owns the pitch.
score_transaction() had a try: block that opened at 8-space indent then broke to 4-space indent mid-function with a bare import statement — not an except clause. Python raised SyntaxError before the module loaded. Every downstream caller was silently receiving the 0.5 fallback score on every transaction.
Resolution: Rebuilt score_transaction() from scratch with correct structure and removed the dead validation block that was shadowing the features parameter.
model.py used 8 raw webhook fields processed through label encoders. features.py had a separate set of 7 engineered signals. The pkl files on disk were trained on the raw set. The retrain script used the engineered set. They had never been connected, meaning the model in production and the model being retrained were operating on completely different inputs.
Resolution: Switched the entire pipeline to engineered features. The key addition was engineer_features_online() — a function that computes all 7 features for a single live transaction by querying the user's DB history at inference time, solving the online feature problem that had previously made engineered features impossible to use outside batch training.
- Saved models as
iso_forest.pkl/xgb_model.pkl; inference loadedisolation_forest.pkl/xgboost.pkl - Raw SQL queried table
squadwebhook_transactions; actual ORM table istransactions - SQL JOIN referenced
trustedlabel.transaction_id; actual table and column aretrusted_labels.txn_id - Velocity grouping used
user_id; Transaction table storesuser_id_hashAll four fixed. The retrain loop is wired and tested end-to-end.
Test waits of 3.5–4.5 seconds were sized for local processing. When Groq rejected the test key and the system fell back to deterministic, the full round-trip took 5–7 seconds. Tests were asserting on verdicts that hadn't been written yet.
Resolution: Increased all background-verdict waits to 8 seconds.
The HMAC test posted with a correct signature but the test process was reading an empty SQUAD_SECRET_KEY while the server had loaded the real value from .env. Signatures never matched.
Resolution: Added load_dotenv(find_dotenv(usecwd=True)) at test module import time.
Function defined with only is_fraud as a parameter but referenced user_id and timestamp as bare local variables. Call sites passed positional arguments the signature didn't accept, including is_velocity=True which didn't exist on the function at all.
Resolution: Updated signature to generate_transaction(is_fraud, user_id, timestamp=None, is_velocity=False) and added card-testing amount reduction logic for velocity fraud patterns.
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in GROQ_API_KEY and SQUAD_SECRET_KEY
uvicorn main:app --reload --port 8010
# Seed demo data (optional, run while server is up)
python seed_multi_merchant.py # BASE_URL defaults to http://localhost:8010cd backend
python data/generate.py
python -c "from ml.retrain import retrain_models_now; retrain_models_now()"cd frontend
npm install
npm run dev # http://localhost:3000GROQ_API_KEY=
SQUAD_SECRET_KEY=
SQUAD_BASE_URL=https://sandbox-api-d.squadco.com
DATABASE_URL=sqlite:///./shieldpay.db
CHROMA_PERSIST_DIR=./chroma_store
MODEL_DIR=./ml/models
ENABLE_RAG=true
AUTO_REFUND_ENABLED=true
RETRAIN_MIN_LABELS=20
RETRAIN_TIME_HOURS=6
RETRAIN_MIN_TOTAL=100
cd backend
pytest -v --tb=short tests/test_integration.py19 tests covering: webhook pipeline, HMAC verification, transactions and verdicts API, investigation logs, feedback and retrain triggering, manual refunds, stats aggregates, and deterministic fallback forcing.
curl -X POST http://localhost:8011/webhook/simulate \
-H "Content-Type: application/json" \
-d '{
"transaction_ref": "DEMO-001",
"amount": 950000,
"merchant_id": "demo_merchant",
"merchant_name": "Demo Merchant",
"channel": "card",
"email": "demo@test.ng",
"timestamp": "2026-05-15T02:30:00"
}'
curl http://localhost:8011/verdicts/ | python -m json.toolmain ← final working build only
└── dev ← integration branch
├── feat/daniel-ui
├── feat/femi-api
├── feat/obimz-ml
└── feat/xanes-core
PRs into dev only. Merge to main once, before judging.
| Layer | Technology |
|---|---|
| Backend framework | FastAPI |
| Database | SQLite (SQLAlchemy ORM) |
| ML models | scikit-learn (IsolationForest), XGBoost |
| LLM agent | Groq — Llama 3.3-70b-versatile |
| Vector store | ChromaDB + SentenceTransformer |
| Squad integration | Squad Sandbox API (webhooks, verify, refund) |
| Frontend | React, Tailwind CSS, Recharts |
| Testing | pytest + httpx |




