DEV Community

Cover image for How I Cut Chargebacks by 40% With 15 Lines of Code at Checkout
Data Sentry
Data Sentry

Posted on

How I Cut Chargebacks by 40% With 15 Lines of Code at Checkout

Chargebacks were killing my margins.
Not in a dramatic way. It was slow and invisible — a disputed charge here, a stolen card there. By the time I actually sat down and looked at the numbers, I was losing somewhere between $300 and $500 a month to fraud I had zero visibility into. My Stripe risk score was creeping up. At a certain threshold Stripe starts flagging your account as high-risk and at that point it doesn't matter how good your product is.

The fix ended up being embarrassingly simple. One check, before checkout completes, that runs the buyer's IP and asks three questions:

  • Is this a VPN or proxy?
  • Is this a TOR exit node?
  • Does the IP country match the billing country?

If any of those come back suspicious, the transaction gets flagged for manual review instead of auto-completing. That's it. No ML model, no complex risk engine, no third party fraud suite charging you $200 a month.

Here's exactly how I built it.


The Stack

  • Vercel Edge Functions — runs the check at the edge before checkout completes, adds about 12ms latency
  • DataSentry — IP intelligence API for geolocation, VPN/proxy/TOR detection
  • Supabase — logs every flagged transaction for review

Step 1: The Edge Function

Create a new file at /api/checkout-guard.ts in your Next.js project:

import { NextRequest, NextResponse } from 'next/server'

export const config = {
  runtime: 'edge',
}

interface GeoResponse {
  country_code: string
  is_vpn: boolean
  is_proxy: boolean
  is_tor: boolean
  threat_score: number
}

export default async function handler(req: NextRequest) {
  const { billingCountry, cartTotal } = await req.json()

  // Get the real IP, works behind Vercel's proxy
  const ip =
    req.headers.get('x-forwarded-for')?.split(',')[0] ??
    req.headers.get('x-real-ip') ??
    '127.0.0.1'

  // Skip check for local dev
  if (ip === '127.0.0.1' || ip === '::1') {
    return NextResponse.json({ allow: true })
  }

  const geoRes = await fetch(
    `https://api.datasentry.site/v1/geo?ip=${ip}&api_key=${process.env.DATASENTRY_API_KEY}`
  )
  const geo: GeoResponse = await geoRes.json()

  const flags = {
    vpn: geo.is_vpn,
    proxy: geo.is_proxy,
    tor: geo.is_tor,
    countryMismatch: geo.country_code !== billingCountry,
    highThreat: geo.threat_score > 70,
  }

  const flagged = Object.values(flags).some(Boolean)

  // Log flagged transactions to Supabase for manual review
  if (flagged) {
    await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/flagged_transactions`, {
      method: 'POST',
      headers: {
        apikey: process.env.SUPABASE_SERVICE_KEY!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ip,
        flags,
        billing_country: billingCountry,
        ip_country: geo.country_code,
        cart_total: cartTotal,
        threat_score: geo.threat_score,
        created_at: new Date().toISOString(),
      }),
    })
  }

  return NextResponse.json({
    allow: !flagged,
    flags: flagged ? flags : null,
  })
}
Enter fullscreen mode Exit fullscreen mode

Step 2: The Supabase Table

Run this in your Supabase SQL editor:

create table flagged_transactions (
  id uuid default gen_random_uuid() primary key,
  ip text not null,
  flags jsonb not null,
  billing_country text,
  ip_country text,
  cart_total numeric,
  threat_score integer,
  reviewed boolean default false,
  created_at timestamptz default now()
);

-- Index for fast lookups on review dashboard
create index on flagged_transactions (reviewed, created_at desc);
Enter fullscreen mode Exit fullscreen mode

Step 3: Call It From Your Checkout

Before you call Stripe's confirmPayment, hit the guard first:

async function handleCheckout(billingCountry: string, cartTotal: number) {
  const guard = await fetch('/api/checkout-guard', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ billingCountry, cartTotal }),
  })

  const { allow, flags } = await guard.json()

  if (!allow) {
    // Don't hard block — show a verification step instead
    // Hard blocking legitimate users who use VPNs for privacy costs you sales
    setCheckoutState('requires_verification')
    setFraudFlags(flags)
    return
  }

  // Proceed to Stripe
  const { error } = await stripe.confirmPayment({ ... })
}
Enter fullscreen mode Exit fullscreen mode

Important: Don't hard block flagged transactions. A lot of legitimate users browse with VPNs for privacy reasons. Flag them for manual review or add a verification step like requiring a phone number. Hard blocking costs you real sales.


Step 4: The Review Dashboard (Optional but Worth It)

A simple Supabase query to pull your review queue:

const { data: flagged } = await supabase
  .from('flagged_transactions')
  .select('*')
  .eq('reviewed', false)
  .order('created_at', { ascending: false })
Enter fullscreen mode Exit fullscreen mode

Render it however you want. Even a basic table where you can mark transactions as approved or rejected is enough. You'll spend maybe 5 minutes a day on it and it saves you hundreds in chargebacks.


The Results

After running this for 90 days:

  • Chargebacks dropped ~40%
  • Stripe risk score recovered to normal
  • Zero legitimate customers were hard blocked because of the verification step instead of hard block approach
  • Total latency added to checkout: ~12ms at the edge

The whole thing is about 15 lines of actual logic. The rest is just types and Supabase boilerplate.


What This Doesn't Solve

IP geolocation is one signal, not a complete fraud solution. It won't catch:

  • Stolen cards used from a legitimate residential IP in the correct country
  • Friendly fraud (buyer disputes a legitimate charge)
  • Account takeovers where the attacker is in the same country

For those you need additional signals — device fingerprinting, velocity checks, purchase history. But for stopping the most obvious fraud patterns, an IP check at checkout is the highest ROI thing you can add in an afternoon.


If you're doing any real transaction volume and not running something like this you're probably leaking more than you think. The check costs fractions of a cent per transaction. The chargeback it prevents costs you $15+ in fees plus the lost revenue plus the Stripe risk score damage.

Happy to answer questions in the comments if anything is unclear.

Top comments (0)