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
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.
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.
Prerequisites
Before starting, ensure you have:
| Requirement | Why Needed |
|---|---|
| Node.js 18+ | Backend API routes |
| Stripe account | Payment processing |
| Basic React/Next.js knowledge | Frontend components |
| Access to your codebase | Implementation |
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' }
}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
}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
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.