Cases / #47 · 2026-10-15 · Easy
A first free scrape on the Community plan: no proxy to bring, country and sticky exits included
On the free Community plan a launch without a proxy goes out through the plan's own shared proxy. Scrape a catalogue page, then pick a country and keep the same exit across sessions with a `cdpfleet-plan` token — and read the plan's limits from the API.
Run on production on 2026-10-15: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Every cdpfleet session goes through a proxy, and on paid plans you bring your own. On the free Community plan the proxy is included: what happens when the launch names no proxy at all, how do you choose a country or keep the same IP, and what are the plan's limits — without guessing from the docs?
What we used, and why
| What | Why |
|---|---|
GET /v1/me | The plan as the account sees it: threads, daily minutes, session length, per-session page and memory limits, and which options the included proxy supports. |
A launch without proxy | On Community the plan's included proxy is used. On a paid plan the same launch gets 400 proxy_required. |
cdpfleet-plan-country-de-session-…-lifetime-10 | A token in place of a proxy URL: exit country Germany, and the same exit for 10 minutes for every session that uses the same session name. The dashboard's Proxies → Community proxy tab builds it. |
browser.close() | Ends the session at once, so the 60 daily minutes per thread go further. |
How it works
- Read the plan from
GET /v1/me. - Launch headless Chromium without a proxy, check the exit country, and scrape the 20 books on the first page of books.toscrape.com.
- Launch twice more with the same
cdpfleet-plancountry + session token and compare the two exits.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
COMMUNITY_API_KEY— an API key of an account on the free Community plan (dashboard → API keys); its launches use the plan's included proxy
// npm install [email protected]
// env: COMMUNITY_API_KEY
import { chromium } from 'playwright';
const KEY = process.env.COMMUNITY_API_KEY;
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(body) {
for (let attempt = 1; ; attempt++) {
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify(body),
});
if (res.ok) return res.json();
if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
}
}
// The exit the site sees. Shared proxies drop the odd connection: retry a few times.
async function exitOf(page) {
for (let i = 1; ; i++) {
try {
await page.goto('http://ip-api.com/json', { timeout: 30000 });
const j = JSON.parse(await page.textContent('body'));
return { ip: j.query, country: j.countryCode };
} catch (err) {
if (i === 4) throw err;
await page.waitForTimeout(2000 * i);
}
}
}
async function session(body, work) {
const s = await launch({ headless: true, record: false, ...body });
const browser = await chromium.connect(s.wsUrl, { headers: { 'x-api-key': KEY } });
try {
return { weight: s.weight, ...(await work(await browser.newPage())) };
} finally {
await browser.close(); // ends the session and stops the clock
}
}
// 1. What the free plan includes.
const me = await (await fetch('https://cdpfleet.com/v1/me', { headers: { 'x-api-key': KEY } })).json();
const sub = me.subscription;
const plan = {
plan: sub.plan_id, threads: sub.threads, minutes_per_thread_per_day: sub.daily_minutes_per_thread,
max_session_minutes: sub.max_session_seconds / 60, max_pages: sub.features.max_pages, memory_mb: sub.features.memory_mb,
proxy_options: sub.plan_proxy?.options,
};
// 2. No proxy in the launch: the plan's included proxy is used. Scrape a catalogue page.
const first = await session({}, async (page) => {
const exit = await exitOf(page);
await page.goto('https://books.toscrape.com/', { timeout: 45000 });
const books = await page.$$eval('article.product_pod', (els) => els.map((e) => ({
title: e.querySelector('h3 a').getAttribute('title'),
price: e.querySelector('.price_color').textContent.trim(),
})));
return { exit, books: books.length, first_book: books[0] };
});
// 3. A cdpfleet-plan token picks a country and keeps the same exit across sessions.
const token = `cdpfleet-plan-country-de-session-c47${Math.random().toString(36).slice(2, 7)}-lifetime-10`;
const a = await session({ proxy: token }, async (page) => ({ exit: await exitOf(page) }));
const b = await session({ proxy: token }, async (page) => ({ exit: await exitOf(page) }));
console.log(JSON.stringify({
plan,
default_proxy: { weight: first.weight, exit_country: first.exit.country, exit_ip: first.exit.ip, books: first.books, first_book: first.first_book },
plan_token: { token: token.replace(/session-\w+/, 'session-…'), first_country: a.exit.country, second_country: b.exit.country, same_ip: a.exit.ip === b.exit.ip },
}, null, 2));
# pip install playwright==1.60.0 requests
# env: COMMUNITY_API_KEY
import json
import os
import random
import re
import string
import time
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["COMMUNITY_API_KEY"]
# 503 means the fleet is momentarily busy: wait Retry-After and try again.
def launch(body):
for attempt in range(1, 6):
r = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY}, json=body, timeout=120)
if r.ok:
return r.json()
if r.status_code != 503 or attempt == 5:
raise RuntimeError(f"launch {r.status_code} {r.text}")
time.sleep(int(r.headers.get("retry-after") or 5))
# The exit the site sees. Shared proxies drop the odd connection: retry a few times.
def exit_of(page):
for i in range(1, 5):
try:
page.goto("http://ip-api.com/json", timeout=30000)
j = json.loads(page.text_content("body"))
return {"ip": j["query"], "country": j["countryCode"]}
except Exception:
if i == 4:
raise
page.wait_for_timeout(2000 * i)
def session(pw, body, work):
s = launch({"headless": True, "record": False, **body})
browser = pw.chromium.connect(s["wsUrl"], headers={"x-api-key": KEY})
try:
return {"weight": s["weight"], **work(browser.new_page())}
finally:
browser.close() # ends the session and stops the clock
def scrape(page):
exit = exit_of(page)
page.goto("https://books.toscrape.com/", timeout=45000)
books = page.eval_on_selector_all("article.product_pod", """els => els.map(e => ({
title: e.querySelector('h3 a').getAttribute('title'),
price: e.querySelector('.price_color').textContent.trim(),
}))""")
return {"exit": exit, "books": len(books), "first_book": books[0]}
# 1. What the free plan includes.
sub = requests.get("https://cdpfleet.com/v1/me", headers={"x-api-key": KEY}, timeout=30).json()["subscription"]
plan = {
"plan": sub["plan_id"], "threads": sub["threads"], "minutes_per_thread_per_day": sub["daily_minutes_per_thread"],
"max_session_minutes": sub["max_session_seconds"] // 60, "max_pages": sub["features"]["max_pages"],
"memory_mb": sub["features"]["memory_mb"], "proxy_options": (sub.get("plan_proxy") or {}).get("options"),
}
with sync_playwright() as pw:
# 2. No proxy in the launch: the plan's included proxy is used. Scrape a catalogue page.
first = session(pw, {}, scrape)
# 3. A cdpfleet-plan token picks a country and keeps the same exit across sessions.
token = "cdpfleet-plan-country-de-session-c47" + "".join(random.choices(string.ascii_lowercase + string.digits, k=5)) + "-lifetime-10"
a = session(pw, {"proxy": token}, lambda page: {"exit": exit_of(page)})
b = session(pw, {"proxy": token}, lambda page: {"exit": exit_of(page)})
print(json.dumps({
"plan": plan,
"default_proxy": {"weight": first["weight"], "exit_country": first["exit"]["country"], "exit_ip": first["exit"]["ip"],
"books": first["books"], "first_book": first["first_book"]},
"plan_token": {"token": re.sub(r"session-\w+", "session-…", token), "first_country": a["exit"]["country"],
"second_country": b["exit"]["country"], "same_ip": a["exit"]["ip"] == b["exit"]["ip"]},
}, 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: COMMUNITY_API_KEY
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.function.Function;
public class Main {
static final String KEY = System.getenv("COMMUNITY_API_KEY");
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static final HttpClient HTTP = HttpClient.newHttpClient();
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
static JsonObject launch(JsonObject body) throws Exception {
for (int attempt = 1; ; attempt++) {
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200) return JsonParser.parseString(res.body()).getAsJsonObject();
if (res.statusCode() != 503 || attempt == 5) throw new RuntimeException("launch " + res.statusCode() + " " + res.body());
Thread.sleep(Long.parseLong(res.headers().firstValue("retry-after").orElse("5")) * 1000);
}
}
// The exit the site sees. Shared proxies drop the odd connection: retry a few times.
static JsonObject exitOf(Page page) {
for (int i = 1; ; i++) {
try {
page.navigate("http://ip-api.com/json", new Page.NavigateOptions().setTimeout(30000));
JsonObject j = JsonParser.parseString(page.textContent("body")).getAsJsonObject();
JsonObject out = new JsonObject();
out.addProperty("ip", j.get("query").getAsString());
out.addProperty("country", j.get("countryCode").getAsString());
return out;
} catch (RuntimeException err) {
if (i == 4) throw err;
page.waitForTimeout(2000 * i);
}
}
}
static JsonObject session(Playwright pw, JsonObject extra, Function<Page, JsonObject> work) throws Exception {
JsonObject body = new JsonObject();
body.addProperty("headless", true);
body.addProperty("record", false);
for (Map.Entry<String, JsonElement> e : extra.entrySet()) body.add(e.getKey(), e.getValue());
JsonObject s = launch(body);
Browser browser = pw.chromium().connect(s.get("wsUrl").getAsString(), new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
JsonObject out = work.apply(browser.newPage());
out.add("weight", s.get("weight"));
return out;
} finally {
browser.close(); // ends the session and stops the clock
}
}
public static void main(String[] args) throws Exception {
// 1. What the free plan includes.
HttpResponse<String> meRes = HTTP.send(HttpRequest.newBuilder(URI.create("https://cdpfleet.com/v1/me")).header("x-api-key", KEY).build(), HttpResponse.BodyHandlers.ofString());
JsonObject sub = JsonParser.parseString(meRes.body()).getAsJsonObject().getAsJsonObject("subscription");
JsonObject features = sub.getAsJsonObject("features");
JsonObject plan = new JsonObject();
plan.add("plan", sub.get("plan_id"));
plan.add("threads", sub.get("threads"));
plan.add("minutes_per_thread_per_day", sub.get("daily_minutes_per_thread"));
plan.addProperty("max_session_minutes", sub.get("max_session_seconds").getAsInt() / 60);
plan.add("max_pages", features.get("max_pages"));
plan.add("memory_mb", features.get("memory_mb"));
plan.add("proxy_options", sub.has("plan_proxy") ? sub.getAsJsonObject("plan_proxy").get("options") : JsonNull.INSTANCE);
try (Playwright pw = Playwright.create()) {
// 2. No proxy in the launch: the plan's included proxy is used. Scrape a catalogue page.
JsonObject first = session(pw, new JsonObject(), page -> {
JsonObject out = new JsonObject();
out.add("exit", exitOf(page));
page.navigate("https://books.toscrape.com/", new Page.NavigateOptions().setTimeout(45000));
Object books = page.evalOnSelectorAll("article.product_pod", "els => els.map(e => ({ title: e.querySelector('h3 a').getAttribute('title'), price: e.querySelector('.price_color').textContent.trim() }))");
JsonArray list = GSON.toJsonTree(books).getAsJsonArray();
out.addProperty("books", list.size());
out.add("first_book", list.get(0));
return out;
});
// 3. A cdpfleet-plan token picks a country and keeps the same exit across sessions.
String token = "cdpfleet-plan-country-de-session-c47" + UUID.randomUUID().toString().substring(0, 5) + "-lifetime-10";
JsonObject tokenBody = new JsonObject();
tokenBody.addProperty("proxy", token);
JsonObject a = session(pw, tokenBody, page -> { JsonObject o = new JsonObject(); o.add("exit", exitOf(page)); return o; });
JsonObject b = session(pw, tokenBody, page -> { JsonObject o = new JsonObject(); o.add("exit", exitOf(page)); return o; });
JsonObject defaultProxy = new JsonObject();
defaultProxy.add("weight", first.get("weight"));
defaultProxy.add("exit_country", first.getAsJsonObject("exit").get("country"));
defaultProxy.add("exit_ip", first.getAsJsonObject("exit").get("ip"));
defaultProxy.add("books", first.get("books"));
defaultProxy.add("first_book", first.get("first_book"));
JsonObject planToken = new JsonObject();
planToken.addProperty("token", token.replaceAll("session-\\w+", "session-…"));
planToken.add("first_country", a.getAsJsonObject("exit").get("country"));
planToken.add("second_country", b.getAsJsonObject("exit").get("country"));
planToken.addProperty("same_ip", a.getAsJsonObject("exit").get("ip").equals(b.getAsJsonObject("exit").get("ip")));
JsonObject result = new JsonObject();
result.add("plan", plan);
result.add("default_proxy", defaultProxy);
result.add("plan_token", planToken);
System.out.println(GSON.toJson(result));
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: COMMUNITY_API_KEY
using System.Net.Http.Json;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("COMMUNITY_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async Task<JsonElement> Launch(JsonObject body)
{
for (var attempt = 1; ; attempt++)
{
var res = await http.PostAsync("https://starter.cdpfleet.com/chromium/session",
new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
if (res.IsSuccessStatusCode) return await res.Content.ReadFromJsonAsync<JsonElement>();
if ((int)res.StatusCode != 503 || attempt == 5) throw new Exception($"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
await Task.Delay((int)(res.Headers.RetryAfter?.Delta?.TotalMilliseconds ?? 5000));
}
}
// The exit the site sees. Shared proxies drop the odd connection: retry a few times.
async Task<(string Ip, string Country)> ExitOf(IPage page)
{
for (var i = 1; ; i++)
{
try
{
await page.GotoAsync("http://ip-api.com/json", new() { Timeout = 30000 });
var j = JsonDocument.Parse(await page.TextContentAsync("body") ?? "{}").RootElement;
return (j.GetProperty("query").GetString()!, j.GetProperty("countryCode").GetString()!);
}
catch (Exception) when (i < 4)
{
await page.WaitForTimeoutAsync(2000 * i);
}
}
}
async Task<(int Weight, T Result)> Session<T>(JsonObject extra, Func<IPage, Task<T>> work)
{
var body = new JsonObject { ["headless"] = true, ["record"] = false };
foreach (var (k, v) in extra) body[k] = v?.DeepClone();
var s = await Launch(body);
var browser = await playwright.Chromium.ConnectAsync(s.GetProperty("wsUrl").GetString()!, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
return (s.GetProperty("weight").GetInt32(), await work(await browser.NewPageAsync()));
}
finally
{
await browser.CloseAsync(); // ends the session and stops the clock
}
}
// 1. What the free plan includes.
var sub = (await http.GetFromJsonAsync<JsonElement>("https://cdpfleet.com/v1/me")).GetProperty("subscription");
var plan = new JsonObject
{
["plan"] = sub.GetProperty("plan_id").GetString(),
["threads"] = sub.GetProperty("threads").GetInt32(),
["minutes_per_thread_per_day"] = sub.GetProperty("daily_minutes_per_thread").GetInt32(),
["max_session_minutes"] = sub.GetProperty("max_session_seconds").GetInt32() / 60,
["max_pages"] = sub.GetProperty("features").GetProperty("max_pages").GetInt32(),
["memory_mb"] = sub.GetProperty("features").GetProperty("memory_mb").GetInt32(),
["proxy_options"] = sub.TryGetProperty("plan_proxy", out var pp) ? JsonNode.Parse(pp.GetProperty("options").GetRawText()) : null,
};
// 2. No proxy in the launch: the plan's included proxy is used. Scrape a catalogue page.
var first = await Session(new JsonObject(), async page =>
{
var exit = await ExitOf(page);
await page.GotoAsync("https://books.toscrape.com/", new() { Timeout = 45000 });
var books = await page.EvalOnSelectorAllAsync<JsonElement>("article.product_pod",
"els => els.map(e => ({ title: e.querySelector('h3 a').getAttribute('title'), price: e.querySelector('.price_color').textContent.trim() }))");
return (exit, books);
});
// 3. A cdpfleet-plan token picks a country and keeps the same exit across sessions.
var token = $"cdpfleet-plan-country-de-session-c47{Guid.NewGuid().ToString("N")[..5]}-lifetime-10";
var a = await Session(new JsonObject { ["proxy"] = token }, ExitOf);
var b = await Session(new JsonObject { ["proxy"] = token }, ExitOf);
var output = new JsonObject
{
["plan"] = plan,
["default_proxy"] = new JsonObject
{
["weight"] = first.Weight,
["exit_country"] = first.Result.exit.Country,
["exit_ip"] = first.Result.exit.Ip,
["books"] = first.Result.books.GetArrayLength(),
["first_book"] = JsonNode.Parse(first.Result.books[0].GetRawText()),
},
["plan_token"] = new JsonObject
{
["token"] = Regex.Replace(token, @"session-\w+", "session-…"),
["first_country"] = a.Result.Country,
["second_country"] = b.Result.Country,
["same_ip"] = a.Result.Ip == b.Result.Ip,
},
};
Console.WriteLine(output.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: COMMUNITY_API_KEY
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("COMMUNITY_API_KEY")
// 503 means the fleet is momentarily busy: wait Retry-After and try again.
func launch(opts map[string]any) (map[string]any, error) {
body, _ := json.Marshal(opts)
for attempt := 1; ; attempt++ {
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/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
}
b, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == 200 {
var session map[string]any
return session, json.Unmarshal(b, &session)
}
if res.StatusCode != 503 || attempt == 5 {
return nil, fmt.Errorf("launch %d %s", res.StatusCode, strings.TrimSpace(string(b)))
}
wait, err := strconv.Atoi(res.Header.Get("retry-after"))
if err != nil {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
}
}
type exit struct{ IP, Country string }
// The exit the site sees. Shared proxies drop the odd connection: retry a few times.
func exitOf(page playwright.Page) (exit, error) {
for i := 1; ; i++ {
_, err := page.Goto("http://ip-api.com/json", playwright.PageGotoOptions{Timeout: playwright.Float(30000)})
if err == nil {
var text string
if text, err = page.TextContent("body"); err == nil {
var j struct {
Query string `json:"query"`
CountryCode string `json:"countryCode"`
}
if err = json.Unmarshal([]byte(text), &j); err == nil {
return exit{j.Query, j.CountryCode}, nil
}
}
}
if i == 4 {
return exit{}, err
}
page.WaitForTimeout(float64(2000 * i))
}
}
// One session: launch, connect, run work on a new page, close (ends the session and the clock).
func session(pw *playwright.Playwright, extra map[string]any, work func(playwright.Page) error) (int, error) {
opts := map[string]any{"headless": true, "record": false}
for k, v := range extra {
opts[k] = v
}
s, err := launch(opts)
if err != nil {
return 0, err
}
browser, err := pw.Chromium.Connect(s["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
return 0, err
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
return 0, err
}
return int(s["weight"].(float64)), work(page)
}
type book struct {
Title string `json:"title"`
Price string `json:"price"`
}
func run() error {
// 1. What the free plan includes.
req, _ := http.NewRequest("GET", "https://cdpfleet.com/v1/me", nil)
req.Header.Set("x-api-key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
var me struct {
Subscription struct {
PlanID string `json:"plan_id"`
Threads int `json:"threads"`
Minutes int `json:"daily_minutes_per_thread"`
MaxSess int `json:"max_session_seconds"`
Features struct {
MaxPages int `json:"max_pages"`
MemoryMB int `json:"memory_mb"`
} `json:"features"`
PlanProxy *struct {
Options map[string]bool `json:"options"`
} `json:"plan_proxy"`
} `json:"subscription"`
}
err = json.NewDecoder(res.Body).Decode(&me)
res.Body.Close()
if err != nil {
return err
}
sub := me.Subscription
var options map[string]bool
if sub.PlanProxy != nil {
options = sub.PlanProxy.Options
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
return err
}
defer pw.Stop()
// 2. No proxy in the launch: the plan's included proxy is used. Scrape a catalogue page.
var first exit
var books []book
weight, err := session(pw, map[string]any{}, func(page playwright.Page) error {
var err error
if first, err = exitOf(page); err != nil {
return err
}
if _, err = page.Goto("https://books.toscrape.com/", playwright.PageGotoOptions{Timeout: playwright.Float(45000)}); err != nil {
return err
}
raw, err := page.EvalOnSelectorAll("article.product_pod", "els => els.map(e => ({ title: e.querySelector('h3 a').getAttribute('title'), price: e.querySelector('.price_color').textContent.trim() }))")
if err != nil {
return err
}
b, _ := json.Marshal(raw)
return json.Unmarshal(b, &books)
})
if err != nil {
return err
}
// 3. A cdpfleet-plan token picks a country and keeps the same exit across sessions.
rnd := make([]byte, 3)
rand.Read(rnd)
token := "cdpfleet-plan-country-de-session-c47" + hex.EncodeToString(rnd)[:5] + "-lifetime-10"
var a, b exit
if _, err = session(pw, map[string]any{"proxy": token}, func(p playwright.Page) (err error) { a, err = exitOf(p); return }); err != nil {
return err
}
if _, err = session(pw, map[string]any{"proxy": token}, func(p playwright.Page) (err error) { b, err = exitOf(p); return }); err != nil {
return err
}
// Field order matches the other languages' output.
type planOut struct {
Plan string `json:"plan"`
Threads int `json:"threads"`
Minutes int `json:"minutes_per_thread_per_day"`
MaxSessMin int `json:"max_session_minutes"`
MaxPages int `json:"max_pages"`
MemoryMB int `json:"memory_mb"`
ProxyOptions map[string]bool `json:"proxy_options"`
}
out := struct {
Plan planOut `json:"plan"`
DefaultProxy struct {
Weight int `json:"weight"`
ExitCountry string `json:"exit_country"`
ExitIP string `json:"exit_ip"`
Books int `json:"books"`
FirstBook book `json:"first_book"`
} `json:"default_proxy"`
PlanToken struct {
Token string `json:"token"`
FirstCountry string `json:"first_country"`
SecondCountry string `json:"second_country"`
SameIP bool `json:"same_ip"`
} `json:"plan_token"`
}{}
out.Plan = planOut{sub.PlanID, sub.Threads, sub.Minutes, sub.MaxSess / 60, sub.Features.MaxPages, sub.Features.MemoryMB, options}
out.DefaultProxy.Weight, out.DefaultProxy.ExitCountry, out.DefaultProxy.ExitIP, out.DefaultProxy.Books = weight, first.Country, first.IP, len(books)
if len(books) > 0 {
out.DefaultProxy.FirstBook = books[0]
}
out.PlanToken.Token = regexp.MustCompile(`session-\w+`).ReplaceAllString(token, "session-…")
out.PlanToken.FirstCountry, out.PlanToken.SecondCountry, out.PlanToken.SameIP = a.Country, b.Country, a.IP == b.IP
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
return err
}
fmt.Print(buf.String())
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
What we got
| Check | Result |
|---|---|
| Plan | community_60: 2 threads × 60 min/day, sessions ≤ 15 min, 5 pages / 3 GB per session |
| Included proxy options | country, session, lifetime |
| Launch without a proxy | weight 1 (headless), exit US |
| Scraped | 20 books, first: "A Light in the Attic" £51.77 |
| `cdpfleet-plan` country + session | DE then DE, same IP |
From the Node.js run on 2026-10-15. 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)
{
"plan": {
"plan": "community_60",
"threads": 2,
"minutes_per_thread_per_day": 60,
"max_session_minutes": 15,
"max_pages": 5,
"memory_mb": 3072,
"proxy_options": {
"country": true,
"os": false,
"session": true,
"lifetime": true
}
},
"default_proxy": {
"weight": 1,
"exit_country": "US",
"exit_ip": "203.0.113.1",
"books": 20,
"first_book": {
"title": "A Light in the Attic",
"price": "£51.77"
}
},
"plan_token": {
"token": "cdpfleet-plan-country-de-session-…-lifetime-10",
"first_country": "DE",
"second_country": "DE",
"same_ip": true
}
}Takeaways
- No proxy needed: the launch without
proxywent out through the plan's shared proxy and scraped the page as usual. Leaveproxyout on Community, or send your own proxy or a residential token when you need one. - Country and sticky exits work on the free proxy: the
cdpfleet-plantoken exited in Germany both times, from the same IP — a second browser that reuses the session name keeps the identity. - The limits, from the API: 2 threads (a headless browser takes 1, a headful one 2), 60 browser-minutes per thread a day, sessions of up to 15 minutes, 5 open pages and 3 GB per session. The included proxy is shared by every Community user, so expect it to be slower at peak times.
- What Community doesn't include: session recordings, persistent profiles and
keep_alive. The plan is invite-only for now — join the waitlist — and request data from Community sessions may be used to learn which browser setups work on which sites, so don't sign in to accounts on it (Terms 4A). Details: Community plan.