Oxwyn Studio
All field reports
23 July 2026Engineering

Geo-aware pricing with no IP lookup, no cookies, no consent banner

Showing a Texan prices in pounds sterling costs you the enquiry. Here is how we localise currency using only signals the browser already offers.

A visitor from Austin lands on your pricing page and sees £199 per month. Now they are doing mental arithmetic instead of evaluating your offer, and mental arithmetic is where purchase intent goes to die. The obvious fix is geolocation, and the obvious implementations all carry a privacy price tag. Ours carries none, because it never learns where anyone is.

Oxwynstudio.com shows Website as a Service pricing in GBP, EUR or USD depending on the visitor, and it does so with no IP lookup, no cookie, no fingerprinting and nothing stored anywhere. This article walks through the actual algorithm in production, why fixed price ladders beat live currency conversion, and where the heuristic gets it wrong on purpose.

The usual solutions and what they cost

The standard approach is IP geolocation: match the visitor's IP address against a commercial database (MaxMind and friends) and resolve it to a country. It works, mostly. It also means processing every visitor's IP address for profiling purposes, which under UK GDPR is personal data being processed, with everything that implies for your privacy notice and your legal basis. Many implementations then write the detected region into a cookie so the choice persists, which under PECR (the UK's Privacy and Electronic Communications Regulations, the law behind cookie banners) needs consent unless it is strictly necessary. A currency preference cookie arguably qualifies as strictly necessary; the IP processing pipeline behind it is harder to wave through.

There is also an operational cost: the geolocation lookup happens server-side or at the edge, which means the page is either dynamic (goodbye edge cache) or needs a header-rewriting layer. For a site that ships static HTML for performance reasons, that is a real architectural tax.

The question worth asking first: how precise does this need to be? We are choosing between three price labels, not doing fraud prevention. Wrong guesses are mildly awkward, not dangerous, and that reframing opens up a much simpler answer.

The signals the browser gives away for free

Every browser exposes two relevant facts to client-side JavaScript without any permission prompt: the interface language (navigator.language, for example en-GB or de-DE) and the IANA timezone (Intl.DateTimeFormat().resolvedOptions().timeZone, for example Europe/London or America/Chicago). Neither identifies a person. Neither is stored. Both are read in the visitor's own browser and never sent to us.

Our detectCurrency function combines them in a deliberate order. This is the real production code, lightly trimmed of comments:

export function detectCurrency(locale?: string, timeZone?: string): Currency {
  const tz = (timeZone || "").toLowerCase()
  const loc = (locale || "").toLowerCase()

  // UK first: canonical billing currency wins any ambiguity.
  if (tz === "europe/london" || loc === "en-gb" || loc.endsWith("-gb")) return "GBP"

  // The Americas, or an explicitly US locale: USD.
  if (tz.startsWith("america/") || loc.endsWith("-us") || loc === "en-us") return "USD"

  // EU member state timezones: EUR.
  const euTimezones = [
    "europe/dublin", "europe/paris", "europe/berlin", "europe/madrid",
    "europe/rome", "europe/amsterdam", "europe/brussels", "europe/vienna",
    "europe/lisbon", "europe/athens", "europe/warsaw", "europe/prague",
    // ... the full EU 27 list continues in production
  ]
  if (euTimezones.includes(tz)) return "EUR"

  // No signals at all: fall back to GBP, our canonical currency.
  if (!tz && !loc) return "GBP"

  // Everyone else: USD as the neutral international default.
  return "USD"
}

The ordering encodes policy, not just logic:

  1. UK first. A Europe/London timezone or a -GB locale returns GBP immediately, before any other rule can fire. GBP is our canonical billing currency, we are a UK-headquartered studio, and a UK visitor seeing pounds is the one case that must never be wrong.
  2. The Americas next. Any America/* timezone, or an explicit en-US locale, returns USD. This is deliberately coarse: a visitor in São Paulo or Toronto also sees USD, which for an international services quote is the least surprising of our three options.
  3. The EU 27 by enumeration. Rather than pattern-matching Europe/* (which would sweep in Zurich, Oslo and Istanbul, none of which use the euro), we keep an explicit list of EU member state capital timezones, Dublin through Nicosia. Only an exact match returns EUR.
  4. Silence means GBP. If both signals are empty, which happens with some privacy tools and unusual embedded browsers, we show the canonical GBP prices rather than guessing.
  5. Everyone else sees USD, the closest thing the world has to a neutral trade currency.

The whole thing runs client-side after the static page loads. The server never participates, the edge cache stays warm, and the default GBP render is replaced in the browser if the heuristic picks differently.

A single stream of light branching into three currency paths: gold, emerald and cyan

Fixed ladders beat live conversion

Detecting the currency is half the job. The other half is deciding what number to show, and here we made a choice that surprises people: we do not convert prices. We maintain a fixed price ladder per currency.

Our £99 plan is $129 and €119. Not $126.72 recalculated nightly from an FX feed. The mapping lives in a small override table keyed by the canonical GBP amount:

export const PRICE_OVERRIDES: Record<number, { USD: number; EUR: number }> = {
  99:  { USD: 129, EUR: 119 },
  149: { USD: 189, EUR: 179 },
  199: { USD: 249, EUR: 229 },
  399: { USD: 499, EUR: 469 },
  // setup fees and hero copy figures follow the same pattern
}

Three reasons this beats live conversion:

No daily drift. A price that was $126 on Monday and $131 on Thursday looks broken, and screenshots, cached pages and quotes sent by email all go stale the moment the rate moves. Fixed ladders mean the number a prospect saw last week is the number they see today.

Psychological pricing survives. £99 converts to something like $126.72. Nobody prices anything at $126.72. Charm prices ($129, €119) are chosen, not computed, and a conversion pipeline cannot choose.

One place to reason about margin. When we set the dollar ladder we priced in FX volatility and card processing differences once, deliberately, rather than letting a rate feed make silent margin decisions every night.

Anything not in the override table, such as arbitrary totals in our cost calculator, falls back to a plain rate multiplication and rounds to a whole unit. Marketing prices are curated; computed sums are converted. Both behaviours live in one function, so there is exactly one source of truth for every figure on the site.

The edge cases, and why we accept them

A heuristic this simple is wrong in predictable ways, and it is worth being honest about them.

VPNs do not fool it, which cuts both ways. Because we never look at the IP address, a Londoner on a US VPN still sees pounds: locale and timezone travel with the browser, not the connection. The reverse also holds: a visitor genuinely in Texas whose VPN exits in Frankfurt still sees dollars. For our purposes this is a feature. The browser's own settings are a better proxy for "what currency does this person think in" than the network path their packets took.

Travellers see their destination, mostly. A Brit in Spain whose laptop has synced to Europe/Madrid sees euros unless their locale is en-GB, in which case rule one catches them and they see pounds.

The en-US locale is everywhere. Plenty of developers and power users worldwide run their systems in en-US regardless of where they live. A Berlin developer with an en-US locale but a Europe/Berlin timezone gets EUR, because our USD locale check only fires for the exact en-US or -us suffix after the timezone has had its say for the UK, and Europe/Berlin matches the EU list. The signals disagree; the timezone, which users rarely fake, usually tells the truth.

And the stakes are low. Every wrong guess shows a real, honest price in a major currency. GBP remains the canonical billing currency either way, which we state plainly. The failure mode of IP geolocation is a privacy disclosure; the failure mode of our heuristic is a Dutch visitor occasionally seeing dollars.

Three studio wall clocks on one shelf: the same team serving three time zones and three currencies

Why there is no consent banner for any of this

The test under UK GDPR is whether personal data is processed; the test under PECR is whether information is stored on or read from the visitor's device beyond what is strictly necessary. Our approach clears both without needing a lawyer's shrug. (We are engineers, not lawyers, and this is a description of our reasoning rather than legal advice.)

Locale and timezone are read by JavaScript already running in the visitor's own browser, used in memory to pick a label, and discarded. Nothing is transmitted to us, nothing is written to storage, no identifier is created and no profile exists. There is no personal data processing to disclose and no cookie to seek consent for. The currency detection does not even appear in our cookie policy, because there is nothing to declare.

Compare the conventional stack: IP addresses processed server-side, a geolocation vendor on the data-flow map, a region cookie set on first visit. Each step is defensible, and each step is also work: privacy notice updates, vendor assessment, one more entry in the consent tooling. We would rather have the version with nothing to defend. The same default-deny instinct runs through the rest of the site, and it started here, with the realisation that the browser already volunteers everything a pricing page needs to know.

Key takeaways

  • Currency display needs a coarse guess, not surveillance: browser locale plus IANA timezone, read client-side, is accurate enough for a three-currency decision.
  • Order the rules by cost of error: the UK-first check guarantees the home market never sees a foreign currency, whatever the other signals say.
  • Enumerate the EU 27 timezones explicitly rather than matching Europe/*, or Switzerland and Norway will quietly see euros.
  • Fixed price ladders per currency beat FX conversion: no daily drift, deliberate charm pricing, and margin decisions made once by a human.
  • Because nothing is stored and no personal data leaves the browser, the whole feature needs no cookie, no consent banner and no privacy notice entry.

Frequently asked

Why not just use the Vercel or Cloudflare geolocation headers?

Edge platforms do expose a country header derived from the IP address, and it is a reasonable tool. But using it means processing IP-derived location data and, in our architecture, either making the page dynamic or adding a rewriting layer, which costs edge cacheability. Locale and timezone give us a good-enough answer entirely client-side with none of that.

What happens if the guess is wrong?

The visitor sees accurate prices in one of three major currencies, just possibly not their preferred one. GBP is the canonical billing currency regardless of what is displayed, and the quoted figures per currency are fixed, so there is never a discrepancy between what was shown and what is charged in a given currency.

Is reading the timezone considered fingerprinting?

Fingerprinting means combining many browser attributes into a persistent identifier for tracking. We read two attributes, use them in memory to pick a price label, and store nothing, so no identifier ever exists. The concern with fingerprinting is recognising a person across visits; this design cannot recognise anyone, by construction.

Why fall back to GBP rather than USD when there are no signals?

Empty signals usually mean an unusual client: strict privacy tooling, some embedded webviews, some bots. GBP is our canonical billing currency and every other figure derives from it, so the fallback shows the one set of numbers that is authoritative rather than a convenience conversion.

Put this to work

Want a system built like this behind your business?

Brief the studio