PPP Pricing for SaaS: The Definitive Implementation Guide
Tutorials25 min read

PPP Pricing for SaaS: The Definitive Implementation Guide

Step-by-step guide to implementing purchasing power parity pricing for your SaaS product. Includes code examples and best practices.

Mantas Karmaza

Mantas Karmaza

Founder · January 1, 2024

PPP Pricing for SaaS: The Complete Implementation Guide

This comprehensive tutorial walks you through implementing Purchasing Power Parity (PPP) pricing for your SaaS product. We'll cover everything from strategy to production-ready code.

!Code Implementation

What You'll Build

By the end of this guide, you'll have:

  • Geolocation-based visitor detection
  • Dynamic pricing display
  • Secure coupon code system
  • Fraud prevention measures
  • Stripe integration for checkout
  • Analytics tracking

Time to implement: 4-8 hours (DIY) or 10 minutes (SmartBanner)

Ready to increase your international revenue?

Start your free trial and see results in days, not months.

Start Free Trial

Prerequisites

Before starting, ensure you have:

RequirementWhy Needed
Node.js 18+Backend API routes
Stripe accountPayment processing
Basic React/Next.js knowledgeFrontend components
Access to your codebaseImplementation

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                      User Flow                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Visitor arrives → Detect Location → Calculate Discount     │
│       ↓                                                     │
│  Show Banner → User Clicks → Generate Coupon                │
│       ↓                                                     │
│  Checkout with Coupon → Validate Country → Process Payment  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Step 1: Define Your Pricing Tiers

First, create your tier configuration. This is the foundation of your PPP system:

// lib/pricing-tiers.ts

export interface PricingTier {
  id: string
  name: string
  discount: number
  countries: string[]
  minPrice?: number  // Optional minimum price
}

export const pricingTiers: PricingTier[] = [
  {
    id: 'tier1',
    name: 'Full Price',
    discount: 0,
    countries: ['US', 'CH', 'NO', 'LU', 'SG', 'IE', 'DK', 'IS', 'QA', 'AE']
  },
  {
    id: 'tier2',
    name: 'Tier 2',
    discount: 15,
    countries: ['GB', 'DE', 'AU', 'CA', 'NL', 'AT', 'SE', 'FI', 'BE', 'NZ']
  },
  {
    id: 'tier3',
    name: 'Tier 3',
    discount: 25,
    countries: ['FR', 'JP', 'KR', 'IT', 'ES', 'IL', 'HK', 'TW', 'CY', 'MT']
  },
  {
    id: 'tier4',
    name: 'Tier 4',
    discount: 40,
    countries: ['PL', 'CZ', 'PT', 'GR', 'HU', 'SK', 'HR', 'RO', 'BG', 'LT', 'LV', 'EE']
  },
  {
    id: 'tier5',
    name: 'Tier 5',
    discount: 55,
    countries: ['BR', 'MX', 'TR', 'TH', 'MY', 'AR', 'CL', 'CO', 'PE', 'ZA', 'RU']
  },
  {
    id: 'tier6',
    name: 'Tier 6',
    discount: 70,
    countries: ['IN', 'ID', 'PH', 'VN', 'UA', 'EG', 'NG', 'PK', 'BD', 'KE', 'GH', 'NP', 'LK']
  }
]

export function getTierForCountry(countryCode: string): PricingTier {
  const tier = pricingTiers.find(t => t.countries.includes(countryCode))
  return tier || pricingTiers[0] // Default to full price
}

export function calculatePrice(basePrice: number, countryCode: string, minPrice = 0): number {
  const tier = getTierForCountry(countryCode)
  const discountedPrice = basePrice * (1 - tier.discount / 100)
  return Math.max(discountedPrice, minPrice)
}

Step 2: Detect Visitor Location

Create a robust geolocation service with fallbacks:

// lib/geolocation.ts

interface GeoData {
  country: string
  city?: string
  region?: string
  isVPN?: boolean
  isProxy?: boolean
}

export async function getVisitorLocation(): Promise<GeoData> {
  // Try multiple providers for reliability
  const providers = [
    fetchFromIPAPI,
    fetchFromIPInfo,
    fetchFromCloudflare
  ]

  for (const provider of providers) {
    try {
      const data = await provider()
      if (data?.country) return data
    } catch (e) {
      console.warn('Geo provider failed, trying next...')
    }
  }

  // Default fallback
  return { country: 'US' }
}

async function fetchFromIPAPI(): Promise<GeoData> {
  const res = await fetch('https://ipapi.co/json/', {
    headers: { 'User-Agent': 'YourApp/1.0' }
  })
  const data = await res.json()
  return {
    country: data.country_code,
    city: data.city,
    region: data.region,
    isVPN: data.security?.is_vpn,
    isProxy: data.security?.is_proxy
  }
}

async function fetchFromIPInfo(): Promise<GeoData> {
  const token = process.env.IPINFO_TOKEN
  const res = await fetch(`https://ipinfo.io/json?token=${token}`)
  const data = await res.json()
  return {
    country: data.country,
    city: data.city,
    region: data.region,
    isVPN: data.privacy?.vpn,
    isProxy: data.privacy?.proxy
  }
}

async function fetchFromCloudflare(): Promise<GeoData> {
  // Works if you're behind Cloudflare
  // Access via request headers: cf-ipcountry
  const res = await fetch('https://www.cloudflare.com/cdn-cgi/trace')
  const text = await res.text()
  const country = text.match(/loc=([A-Z]{2})/)?.[1]
  return { country: country || 'US' }
}

!Security

Step 3: Create the Pricing Banner Component

Build a React component that displays personalized pricing:

// components/PPPBanner.tsx

'use client'

import { useState, useEffect } from 'react'
import { getTierForCountry, calculatePrice } from '@/lib/pricing-tiers'
import { getVisitorLocation } from '@/lib/geolocation'

interface PPPBannerProps {
  basePrice: number
  productName: string
  onApplyCoupon: (couponCode: string) => void
}

const countryNames: Record<string, string> = {
  IN: 'India', BR: 'Brazil', ID: 'Indonesia', MX: 'Mexico',
  TR: 'Turkey', PH: 'Philippines', VN: 'Vietnam', // ... add more
}

export function PPPBanner({ basePrice, productName, onApplyCoupon }: PPPBannerProps) {
  const [country, setCountry] = useState<string | null>(null)
  const [tier, setTier] = useState<{ discount: number } | null>(null)
  const [couponCode, setCouponCode] = useState<string | null>(null)
  const [isVisible, setIsVisible] = useState(false)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    async function detectLocation() {
      try {
        const geo = await getVisitorLocation()
        const tierData = getTierForCountry(geo.country)

        setCountry(geo.country)
        setTier(tierData)

        // Only show banner if there's a discount
        if (tierData.discount > 0) {
          // Generate unique coupon code
          const code = await generateCouponCode(geo.country, tierData.discount)
          setCouponCode(code)
          setIsVisible(true)
        }
      } catch (e) {
        console.error('PPP detection failed:', e)
      } finally {
        setIsLoading(false)
      }
    }

    detectLocation()
  }, [])

  if (isLoading || !isVisible || !tier || !country) return null

  const discountedPrice = calculatePrice(basePrice, country)
  const savings = basePrice - discountedPrice
  const countryName = countryNames[country] || country

  return (
    <div className="fixed bottom-4 right-4 max-w-sm bg-white rounded-xl shadow-2xl border border-gray-200 p-4 z-50 animate-slide-up">
      <button
        onClick={() => setIsVisible(false)}
        className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
      >
        ✕
      </button>

      <div className="flex items-start gap-3">
        <span className="text-3xl">🎉</span>
        <div>
          <h3 className="font-bold text-gray-900">
            Special pricing for {countryName}!
          </h3>
          <p className="text-sm text-gray-600 mt-1">
            We support fair pricing based on location.
          </p>

          <div className="mt-3 flex items-baseline gap-2">
            <span className="text-2xl font-bold text-green-600">
              ${discountedPrice.toFixed(0)}
            </span>
            <span className="text-sm text-gray-400 line-through">
              ${basePrice}
            </span>
            <span className="text-sm text-green-600 font-medium">
              Save ${savings.toFixed(0)} ({tier.discount}% off)
            </span>
          </div>

          <div className="mt-3 flex items-center gap-2">
            <code className="bg-gray-100 px-3 py-1 rounded text-sm font-mono">
              {couponCode}
            </code>
            <button
              onClick={() => {
                navigator.clipboard.writeText(couponCode!)
                // Show toast notification
              }}
              className="text-sm text-blue-600 hover:underline"
            >
              Copy
            </button>
          </div>

          <button
            onClick={() => onApplyCoupon(couponCode!)}
            className="mt-3 w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition"
          >
            Apply Discount & Continue
          </button>
        </div>
      </div>
    </div>
  )
}

async function generateCouponCode(country: string, discount: number): Promise<string> {
  const res = await fetch('/api/ppp/generate-coupon', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ country, discount })
  })
  const data = await res.json()
  return data.couponCode
}

Step 4: Create Secure Coupon Generation

// app/api/ppp/generate-coupon/route.ts

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

const SECRET = process.env.PPP_SECRET! // Set this in your .env

interface CouponPayload {
  country: string
  discount: number
  timestamp: number
  sessionId: string
}

export async function POST(request: NextRequest) {
  const { country, discount } = await request.json()

  // Get or create session ID
  const sessionId = request.cookies.get('session_id')?.value ||
    crypto.randomBytes(16).toString('hex')

  // Create secure coupon code
  const payload: CouponPayload = {
    country,
    discount,
    timestamp: Date.now(),
    sessionId
  }

  const couponCode = generateSecureCoupon(payload)

  // Store in database for validation later
  await storeCoupon(couponCode, payload)

  const response = NextResponse.json({ couponCode })

  // Set session cookie if new
  if (!request.cookies.get('session_id')) {
    response.cookies.set('session_id', sessionId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 // 24 hours
    })
  }

  return response
}

function generateSecureCoupon(payload: CouponPayload): string {
  const data = JSON.stringify(payload)
  const signature = crypto
    .createHmac('sha256', SECRET)
    .update(data)
    .digest('hex')
    .slice(0, 8)
    .toUpperCase()

  return `PPP-${payload.country}-${payload.discount}-${signature}`
}

async function storeCoupon(code: string, payload: CouponPayload) {
  // Store in your database (Prisma example)
  // await prisma.pppCoupon.create({
  //   data: {
  //     code,
  //     country: payload.country,
  //     discount: payload.discount,
  //     sessionId: payload.sessionId,
  //     expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
  //   }
  // })
}

Step 5: Integrate with Stripe Checkout

// app/api/checkout/route.ts

import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { validateCoupon, validateCountry } from '@/lib/ppp-validation'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: NextRequest) {
  const { priceId, couponCode, customerCountry } = await request.json()

  // Validate coupon if provided
  let stripeCouponId: string | undefined
  if (couponCode) {
    const validation = await validateCoupon(couponCode, customerCountry)

    if (!validation.valid) {
      return NextResponse.json(
        { error: validation.error },
        { status: 400 }
      )
    }

    // Get or create Stripe coupon
    stripeCouponId = await getOrCreateStripeCoupon(validation.discount)
  }

  // Create checkout session
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    discounts: stripeCouponId ? [{ coupon: stripeCouponId }] : undefined,
    success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
    metadata: {
      ppp_coupon: couponCode || '',
      ppp_country: customerCountry || ''
    }
  })

  return NextResponse.json({ sessionId: session.id, url: session.url })
}

async function getOrCreateStripeCoupon(discount: number): Promise<string> {
  const couponId = `ppp_${discount}`

  try {
    await stripe.coupons.retrieve(couponId)
  } catch {
    // Create if doesn't exist
    await stripe.coupons.create({
      id: couponId,
      percent_off: discount,
      duration: 'forever',
      name: `PPP ${discount}% Discount`
    })
  }

  return couponId
}

!Analytics Dashboard

Step 6: Implement Fraud Prevention

// lib/ppp-validation.ts

import { getTierForCountry } from './pricing-tiers'

interface ValidationResult {
  valid: boolean
  discount?: number
  error?: string
}

export async function validateCoupon(
  couponCode: string,
  customerCountry: string
): Promise<ValidationResult> {
  // 1. Parse coupon code
  const parts = couponCode.split('-')
  if (parts.length !== 4 || parts[0] !== 'PPP') {
    return { valid: false, error: 'Invalid coupon format' }
  }

  const [, couponCountry, discountStr] = parts
  const discount = parseInt(discountStr)

  // 2. Verify country matches
  if (couponCountry !== customerCountry) {
    return {
      valid: false,
      error: 'This discount is not available for your country'
    }
  }

  // 3. Verify discount is valid for country
  const tier = getTierForCountry(customerCountry)
  if (discount !== tier.discount) {
    return { valid: false, error: 'Invalid discount amount' }
  }

  // 4. Check if coupon exists and is not used (database check)
  // const coupon = await prisma.pppCoupon.findUnique({ where: { code: couponCode } })
  // if (!coupon || coupon.usedAt) {
  //   return { valid: false, error: 'Coupon not found or already used' }
  // }

  // 5. Check if not expired
  // if (coupon.expiresAt < new Date()) {
  //   return { valid: false, error: 'Coupon has expired' }
  // }

  return { valid: true, discount }
}

export async function validateCountryAtCheckout(
  expectedCountry: string,
  cardCountry: string
): Promise<ValidationResult> {
  // Stripe provides card country from payment method
  if (expectedCountry !== cardCountry) {
    return {
      valid: false,
      error: 'Payment card country does not match discount country. Please use a card issued in your country.'
    }
  }

  return { valid: true }
}

Step 7: Add VPN Detection

// lib/vpn-detection.ts

interface VPNCheckResult {
  isVPN: boolean
  isProxy: boolean
  isTor: boolean
  confidence: number
}

export async function checkForVPN(ip: string): Promise<VPNCheckResult> {
  // Use IPInfo's privacy detection
  const token = process.env.IPINFO_TOKEN
  const res = await fetch(`https://ipinfo.io/${ip}?token=${token}`)
  const data = await res.json()

  return {
    isVPN: data.privacy?.vpn || false,
    isProxy: data.privacy?.proxy || false,
    isTor: data.privacy?.tor || false,
    confidence: data.privacy?.confidence || 0
  }
}

export function handleVPNDetection(
  vpnResult: VPNCheckResult,
  onVPNDetected: () => void
) {
  if (vpnResult.isVPN || vpnResult.isProxy || vpnResult.isTor) {
    // Option 1: Block discount entirely
    // onVPNDetected()

    // Option 2: Show warning and require card verification
    return {
      showWarning: true,
      message: 'VPN detected. Your discount will be verified at checkout based on your payment card country.'
    }
  }

  return { showWarning: false }
}

Step 8: Post-Purchase Validation (Stripe Webhook)

// app/api/webhooks/stripe/route.ts

import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { validateCountryAtCheckout } from '@/lib/ppp-validation'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!

export async function POST(request: NextRequest) {
  const payload = await request.text()
  const signature = request.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(payload, signature, webhookSecret)
  } catch (err) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session

    // Validate PPP discount if applied
    if (session.metadata?.ppp_coupon) {
      const paymentIntent = await stripe.paymentIntents.retrieve(
        session.payment_intent as string,
        { expand: ['payment_method'] }
      )

      const paymentMethod = paymentIntent.payment_method as Stripe.PaymentMethod
      const cardCountry = paymentMethod.card?.country

      const expectedCountry = session.metadata.ppp_country

      const validation = await validateCountryAtCheckout(
        expectedCountry,
        cardCountry || ''
      )

      if (!validation.valid) {
        // Flag for review or cancel subscription
        console.warn(`PPP fraud detected: expected ${expectedCountry}, got ${cardCountry}`)

        // Option: Create refund
        // await stripe.refunds.create({ payment_intent: paymentIntent.id })
      }
    }
  }

  return NextResponse.json({ received: true })
}

The Easy Way: Use SmartBanner

All of the above code? SmartBanner handles it with one line:

<script src="https://cdn.smartbanner.pro/sb.js" data-id="YOUR_ID"></script>

What SmartBanner provides:

  • ✅ Geolocation with 99.9% accuracy
  • ✅ Optimized country tiers (195 countries)
  • ✅ Beautiful, customizable banners
  • ✅ Secure coupon generation
  • ✅ VPN/Proxy detection
  • ✅ Card country verification
  • ✅ Analytics dashboard
  • ✅ A/B testing for discount levels
  • ✅ <0.1% fraud rate

Setup time: 2 minutes vs 4-8 hours DIY

Best Practices Checklist

Before Launch

  • [ ] Test with VPN from multiple countries
  • [ ] Verify coupon generation works correctly
  • [ ] Test Stripe webhook handling
  • [ ] Set up monitoring for fraud patterns

After Launch

  • [ ] Monitor conversion rates by country
  • [ ] Track fraud rate (should be <1%)
  • [ ] A/B test discount levels monthly
  • [ ] Review customer feedback by region

Common Questions

Q: Should I show the original price crossed out?

A: Yes! It increases perceived value and conversion rate by 15-20%.

Q: How do I handle existing customers?

A: Honor their original price. Never retroactively change pricing.

Q: What about annual plans?

A: Apply the same discount percentage. Annual plans already have a discount built in.

Q: Should I localize currencies?

A: Display local currency for reference, but charge in USD to avoid exchange rate complexities.

Conclusion

Implementing PPP pricing from scratch is doable but time-consuming. Whether you build it yourself or use SmartBanner, the key is:

  • **Start with data-driven tiers** - Use GDP and PPP data
  • **Prioritize fraud prevention** - VPN detection + card verification
  • **Test and iterate** - A/B test discount levels
  • **Monitor constantly** - Track conversion, revenue, and fraud

Ready to implement? Start your SmartBanner free trial and have PPP pricing live in minutes.

SmartBanner includes everything you need

Stop building regional pricing from scratch. Get started in 2 minutes.

  • Location-based pricing for 195+ countries
  • VPN/proxy fraud protection
  • 50+ automated holiday campaigns
  • A/B testing for discount optimization
  • One-line JavaScript integration
Try SmartBanner Free

Stop leaving money on the table

Join 2,847+ SaaS founders who use SmartBanner to unlock international revenue. Setup takes 2 minutes. See results in days.

No credit card required. 14-day free trial on all paid plans.