[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #47 · 2026-10-15 · Example · Easy · for [web scraping](https://cdpfleet.com/web-scraping), [geo testing](https://cdpfleet.com/geo-testing)

# 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.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

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

1.  Read the plan from `GET /v1/me`.
2.  Launch headless Chromium without a proxy, check the exit country, and scrape the 20 books on the first page of books.toscrape.com.
3.  Launch twice more with the same `cdpfleet-plan` country + session token and compare the two exits.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/47-community-first-scrape), with the raw output). Set these environment variables first:

-   `COMMUNITY_API_KEY` — an API key of an account on the free [Community plan](https://cdpfleet.com/docs/pricing#community) (dashboard → API keys); its launches use the plan's included proxy

 

```node
// npm install playwright@1.60.0
// 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));
```

```python
# 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))
```

```java
// 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));
    }
  }
}
```

```csharp
// 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
// go get github.com/playwright-community/playwright-go@v0.6000.0
// 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 `proxy` went out through the plan's shared proxy and scraped the page as usual. Leave `proxy` out on Community, or send your own proxy or a [residential](https://cdpfleet.com/docs/proxies#residential) token when you need one.
-   **Country and sticky exits work on the free proxy:** the `cdpfleet-plan` token 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, 50 open pages and 2 GB per session (since 2026-10-29; 5 pages and 3 GB when first measured). 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](https://cdpfleet.com/#join) — 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](https://cdpfleet.com/terms)). Details: [Community plan](https://cdpfleet.com/docs/pricing#community).

---
Source: https://cdpfleet.com/docs/cases/community-first-scrape · Markdown: https://cdpfleet.com/docs/cases/community-first-scrape.md · All docs for AI tools: https://cdpfleet.com/llms.txt
