<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Souptik Chakraborty</title>
    <description>The latest articles on DEV Community by Souptik Chakraborty (@souptik96).</description>
    <link>https://dev.to/souptik96</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3921021%2F2b0f4fac-6944-4a81-8c7a-d9395cc002f4.jpeg</url>
      <title>DEV Community: Souptik Chakraborty</title>
      <link>https://dev.to/souptik96</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/souptik96"/>
    <language>en</language>
    <item>
      <title>ML fraud detection platform using AI agents</title>
      <dc:creator>Souptik Chakraborty</dc:creator>
      <pubDate>Tue, 12 May 2026 15:01:48 +0000</pubDate>
      <link>https://dev.to/souptik96/ml-fraud-detection-platform-using-ai-agents-39k8</link>
      <guid>https://dev.to/souptik96/ml-fraud-detection-platform-using-ai-agents-39k8</guid>
      <description>&lt;h2&gt;
  
  
  I built a production ML fraud detection platform using AI agents. Here's everything
&lt;/h2&gt;

&lt;p&gt;Few months ago I had an idea for an open-source fraud detection platform.&lt;/p&gt;

&lt;p&gt;I had no engineering team. I had no budget. And I cannot write production Python.&lt;/p&gt;

&lt;p&gt;Today, RiskOS is live. Four ML services, real APIs, open source, MIT licensed. You can call the fraud detection endpoint right now with no signup.&lt;/p&gt;

&lt;p&gt;This post is the honest account of how I built it, what broke badly, and the exact prompting patterns that finally worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who I Am (Context Matters)
&lt;/h2&gt;

&lt;p&gt;I'm Souptik Chakraborty — an AI Product Manager based in Kolkata, India. My background is product strategy, not engineering. I can read code well enough to understand what it does. I cannot write it reliably enough to ship to production.&lt;/p&gt;

&lt;p&gt;I used three AI tools as my engineering team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; (Anthropic) — architecture, reasoning, debugging strategy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex&lt;/strong&gt; (OpenAI) — implementation execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; via Antigravity — deployment and infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My job was to write prompts. Their job was to write code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;RiskOS is four composable services, each independently deployable, all with FastAPI backends running alongside Gradio on HuggingFace Spaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Fraud Intelligence Suite
&lt;/h3&gt;

&lt;p&gt;Five agents in one space: transaction fraud scoring (XGBoost), credit risk assessment (LightGBM), KYC identity anomaly detection (IsolationForest), sanctions and PEP screening (RapidFuzz), and an LLM-backed risk consultant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics on synthetic test set:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recall: ≥ 88%&lt;/li&gt;
&lt;li&gt;Precision: ≥ 70%&lt;/li&gt;
&lt;li&gt;Inference latency: ~55ms on CPU&lt;/li&gt;
&lt;li&gt;SHAP interpretability on every prediction&lt;/li&gt;
&lt;li&gt;Drift detection on out-of-distribution inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/soupstick/fraud-detector-app" rel="noopener noreferrer"&gt;https://huggingface.co/spaces/soupstick/fraud-detector-app&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚡ Risk Pipeline
&lt;/h3&gt;

&lt;p&gt;LightGBM scorer combined with a 15-rule decision engine. Ingests batches of up to 500 transactions, scores each one, applies rules, and triages into ESCALATE / MONITOR / AUTO_CLOSE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AUC-ROC: ≥ 0.88&lt;/li&gt;
&lt;li&gt;Workload reduction on test set: ~70%&lt;/li&gt;
&lt;li&gt;Processing time: &amp;lt;5 seconds for 100 transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/soupstick/risk-pipeline" rel="noopener noreferrer"&gt;https://huggingface.co/spaces/soupstick/risk-pipeline&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🛡️ LLM Guard
&lt;/h3&gt;

&lt;p&gt;RAG-augmented guardrail layer using LangChain and Opik. Evaluates LLM outputs against policy documents. Blocks jailbreaks, prompt injections, PII leakage, and social engineering scripts. Every call logged to Opik for audit trails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics on adversarial test set:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Block rate on unsafe inputs: ~94%&lt;/li&gt;
&lt;li&gt;Safe pass-through rate: &amp;gt;95% (no over-blocking)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/soupstick/opik_guard_v1" rel="noopener noreferrer"&gt;https://huggingface.co/spaces/soupstick/opik_guard_v1&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  📊 Marketplace Intelligence
&lt;/h3&gt;

&lt;p&gt;Natural language to SQL to Plotly chart. Ask a question in plain English, get structured results and a visualization. SELECT-only enforcement via sqlglot — blocks DROP, DELETE, INSERT, UPDATE, PRAGMA, and ATTACH before execution. 15,000-row SQLite database seeded with realistic e-commerce patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://huggingface.co/spaces/soupstick/marketplace-intelligence" rel="noopener noreferrer"&gt;https://huggingface.co/spaces/soupstick/marketplace-intelligence&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Four Services Instead of One
&lt;/h2&gt;

&lt;p&gt;This was the first major architectural decision I had to make, and it shaped everything else.&lt;/p&gt;

&lt;p&gt;The initial instinct was to build one monolithic service. One API, one model, one space. Simpler to explain, simpler to deploy.&lt;/p&gt;

&lt;p&gt;I pushed back on that for three reasons, which I had Claude reason through with me explicitly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start isolation.&lt;/strong&gt; HuggingFace free tier spaces sleep after inactivity. If one service is sleeping, it should not block the others. Four spaces means four independent cold starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independent deployability.&lt;/strong&gt; A bug in the SQL safety layer of the marketplace service should not require redeploying the fraud model. Separate repos, separate deployments, separate failure domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The FastAPI/Gradio port constraint.&lt;/strong&gt; HuggingFace Spaces exposes exactly one port (7860). Mounting FastAPI alongside Gradio on a single port requires a specific pattern — &lt;code&gt;gr.mount_gradio_app(fastapi_app, gradio_app, path="/")&lt;/code&gt;. One monolith would mean one complex routing layer. Four services means four clean implementations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prompting Workflow
&lt;/h2&gt;

&lt;p&gt;I did not write a single line of Python. Here is what I actually did.&lt;/p&gt;

&lt;p&gt;For each component, I wrote a prompt that included:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What the system does&lt;/strong&gt; — in plain English, no jargon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exact output schema&lt;/strong&gt; — the precise JSON structure the API must return&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success metrics with numeric thresholds&lt;/strong&gt; — "recall &amp;gt;= 0.88, AUC &amp;gt;= 0.82, latency &amp;lt; 200ms"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure modes to explicitly guard against&lt;/strong&gt; — "do not fabricate model artifacts, do not write tests that only validate synthetic data against synthetic models"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A test gate&lt;/strong&gt; — "the agent must not push until all tests pass against the live HF Space URL, not the local mock"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last constraint — tests against the live URL — was the single most important thing I learned. Without it, the AI will validate its own work against its own synthetic data and report success. It is circular and it is invisible until something breaks in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Prompt That Worked
&lt;/h3&gt;

&lt;p&gt;Here is the actual prompt structure I used for the XGBoost fraud model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Train a LightGBM classifier on data/train.csv.

Minimum performance thresholds — raise an exception and do not save
the model if any of these are not met:
- Recall on test set &amp;gt;= 0.88
- Precision &amp;gt;= 0.70  
- AUC-ROC &amp;gt;= 0.82

The model must be saved via model.booster_.save_model() to
model_artifacts/risk_lgbm.txt — not serialized via pickle.

Write the training script to model_artifacts/metadata.json with:
version, training date, actual recall, actual precision, actual AUC.

If the thresholds are not met, do not adjust the thresholds.
Fix the model. Tune class_weight, learning_rate, or n_estimators.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key phrase: "do not adjust the thresholds. Fix the model." Without that, the agent lowers the bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke (The Honest Part)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fabricated Model Artifacts
&lt;/h3&gt;

&lt;p&gt;The first time I ran Codex on the fraud detector, it produced a file called &lt;code&gt;fraud_xgb.json&lt;/code&gt; and reported that the model had 1.0 recall on the test set.&lt;/p&gt;

&lt;p&gt;It had not trained a model. It had written a JSON file by hand that looked like an XGBoost model. The "test set" it validated against was 50 rows of synthetic data it had generated itself. The recall was 1.0 because the model was perfectly overfit to data it had invented.&lt;/p&gt;

&lt;p&gt;I caught this because I required tests to hit the live HF Space API. When the live endpoint returned wrong predictions on known-fraud transactions, I investigated and found the artifact was fabricated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; I added this line to every training prompt: &lt;em&gt;"The model must be trained by calling model.fit() on real data from the CSV. Do not write model artifacts by hand. If a .json or .pkl file already exists, delete it and retrain."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The SQL Security Silent Failure
&lt;/h3&gt;

&lt;p&gt;I built a 10-test SQL security suite. Six tests failed in production — DROP TABLE, DELETE, INSERT, UPDATE, PRAGMA, ATTACH DATABASE were all passing through without being blocked.&lt;/p&gt;

&lt;p&gt;The sql_validator.py file existed. The logic looked correct. The problem: the module was imported at the top level, failing on a version conflict with sqlglot, and the &lt;code&gt;except ImportError&lt;/code&gt; block was catching the failure silently and proceeding with an unprotected execution path.&lt;/p&gt;

&lt;p&gt;Every test I ran locally passed because locally, sqlglot was installed. On HF Spaces, a dependency conflict on the first import caused the validator to silently not load, and queries went directly to SQLite execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; I replaced the validator entirely with a first-token whitelist approach that has zero external dependencies — pure Python string operations, no sqlglot at module level. If the first token of the SQL is not SELECT, the query is blocked. No imports that can fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Suites That Validated Themselves
&lt;/h3&gt;

&lt;p&gt;Three times I received "all tests pass" reports where the agent had written the test data, the training data, and the model in the same session. The tests passed because everything was internally consistent — not because the system worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Test data must come from a different source than training data. I started requiring agents to generate test fixtures first, commit them, and then train models — so the test data was frozen before the model saw any data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Decision I Got Right
&lt;/h2&gt;

&lt;p&gt;Running FastAPI alongside Gradio on HuggingFace Spaces.&lt;/p&gt;

&lt;p&gt;HF exposes one port. Gradio wants that port. FastAPI wants that port. The solution is to mount Gradio as a sub-application inside FastAPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gradio&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;fastapi_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# ... define all FastAPI routes ...
&lt;/span&gt;
&lt;span class="n"&gt;gradio_interface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# ... build Gradio UI ...
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount_gradio_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fastapi_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gradio_interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes Gradio serve on &lt;code&gt;/&lt;/code&gt; and FastAPI serve on &lt;code&gt;/api/v1/*&lt;/code&gt;, all on port 7860. One Dockerfile, one exposed port, both interfaces live.&lt;/p&gt;

&lt;p&gt;I did not know this pattern existed. I described the constraint to Claude — "HF only exposes port 7860, I need both a UI and an API" — and it found this solution in the Gradio docs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned About Being an AI Product Manager
&lt;/h2&gt;

&lt;p&gt;The job is not prompting. The job is knowing what good looks like.&lt;/p&gt;

&lt;p&gt;Anyone can ask Claude to "build a fraud detection system." Getting production-quality output requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specifying exact success criteria&lt;/strong&gt; — not "good accuracy" but "recall &amp;gt;= 0.88 on a 15% fraud-rate test set"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specifying failure modes explicitly&lt;/strong&gt; — if you do not tell the AI what not to do, it will do it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial validation&lt;/strong&gt; — test suites must be designed to catch the AI's blind spots, not confirm its assumptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading output critically&lt;/strong&gt; — confident, well-structured code can be completely broken. You have to know enough to notice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is where being a PM actually helps. PMs are trained to ask "what could go wrong" and "how do we know this is working." Those are the same questions that produce good prompts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Live System
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://souptik-aipm.vercel.app" rel="noopener noreferrer"&gt;https://souptik-aipm.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try the fraud API right now — no signup, no key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://soupstick-fraud-detector-app.hf.space/api/v1/fraud/predict &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "transaction_id": "devto-test",
    "amount": 9500,
    "hour_of_day": 3,
    "is_international": true,
    "merchant_category": "electronics",
    "transaction_velocity_1h": 8,
    "amount_vs_avg_ratio": 4.5,
    "is_new_device": true,
    "distance_from_home_km": 650,
    "failed_attempts_before": 2,
    "account_age_days": 15
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected response: &lt;code&gt;"verdict": "FRAUD"&lt;/code&gt; in under 60 seconds (first call may hit a cold start).&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Souptik96/riskos" rel="noopener noreferrer"&gt;https://github.com/Souptik96/riskos&lt;/a&gt;&lt;br&gt;
HuggingFace: &lt;a href="https://huggingface.co/soupstick" rel="noopener noreferrer"&gt;https://huggingface.co/soupstick&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  One Honest Limitation
&lt;/h2&gt;

&lt;p&gt;All models are trained on synthetically generated data with engineered fraud signals. They are not production-ready without retraining on real labeled data from a live system. The metrics in this post reflect performance on held-out synthetic test sets. I have stated this clearly in every README and I am stating it here.&lt;/p&gt;

&lt;p&gt;The value of RiskOS is the architecture, the API contracts, the test harnesses, and the build process — not the model weights. Those need to be replaced with real data before anyone puts this in front of real transactions.&lt;/p&gt;




&lt;h2&gt;
  
  
  If You're a PM Trying to Build AI Products
&lt;/h2&gt;

&lt;p&gt;The biggest unlock for me was treating Claude like a senior engineer, not a code generator. I did not say "write me a fraud model." I said "I need a fraud model that meets these specific criteria, these are the constraints, here are the failure modes I'm worried about, here is how I will know it worked."&lt;/p&gt;

&lt;p&gt;That is a product spec, not a prompt.&lt;/p&gt;

&lt;p&gt;If you're building something similar and want to compare notes, my DMs are open on LinkedIn: &lt;a href="https://www.linkedin.com/in/souptikchakraborty" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/souptikchakraborty&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if this was useful — a GitHub star means a lot: &lt;a href="https://github.com/Souptik96/riskos" rel="noopener noreferrer"&gt;https://github.com/Souptik96/riskos&lt;/a&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
