[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build Multi-Currency Pricing

Build Multi-Currency Pricing

Build a multi-currency pricing system with real-time exchange rates, localized pricing, currency-specific rounding rules, Stripe multi-currency checkout, and revenue reporting in base currency.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Yuki leads product at a 25-person SaaS selling globally. All prices are in USD. European customers pay 5-8% extra in conversion fees. Japanese customers see "$79/mo" and have no idea what that is in yen. When they tried manual pricing per currency, exchange rate changes made some currencies 20% cheaper — customers gamed it by switching regions. They need localized pricing that updates with exchange rates, uses psychologically rounded prices (€49 not €48.73), and reports revenue in their base currency.

Step 1: Build the Multi-Currency Engine

typescript
// src/pricing/currency.ts — Multi-currency with exchange rates and localized pricing
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

const BASE_CURRENCY = "USD";
const SUPPORTED_CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "SEK", "NOK", "DKK", "PLN", "BRL", "MXN", "INR", "SGD"];

// Rounding rules per currency
const ROUNDING_RULES: Record<string, { precision: number; strategy: "psychological" | "nearest" }> = {
  USD: { precision: 99, strategy: "psychological" },  // $49.99, $79.99
  EUR: { precision: 99, strategy: "psychological" },
  GBP: { precision: 99, strategy: "psychological" },
  JPY: { precision: 0, strategy: "nearest" },          // ¥7,900 (no decimals)
  SEK: { precision: 0, strategy: "nearest" },           // 499 kr
  INR: { precision: 0, strategy: "nearest" },           // ₹5,999
  BRL: { precision: 90, strategy: "psychological" },    // R$249,90
  default: { precision: 99, strategy: "psychological" },
};

// Country → Currency mapping
const COUNTRY_CURRENCY: Record<string, string> = {
  US: "USD", CA: "CAD", GB: "GBP", DE: "EUR", FR: "EUR", IT: "EUR", ES: "EUR",
  NL: "EUR", BE: "EUR", AT: "EUR", JP: "JPY", AU: "AUD", CH: "CHF",
  SE: "SEK", NO: "NOK", DK: "DKK", PL: "PLN", BR: "BRL", MX: "MXN",
  IN: "INR", SG: "SGD",
};

interface LocalizedPrice {
  amount: number;
  currency: string;
  formatted: string;
  baseAmount: number;
  baseCurrency: string;
  exchangeRate: number;
}

// Fetch and cache exchange rates (from free API)
export async function updateExchangeRates(): Promise<void> {
  const response = await fetch(`https://api.exchangerate-api.com/v4/latest/${BASE_CURRENCY}`);
  const data = await response.json();

  for (const [currency, rate] of Object.entries(data.rates as Record<string, number>)) {
    if (SUPPORTED_CURRENCIES.includes(currency)) {
      await redis.hset("exchange_rates", currency, String(rate));
    }
  }
  await redis.set("exchange_rates:updated", new Date().toISOString());
}

async function getRate(currency: string): Promise<number> {
  if (currency === BASE_CURRENCY) return 1;

  const cached = await redis.hget("exchange_rates", currency);
  if (cached) return parseFloat(cached);

  // Fallback: update rates
  await updateExchangeRates();
  const rate = await redis.hget("exchange_rates", currency);
  return rate ? parseFloat(rate) : 1;
}

// Get localized price
export async function getLocalizedPrice(
  baseAmountCents: number,
  targetCurrency: string,
  options?: { override?: number }  // manual price override
): Promise<LocalizedPrice> {
  // Check for manual price override (for strategic pricing)
  if (options?.override) {
    return {
      amount: options.override,
      currency: targetCurrency,
      formatted: formatCurrency(options.override, targetCurrency),
      baseAmount: baseAmountCents,
      baseCurrency: BASE_CURRENCY,
      exchangeRate: options.override / baseAmountCents,
    };
  }

  const rate = await getRate(targetCurrency);
  const converted = baseAmountCents * rate;
  const rounded = applyRounding(converted, targetCurrency);

  return {
    amount: rounded,
    currency: targetCurrency,
    formatted: formatCurrency(rounded, targetCurrency),
    baseAmount: baseAmountCents,
    baseCurrency: BASE_CURRENCY,
    exchangeRate: rate,
  };
}

// Get all plan prices in a currency
export async function getPlanPrices(targetCurrency: string): Promise<Array<{
  planId: string;
  name: string;
  monthly: LocalizedPrice;
  annual: LocalizedPrice;
  savings: string;
}>> {
  const plans = [
    { id: "starter", name: "Starter", monthly: 2900, annual: 29000 },
    { id: "pro", name: "Pro", monthly: 7900, annual: 79000 },
    { id: "enterprise", name: "Enterprise", monthly: 19900, annual: 199000 },
  ];

  // Check for strategic price overrides
  const overrides = await redis.hgetall(`price_overrides:${targetCurrency}`);

  return Promise.all(plans.map(async (plan) => {
    const monthlyOverride = overrides[`${plan.id}:monthly`] ? parseInt(overrides[`${plan.id}:monthly`]) : undefined;
    const annualOverride = overrides[`${plan.id}:annual`] ? parseInt(overrides[`${plan.id}:annual`]) : undefined;

    const monthly = await getLocalizedPrice(plan.monthly, targetCurrency, { override: monthlyOverride });
    const annual = await getLocalizedPrice(plan.annual, targetCurrency, { override: annualOverride });

    const monthlyCostFromAnnual = annual.amount / 12;
    const savingsPercent = Math.round((1 - monthlyCostFromAnnual / monthly.amount) * 100);

    return {
      planId: plan.id,
      name: plan.name,
      monthly,
      annual,
      savings: `${savingsPercent}%`,
    };
  }));
}

// Apply currency-specific rounding
function applyRounding(amount: number, currency: string): number {
  const rules = ROUNDING_RULES[currency] || ROUNDING_RULES.default;

  if (rules.strategy === "psychological") {
    // Round to nearest X.99 or X.90
    const whole = Math.round(amount / 100) * 100;
    return whole - (100 - rules.precision); // e.g., 5000 - 1 = 4999
  }

  // Nearest round number
  if (currency === "JPY" || currency === "SEK" || currency === "INR") {
    // Round to nearest 100
    return Math.round(amount / 100) * 100;
  }

  return Math.round(amount);
}

// Format currency for display
function formatCurrency(amountCents: number, currency: string): string {
  const symbols: Record<string, { symbol: string; position: "before" | "after"; decimals: number }> = {
    USD: { symbol: "$", position: "before", decimals: 2 },
    EUR: { symbol: "€", position: "before", decimals: 2 },
    GBP: { symbol: "£", position: "before", decimals: 2 },
    JPY: { symbol: "¥", position: "before", decimals: 0 },
    CAD: { symbol: "CA$", position: "before", decimals: 2 },
    AUD: { symbol: "A$", position: "before", decimals: 2 },
    CHF: { symbol: "CHF", position: "before", decimals: 2 },
    SEK: { symbol: "kr", position: "after", decimals: 0 },
    BRL: { symbol: "R$", position: "before", decimals: 2 },
    INR: { symbol: "₹", position: "before", decimals: 0 },
  };

  const config = symbols[currency] || { symbol: currency, position: "before", decimals: 2 };
  const value = config.decimals > 0 ? (amountCents / 100).toFixed(config.decimals) : String(Math.round(amountCents / 100));

  const formatted = Number(value).toLocaleString("en-US", {
    minimumFractionDigits: config.decimals,
    maximumFractionDigits: config.decimals,
  });

  return config.position === "before" ? `${config.symbol}${formatted}` : `${formatted} ${config.symbol}`;
}

// Convert revenue to base currency for reporting
export async function convertToBase(amount: number, fromCurrency: string): Promise<number> {
  const rate = await getRate(fromCurrency);
  return Math.round(amount / rate);
}

// Detect currency from country
export function detectCurrency(countryCode: string): string {
  return COUNTRY_CURRENCY[countryCode] || "USD";
}

Results

  • European conversion fees eliminated — customers pay in EUR directly; Stripe multi-currency handles settlement; no 5-8% bank conversion fees
  • Japanese signups up 40% — seeing ¥7,900/mo instead of $79/mo removes friction; price "feels" local
  • Psychological pricing maintained across currencies — $79.99 becomes €74.99 (not €74.37); ¥7,900 (not ¥7,847); prices feel intentional
  • Exchange rate gaming prevented — rates update daily; strategic price overrides lock pricing in key markets; switching regions doesn't give unfair discounts
  • Revenue reporting accurate — all transactions convert to USD at the rate used at purchase time; monthly reports show true USD revenue regardless of payment currency