I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work

I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work)

Most AML tutorials end with a confusion matrix and a 99% accuracy score. Here's why that doesn't work — and what I built instead. I've been working in fintech compliance data for a while. The one thing I kept noticing: every "fraud detection project" on GitHub or Kaggle uses the same dataset — the UCI credit card fraud dataset from 2013. It has 284,000 rows, 30 features labeled V1-V28, and approximately zero explanatory value for anyone who wants to understand how financial crime actually works. So I built something different. The problem with the standard approach Real transaction monitoring engines don't work like Kaggle competitions. They don't take a CSV, train a model, and output a probability score. They work like this: A rule engine runs first — deterministic, auditable, regulatory-cited rules that generate alerts Those alerts get scored and triaged by risk tier An ML layer reduces false positives among the high-risk alerts ...

The $2,400 Pricing Mistake I Made on My First PWA Gig—and How I Fixed It

freelance-pricing

AI-generated illustration

Real‑World Hook

It was a rainy Thursday in Gulshan. My inbox pinged: "Need a PWA for my boutique, budget BDT 30,000, deadline 2 weeks". The client, a 28‑year‑old fashion retailer named Riya, showed me her Shopify store loading at 7 seconds on 3G. She’d lost roughly BDT 12,000 in sales last month because customers abandoned carts. I replied, "I can make it offline‑first, under 2 seconds, for BDT 30,000." I signed the contract on Upwork, sent the first invoice, and started building.

The Hidden Problem

Most freelancers think pricing is just a math exercise: hours × rate + buffer. I did the same. I estimated 80 hours, set my hourly rate at BDT 400, added a 10% contingency, and landed on BDT 35,200. I rounded down to BDT 30,000 to look tempting. What I missed was the hidden cost of post‑launch support and the reality of Bangladeshi data plans. Riya’s customers use 3G most of the day, paying BDT 30 per GB. A PWA that still pulls a 5 MB bundle every visit eats her margin.

Technical Breakdown & Logic Flow

Step 1: Audit the existing site. I ran Lighthouse on a 3G throttling profile. Score: 42 % overall, 3 seconds First Contentful Paint, 1.8 seconds Time to Interactive. Step 2: Identify the biggest offenders. The bundle.js was 6.4 MB, images were unoptimized, service worker not caching anything. Step 3: Design a lean architecture.

  • Split code with dynamic import()
  • Compress images to WebP, serve via Cloudflare Polish
  • Use Workbox to precache only the shell (HTML, CSS, critical JS)
  • Implement background sync for cart actions

Why this over a monolithic bundle? A monolith would force every visitor to download 6 MB, regardless of connection. The split approach lets a 3G user fetch 200 KB shell, then lazily load product images when scrolling.

Code/Implementation

Below is the service worker registration and Workbox precaching logic I used. First, I explained the reasoning: I need to cache the shell on install, clean old caches on activate, and fallback to network when offline for API calls.

// sw.js – Service Worker entry point
self.addEventListener('install', event => {
  // Precache the app shell – tiny, critical assets only
  const cacheFiles = [
    '/',
    '/index.html',
    '/styles/main.css',
    '/scripts/app-shell.js',
    '/images/icon-192.png'
  ];
  event.waitUntil(
    caches.open('pwa-shell-v1').then(cache => cache.addAll(cacheFiles))
  );
});

self.addEventListener('activate', event => {
  // Delete old caches that don’t match current version
  const keepCaches = ['pwa-shell-v1'];
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!keepCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    ))
  );
});

self.addEventListener('fetch', event => {
  const {request} = event;
  // Network‑first for API calls, cache‑first for everything else
  if (request.url.includes('/api/')) {
    event.respondWith(
      fetch(request).catch(() => caches.match(request))
    );
  } else {
    event.respondWith(
      caches.match(request).then(cached => cached || fetch(request))
    );
  }
});

// Background sync for cart actions
self.addEventListener('sync', event => {
  if (event.tag === 'sync-cart') {
    event.waitUntil(syncCart());
  }
});

async function syncCart() {
  const db = await idb.openDB('cart-db', 1);
  const pending = await db.getAll('outbox');
  for (const item of pending) {
    try {
      await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify(item),
        headers: {'Content-Type': 'application/json'}
      });
      await db.delete('outbox', item.id);
    } catch (e) {
      // keep item for next sync attempt
    }
  }
}

Alternative: I could have used workbox-cli to generate this automatically, but I needed fine‑grained control over the sync tag because Riya’s cart data is sensitive and must survive intermittent connectivity.

Business Application

After deploying, the PWA loaded in 1.2 seconds on 3G, a 40 % improvement. Riya reported a 22 % lift in conversion over the next two weeks, translating to about BDT 2,600 extra revenue per day. That’s BDT 78,000 a month—more than double the price I charged.

How did the pricing mistake factor in? I had quoted BDT 30,000, but the actual effort was 95 hours (including testing on low‑end Android devices). My hourly rate stayed at BDT 400, so the true cost was BDT 38,000. I lost BDT 8,000 on paper, but the client’s revenue boost covered it within a week.

Common Pitfalls & Edge Cases

1. Assuming “offline‑first” means offline only. Users still need fresh data; neglecting a network‑first strategy for API calls leads to stale carts.
2. Ignoring CDN costs. Cloudflare’s free tier is fine for static assets, but large image libraries can blow the budget if not optimized.
3. Hard‑coding cache names. When you forget to bump the version, users keep old assets, causing UI glitches.
4. Skipping device testing. I once missed a Samsung J2 bug where IndexedDB failed on low RAM, causing the app to crash for 12 % of users.

Counterintuitive Insight

The biggest profit driver wasn’t the PWA itself but the post‑launch monitoring contract. I offered a 3‑month SLA for performance audits at BDT 5,000 per month. Riya accepted because the numbers were small, but the SLA gave me a steady cash flow and an excuse to fine‑tune the service worker every month. In hindsight, bundling a maintenance retainer into the original quote would have prevented the under‑pricing mistake.

Conclusion & CTA

Pricing a PWA project isn’t just about hours. It’s about bandwidth, local data costs, and the hidden value of ongoing support. My first mistake taught me to:

  1. Map every network‑heavy operation.
  2. Price the maintenance window up front.
  3. Validate assumptions with real‑world 3G tests.
If you’ve ever under‑priced a project, share your story in the comments. Want a checklist for PWA pricing in Bangladesh? Grab it from the resources section of aitipseveryday.com.

Comments

Popular posts from this blog

How to Use Notion to Improve Your Blog: A Step-by-Step Guide 🌱

I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work)

How to Start Freelancing with AI in 2025 for Beginners