Examples & benchmarks / #15 · 2026-10-01 · Example · Hard · for geo testing, web scraping
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, all consistent with the exit country.
Run on production on 2026-10-23: ✓ 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 | Lafayette, United States | America/New_York | America/New_York | 10/1/2026, 11:30:00 AM | 1,234,567.891 | €49.90 | 1580 |
| Germany | Munich, Germany | Europe/Berlin | Europe/Berlin | 1.10.2026, 17:30:00 | 1.234.567,891 | 49,90 € | 501 |
| Japan | Kobe, Japan | Asia/Tokyo | Asia/Tokyo | 2026/10/2 0:30:00 | 1,234,567.891 | €49.90 | 425 |
From the Node.js run on 2026-10-23. 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": "Lafayette, 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": 41.5433,
"lon": -81.4838
},
"position_km_from_exit": 1580,
"isolated_world_timezone": "America/New_York"
},
{
"country": "Germany",
"locale": "de-DE",
"exit": "Munich, 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": 52.524526,
"lon": 13.410037
},
"position_km_from_exit": 501,
"isolated_world_timezone": "Europe/Berlin"
},
{
"country": "Japan",
"locale": "ja-JP",
"exit": "Kobe, 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": 35.689497,
"lon": 139.692317
},
"position_km_from_exit": 425,
"isolated_world_timezone": "Asia/Tokyo"
}
]Takeaways
- Timezone and languages are consistent: the browser timezone matches the exit country,
navigator.languagesandAccept-Languagefollowlocale, and the same instant (15:30 UTC) shows as local time — morning in New York, afternoon in Berlin, after midnight in Tokyo. localereaches JavaScript's formats too. Withlocale: "de-DE"the German persona formats1.234.567,891and1.10.2026, 17:30:00, and the Japanese one2026/10/2 0:30:00, matching their languages. Measured 2026-10-23 on the fleet's current Camoufox 156 build (the official Camoufox library); the first 156 build, live 2026-10-21 to 2026-10-22, keptIntlonen-USwhatever the locale.- The old trap is gone on 156: on 152 a default
page.evaluatereportedUTCfor all three (Playwright's isolated world wasn't patched); on 156 it reports the persona's timezone too.main_world_evalwith the"mw:"prefix is still the way to read exactly what the site's scripts see. - 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 euro price is only formatted, never converted. It's
€49.90for the US and Japanese personas and49,90 €for the German one.