[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
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.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.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.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
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

Related use cases