Cases / #15 · 2026-10-01 · Hard
Three countries, one script — and reading values the way the site does
Camoufox personas for the US, Germany and Japan: timezone, language, local dates and numbers, geolocation. Plus the Playwright trap that reports UTC for all of them.
Run on production on 2026-10-01: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Prices, search results and availability change by country, so you run the same flow as a visitor from several countries. Each visit has to be consistent — IP, timezone, language and formats from the same country — or the site notices. And when you check that from your script, you have to read the values the way the site does.
What we used, and why
| What | Why |
|---|---|
PROXY_URL_US, _DE, _JP | Exits in each country (most providers take a country in the proxy username). |
locale | Per country: en-US, de-DE, ja-JP — languages, Accept-Language, and number/date formats. |
geoip: true | Camoufox sets the timezone and geolocation from the proxy's exit IP at launch. |
main_world_eval: true + "mw:" | Runs the check in the page's own JavaScript world — the world the site's scripts see. |
context.grantPermissions(['geolocation']) | As if the visitor allowed location access. |
How it works
- Launch one Camoufox session per country with that country's exit and locale, all at once.
- Look up the exit IP's city, timezone and coordinates.
- In the page's main world, read languages, timezone, a fixed UTC instant formatted locally, a number, a price and the geolocation.
- For contrast, read the timezone again with a default
page.evaluate.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000PROXY_URL_DE— a proxy exit in GermanyPROXY_URL_US— a proxy exit in the United StatesPROXY_URL_JP— a proxy exit in Japan
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import { firefox } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const COUNTRIES = [
{ country: 'United States', locale: 'en-US', proxy: process.env.PROXY_URL_US },
{ country: 'Germany', locale: 'de-DE', proxy: process.env.PROXY_URL_DE },
{ country: 'Japan', locale: 'ja-JP', proxy: process.env.PROXY_URL_JP },
];
// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const LOCAL_VIEW = `(async () => {
const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
(p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
return {
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
number: (1234567.891).toLocaleString(),
price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
position,
};
})()`;
// Great-circle distance in km.
const km = (a, b) => {
const r = (d) => (d * Math.PI) / 180;
const h = Math.sin(r(b.lat - a.lat) / 2) ** 2 + Math.cos(r(a.lat)) * Math.cos(r(b.lat)) * Math.sin(r(b.lon - a.lon) / 2) ** 2;
return Math.round(12742 * Math.asin(Math.sqrt(h)));
};
async function persona({ country, locale, proxy }) {
const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
// geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
body: JSON.stringify({ proxy, headless: true, os: 'windows', locale, geoip: true, main_world_eval: true }),
});
if (!res.ok) return { country, error: `launch ${res.status} ${await res.text()}` };
const { wsUrl } = await res.json();
const browser = await firefox.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
const context = await browser.newContext();
await context.grantPermissions(['geolocation']); // as if the visitor clicked "Allow"
const page = await context.newPage();
const exit = await (await page.goto('http://ip-api.com/json/?fields=country,city,timezone,lat,lon', { timeout: 60000 })).json();
await page.goto('https://httpbin.org/html', { timeout: 60000 });
const seen = await page.evaluate(`mw:${LOCAL_VIEW}`);
const isolatedTimezone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
return {
country, locale,
exit: `${exit.city}, ${exit.country}`,
exit_timezone: exit.timezone,
...seen,
position_km_from_exit: seen.position ? km(seen.position, exit) : null,
isolated_world_timezone: isolatedTimezone, // what a default page.evaluate would have reported
};
} finally {
await browser.close();
}
}
console.log(JSON.stringify(await Promise.all(COUNTRIES.map(persona)), null, 2));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import json
import math
import os
from concurrent.futures import ThreadPoolExecutor
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
COUNTRIES = [
{"country": "United States", "locale": "en-US", "proxy": os.environ["PROXY_URL_US"]},
{"country": "Germany", "locale": "de-DE", "proxy": os.environ["PROXY_URL_DE"]},
{"country": "Japan", "locale": "ja-JP", "proxy": os.environ["PROXY_URL_JP"]},
]
# What a localizing site reads in the page. Run in the PAGE's own JavaScript world
# ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
# the same way and can report the server's UTC timezone instead of the persona's.
LOCAL_VIEW = """(async () => {
const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
(p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
return {
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
number: (1234567.891).toLocaleString(),
price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
position,
};
})()"""
def km(a, b):
"""Great-circle distance in km."""
r = math.radians
h = math.sin(r(b["lat"] - a["lat"]) / 2) ** 2 + math.cos(r(a["lat"])) * math.cos(r(b["lat"])) * math.sin(r(b["lon"] - a["lon"]) / 2) ** 2
return round(12742 * math.asin(math.sqrt(h)))
def persona(c):
# geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
res = requests.post("https://starter.cdpfleet.com/camoufox/session", headers={"x-api-key": KEY}, timeout=60,
json={"proxy": c["proxy"], "headless": True, "os": "windows", "locale": c["locale"],
"geoip": True, "main_world_eval": True})
if not res.ok:
return {"country": c["country"], "error": f"launch {res.status_code} {res.text}"}
with sync_playwright() as p: # the sync API is per thread
browser = p.firefox.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
context = browser.new_context()
context.grant_permissions(["geolocation"]) # as if the visitor clicked "Allow"
page = context.new_page()
exit_ = page.goto("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", timeout=60000).json()
page.goto("https://httpbin.org/html", timeout=60000)
seen = page.evaluate(f"mw:{LOCAL_VIEW}")
isolated = page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
return {"country": c["country"], "locale": c["locale"], "exit": f"{exit_['city']}, {exit_['country']}",
"exit_timezone": exit_["timezone"], **seen,
"position_km_from_exit": km(seen["position"], exit_) if seen["position"] else None,
"isolated_world_timezone": isolated} # what a default page.evaluate would have reported
finally:
browser.close()
with ThreadPoolExecutor(len(COUNTRIES)) as pool:
print(json.dumps(list(pool.map(persona, COUNTRIES)), indent=2, ensure_ascii=False))
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
static final String LOCAL_VIEW = """
(async () => {
const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
(p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
return {
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
number: (1234567.891).toLocaleString(),
price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
position,
};
})()""";
// Great-circle distance in km.
static long km(double lat1, double lon1, double lat2, double lon2) {
double h = Math.pow(Math.sin(Math.toRadians(lat2 - lat1) / 2), 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.pow(Math.sin(Math.toRadians(lon2 - lon1) / 2), 2);
return Math.round(12742 * Math.asin(Math.sqrt(h)));
}
static JsonObject persona(String country, String locale, String proxy) throws Exception {
Gson gson = new Gson();
// geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
String body = "{\"proxy\": " + gson.toJson(proxy) + ", \"headless\": true, \"os\": \"windows\", \"locale\": " + gson.toJson(locale)
+ ", \"geoip\": true, \"main_world_eval\": true}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/camoufox/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
JsonObject out = new JsonObject();
out.addProperty("country", country);
if (res.statusCode() != 200) { out.addProperty("error", "launch " + res.statusCode() + " " + res.body()); return out; }
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) { // Playwright objects are per thread
Browser browser = playwright.firefox().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
BrowserContext context = browser.newContext();
context.grantPermissions(List.of("geolocation")); // as if the visitor clicked "Allow"
Page page = context.newPage();
JsonObject exit = JsonParser.parseString(page.navigate("http://ip-api.com/json/?fields=country,city,timezone,lat,lon",
new Page.NavigateOptions().setTimeout(60000)).text()).getAsJsonObject();
page.navigate("https://httpbin.org/html", new Page.NavigateOptions().setTimeout(60000));
JsonObject seen = gson.toJsonTree(page.evaluate("mw:" + LOCAL_VIEW)).getAsJsonObject();
Object isolated = page.evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone");
out.addProperty("locale", locale);
out.addProperty("exit", exit.get("city").getAsString() + ", " + exit.get("country").getAsString());
out.add("exit_timezone", exit.get("timezone"));
for (String k : seen.keySet()) out.add(k, seen.get(k));
JsonElement pos = seen.get("position");
out.add("position_km_from_exit", pos == null || pos.isJsonNull() ? JsonNull.INSTANCE : new JsonPrimitive(km(
pos.getAsJsonObject().get("lat").getAsDouble(), pos.getAsJsonObject().get("lon").getAsDouble(),
exit.get("lat").getAsDouble(), exit.get("lon").getAsDouble())));
out.addProperty("isolated_world_timezone", (String) isolated); // what a default page.evaluate would have reported
return out;
} finally {
browser.close();
}
}
}
public static void main(String[] args) throws Exception {
String[][] countries = {
{"United States", "en-US", System.getenv("PROXY_URL_US")},
{"Germany", "de-DE", System.getenv("PROXY_URL_DE")},
{"Japan", "ja-JP", System.getenv("PROXY_URL_JP")},
};
ExecutorService pool = Executors.newFixedThreadPool(countries.length);
List<Future<JsonObject>> jobs = new ArrayList<>();
for (String[] c : countries) jobs.add(pool.submit(() -> persona(c[0], c[1], c[2])));
JsonArray rows = new JsonArray();
for (Future<JsonObject> f : jobs) rows.add(f.get());
pool.shutdown();
System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(rows));
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
var countries = new (string Country, string Locale, string Proxy)[]
{
("United States", "en-US", Environment.GetEnvironmentVariable("PROXY_URL_US")!),
("Germany", "de-DE", Environment.GetEnvironmentVariable("PROXY_URL_DE")!),
("Japan", "ja-JP", Environment.GetEnvironmentVariable("PROXY_URL_JP")!),
};
// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const string LocalView = """
(async () => {
const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
(p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
return {
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
number: (1234567.891).toLocaleString(),
price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
position,
};
})()
""";
// Great-circle distance in km.
long Km(double lat1, double lon1, double lat2, double lon2)
{
double R(double d) => d * Math.PI / 180;
var h = Math.Pow(Math.Sin(R(lat2 - lat1) / 2), 2) + Math.Cos(R(lat1)) * Math.Cos(R(lat2)) * Math.Pow(Math.Sin(R(lon2 - lon1) / 2), 2);
return (long)Math.Round(12742 * Math.Asin(Math.Sqrt(h)));
}
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
async Task<JsonObject> Persona(string country, string locale, string proxy)
{
// geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/camoufox/session",
new { proxy, headless = true, os = "windows", locale, geoip = true, main_world_eval = true });
if (!res.IsSuccessStatusCode) return new JsonObject { ["country"] = country, ["error"] = $"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}" };
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
var browser = await playwright.Firefox.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var context = await browser.NewContextAsync();
await context.GrantPermissionsAsync(new[] { "geolocation" }); // as if the visitor clicked "Allow"
var page = await context.NewPageAsync();
var exit = JsonNode.Parse(await (await page.GotoAsync("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", new() { Timeout = 60000 }))!.TextAsync())!;
await page.GotoAsync("https://httpbin.org/html", new() { Timeout = 60000 });
var seen = JsonNode.Parse((await page.EvaluateAsync<JsonElement>("mw:" + LocalView)).GetRawText())!.AsObject();
var isolated = await page.EvaluateAsync<string>("Intl.DateTimeFormat().resolvedOptions().timeZone");
var outp = new JsonObject
{
["country"] = country, ["locale"] = locale, ["exit"] = $"{exit["city"]}, {exit["country"]}", ["exit_timezone"] = exit["timezone"]!.DeepClone(),
};
foreach (var (k, v) in seen) outp[k] = v?.DeepClone();
var pos = seen["position"];
outp["position_km_from_exit"] = pos is null ? null
: Km((double)pos["lat"]!, (double)pos["lon"]!, (double)exit["lat"]!, (double)exit["lon"]!);
outp["isolated_world_timezone"] = isolated; // what a default page.evaluate would have reported
return outp;
}
finally
{
await browser.CloseAsync();
}
}
var rows = await Task.WhenAll(countries.Select(c => Persona(c.Country, c.Locale, c.Proxy)));
Console.WriteLine(new JsonArray(rows.ToArray<JsonNode?>()).ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL_US, PROXY_URL_DE, PROXY_URL_JP (exits in each country)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"sync"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
// What a localizing site reads in the page. Run in the PAGE's own JavaScript world
// ("mw:" prefix, needs main_world_eval): Playwright's default isolated world isn't patched
// the same way and can report the server's UTC timezone instead of the persona's.
const localView = `(async () => {
const position = await new Promise((ok) => navigator.geolocation.getCurrentPosition(
(p) => ok({ lat: p.coords.latitude, lon: p.coords.longitude }), () => ok(null), { timeout: 10000 }));
return {
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
date: new Date('2026-10-01T15:30:00Z').toLocaleString(),
number: (1234567.891).toLocaleString(),
price: new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(49.9),
position,
};
})()`
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
// Great-circle distance in km.
func km(lat1, lon1, lat2, lon2 float64) int64 {
r := func(d float64) float64 { return d * math.Pi / 180 }
h := math.Pow(math.Sin(r(lat2-lat1)/2), 2) + math.Cos(r(lat1))*math.Cos(r(lat2))*math.Pow(math.Sin(r(lon2-lon1)/2), 2)
return int64(math.Round(12742 * math.Asin(math.Sqrt(h))))
}
// Evaluate returns whole numbers as int and others as float64.
func num(v any) float64 {
switch n := v.(type) {
case float64:
return n
case int:
return float64(n)
case int64:
return float64(n)
}
return 0
}
func persona(pw *playwright.Playwright, country, locale, proxy string) map[string]any {
fail := func(err error) map[string]any { return map[string]any{"country": country, "error": err.Error()} }
// geoip: Camoufox sets timezone and geolocation from the proxy's exit IP at launch.
session, err := launch("camoufox", map[string]any{"proxy": proxy, "headless": true, "os": "windows", "locale": locale, "geoip": true, "main_world_eval": true})
if err != nil {
return fail(err)
}
browser, err := pw.Firefox.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
return fail(err)
}
defer browser.Close()
context, _ := browser.NewContext()
context.GrantPermissions([]string{"geolocation"}) // as if the visitor clicked "Allow"
page, _ := context.NewPage()
res, err := page.Goto("http://ip-api.com/json/?fields=country,city,timezone,lat,lon", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
if err != nil {
return fail(err)
}
var exit struct {
Country, City, Timezone string
Lat, Lon float64
}
res.JSON(&exit)
if _, err := page.Goto("https://httpbin.org/html", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
return fail(err)
}
v, err := page.Evaluate("mw:" + localView)
if err != nil {
return fail(err)
}
isolated, _ := page.Evaluate("Intl.DateTimeFormat().resolvedOptions().timeZone")
out := v.(map[string]any)
out["country"], out["locale"], out["exit"], out["exit_timezone"] = country, locale, exit.City+", "+exit.Country, exit.Timezone
out["position_km_from_exit"] = nil
if pos, ok := out["position"].(map[string]any); ok {
out["position_km_from_exit"] = km(num(pos["lat"]), num(pos["lon"]), exit.Lat, exit.Lon)
}
out["isolated_world_timezone"] = isolated // what a default page.evaluate would have reported
return out
}
func main() {
pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
defer pw.Stop()
countries := [][3]string{
{"United States", "en-US", os.Getenv("PROXY_URL_US")},
{"Germany", "de-DE", os.Getenv("PROXY_URL_DE")},
{"Japan", "ja-JP", os.Getenv("PROXY_URL_JP")},
}
rows := make([]map[string]any, len(countries))
var wg sync.WaitGroup
for i, c := range countries {
wg.Add(1)
go func(i int, c [3]string) { defer wg.Done(); rows[i] = persona(pw, c[0], c[1], c[2]) }(i, c)
}
wg.Wait()
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(rows)
fmt.Print(buf.String())
}
What we got
| Persona | Exit | Timezone (page) | Default page.evaluate | 15:30 UTC shown as | Number | Price | Geolocation vs exit (km) |
|---|---|---|---|---|---|---|---|
| United States | Birmingham, United States | America/New_York | UTC | 10/1/2026, 11:30:00 AM | 1,234,567.891 | €49.90 | 573 |
| Germany | Berlin, Germany | Europe/Berlin | UTC | 1.10.2026, 17:30:00 | 1.234.567,891 | 49,90 € | 446 |
| Japan | Nagoya, Japan | Asia/Tokyo | UTC | 2026/10/2 0:30:00 | 1,234,567.891 | €49.90 | 138 |
From the Node.js run on 2026-10-01. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
[
{
"country": "United States",
"locale": "en-US",
"exit": "Birmingham, United States",
"exit_timezone": "America/Chicago",
"languages": [
"en-US",
"en"
],
"timezone": "America/New_York",
"date": "10/1/2026, 11:30:00 AM",
"number": "1,234,567.891",
"price": "€49.90",
"position": {
"lat": 35.1947,
"lon": -80.744
},
"position_km_from_exit": 573,
"isolated_world_timezone": "UTC"
},
{
"country": "Germany",
"locale": "de-DE",
"exit": "Berlin, Germany",
"exit_timezone": "Europe/Berlin",
"languages": [
"de-DE",
"de"
],
"timezone": "Europe/Berlin",
"date": "1.10.2026, 17:30:00",
"number": "1.234.567,891",
"price": "49,90 €",
"position": {
"lat": 51.5791,
"lon": 7.0599
},
"position_km_from_exit": 446,
"isolated_world_timezone": "UTC"
},
{
"country": "Japan",
"locale": "ja-JP",
"exit": "Nagoya, Japan",
"exit_timezone": "Asia/Tokyo",
"languages": [
"ja-JP",
"ja"
],
"timezone": "Asia/Tokyo",
"date": "2026/10/2 0:30:00",
"number": "1,234,567.891",
"price": "€49.90",
"position": {
"lat": 34.6967,
"lon": 135.5154
},
"position_km_from_exit": 138,
"isolated_world_timezone": "UTC"
}
]Takeaways
- Each persona is consistent: the browser timezone matches the exit country, and the same instant (15:30 UTC) shows as local time — morning in New York, afternoon in Berlin, after midnight in Tokyo — with local number formats.
- The trap: a default
page.evaluatein Camoufox reportedUTCfor all three. Camoufox runs Playwright's scripts in an isolated world that isn't patched like the page's own world. The site saw the right timezone from the first millisecond; only the check was wrong. Read fingerprint values withmain_world_evaland the"mw:"prefix. - Geolocation lands in the right country, not the right city: geo-IP databases disagree, and a rotating residential proxy may exit from a different IP than the one Camoufox looked up at launch. Use sticky sessions when city-level consistency matters.
- Currency is the site's choice, not the browser's: a price in euros is formatted as
€49.90or49,90 €depending on the locale — the browser never converts it.